【问题标题】:How can a switch statement be avoided if the return type is unknown by the calling method?如果调用方法不知道返回类型,如何避免 switch 语句?
【发布时间】:2011-05-23 07:53:42
【问题描述】:

我一直在读一本名为Clean Code A Handbook of Agile Software Craftsmanship 的书。书中的作者主张应避免使用 switch 语句,如果无法避免,则应将其归为工厂方法。我有一个连接对象,它正在接收各种 PDU(协议数据单元)。 PDU 各不相同,可以按任何顺序接收。因此,如果我有一个方法,例如:

public BasePdu ReceiveNext();

因为在完全接收到数据包之前,我无法判断它是什么类型。在 PDU 的报头中是一个关于它应该是哪种类型的标识符。这意味着调用方法将确定 PDU 的类型,并基于调用相关方法来处理它。这听起来像是 switch 语句的完美示例。包含连接的对象我希望有两个线程。一个用于接收 PDU,另一个用于处理要发送的 PDU 队列。

现在我知道你不可能遵循每一个好的建议,而且只有一些情况是规则的例外。这是其中之一吗?或者有没有我还没有想到的解决方法。

更新:

通过创建响应处理程序的子类,我听到很多人在说什么。问题是包含对象有很多上下文和处理程序需要的附加信息,例如查找和节流等。将所有这些信息注入处理程序的子类将是一项相当繁琐的维护工作,并且还会拆分一个当被封装在它现在所在的对象中感觉更好时,就会增加很多逻辑。

【问题讨论】:

标签: c# oop design-patterns


【解决方案1】:

我个人不会太担心;如果它看起来像是一个 switch 语句的好地方,请使用一个。另一方面,如果每个 PDU 类型由类而不是方法处理,这看起来也像是可以使用工厂方法的情况。而且,根据你的书,你可以使用 switch 语句

【讨论】:

    【解决方案2】:

    只需创建一个 PDUParserFactory,它使用 PDU 类型标识符上的 switch 语句创建基于 PDU 类型的解析器。这是书上说没问题的情况:)

    更新:一种可能的方法

     class BasePDU
     {
         string Name { get; set; }
         ...
     }
    
     class PDUType1 : BasePDU
     {
         ...
     }
     ...
    
     class PDUReceiver
     {
         public event EventHandler<PDUReceivedEventArgs> PDUReceived;
    
         private void ParsePDU(byte[] data)
         {
              BasePDU pdu;
              switch (byte[0]) // PDU type selector
              {
                  ....    parse PDU based on type
              }
    
              OnPDUReceived(pdu);
         }
    
          private void OnPDUReceived(BasePDU pdu)
          {
               var handler = PDUReceived;
               if (handler != null)
               {
                    handler(this, new PDUReceivedEventArgs(pdu));
               }
          }
     }
    

    然后您可以将侦听器附加到事件:

     pduReceiver.PDUReceived += BaseHandler;
     pduReceiver.PDUReceived += PDUType1Handler;
     ...
    
     void PDUType1Handler(object sender, PDUReceivedEventArgs e)
     {
          // only care about PDUType1
          if (e.PDU.GetType() != typeof(PDUType1))
                return;
          ....
     }
    

    或者,您也可以在接收器中创建事件处理程序字典,将 pdu 类型映射到事件处理程序,然后让处理程序仅注册特定类型。这样就不会为每个接收到的 PDU 调用所有处理程序。

    除了增加 PDU 类型层次结构,您还可以只拥有一个:

     class PDU
     {
          public PDUType PDUType { get; }
          public byte[] PDUData { get }
     }
    

    然后在接收器中为每个PDUType 注册处理程序,并让处理程序对数据做任何事情。

    如果不知道您到底想对收到的数据包做什么,很难给出更具体的建议。

    【讨论】:

    • 但是调用代码仍然不知道将其转换为哪个类?所以我会被困在同一个位置,除非我错过了你的意思。
    • @uriDium:稍微更新了答案
    【解决方案3】:

    如果我正确理解您的问题,您确实有两个问题:


    当您收到名称而不使用switch 时如何创建正确的 PDU。

    使用字典Dictionary&lt;string, Func&lt;PduBase&gt;&gt;创建一个简单的工厂


    调用public BasePdu ReceiveNext();的方法如何在不使用switch的情况下正确处理

    不要使用RecieveNext 方法。为接收所有 PDU 的类创建一个AddPduHandler&lt;T&gt;(IPduHandler&lt;T&gt; handler) where T : PduBase 方法。将所有处理程序存储在字典中,类型为键:Dictionary&lt;Type, Delegate&gt;

    存储委托是一种技巧,因为您不能在接收类中使用类型化接口。

    更新

    此解决方案不会违反所有使用 switch 的实现的 Liskovs 替换原则。这意味着无论你有多少不同类型的 PDU,这个类都可以工作。

    测试您的应用程序也更容易,因为每个处理程序都与其他所有处理程序隔离。

    好处是所有东西都是类型化的(除了阅读器类),这将更容易找到错误,而不是使用施法魔法等。

        public class Receiver
        {
            Dictionary<Type, MethodMapping> _handlers = new Dictionary<Type, MethodMapping>();
            Dictionary<string, Func<PduBase>> _factories = new Dictionary<string, Func<PduBase>>();
    
            // Small container making it easier to invoke each handler
            // also needed since different generic types cannot be stored in the same 
            // dictionary
            private class MethodMapping
            {
                public object Instance { get; set; }
                public MethodInfo Method { get; set; }
                public void Invoke(PduBase pdu)
                {
                    Method.Invoke(Instance, new[] {pdu});
                }
            }
    
            // add a method used to create a certain PDU type
            public void AddFactory(string name, Func<PduBase> factoryMethod)
            {
                _factories.Add(name, factoryMethod);
            }
    
            // register a class that handles a specific PDU type
            // we need to juggle a bit with reflection to be able to invoke it
            // hence everything is type safe outside this class, but not internally.
            // but that should be a sacrifice we can live with.
            public void Register<T>(IPduHandler<T> handler) where T : PduBase
            {
                var method = handler.GetType().GetMethod("Handle", new Type[] { typeof(T) });
                _handlers.Add(typeof(T), new MethodMapping{Instance = handler, Method = method});
            }
    
            // fake that we've received a new PDU
            public void FakeReceive(string pduName)
            {
               // create the PDU using the factory method
                var pdu = _factories[pduName]();
    
                // and invoke the handler.
                _handlers[pdu.GetType()].Invoke(pdu);
            }
        }
    
        public interface IPduHandler<in T> where T: PduBase
        {
            void Handle(T pdu);
        }
    
        public class TempPdu : PduBase
        {}
    
        public class TempPduHandler : IPduHandler<TempPdu>
        {
            public void Handle(TempPdu pdu)
            {
                Console.WriteLine("Handling pdu");
            }
        }
    
        public class PduBase
        { }
    
    
    
        private static void Main(string[] args)
        {
            Receiver r = new Receiver();
            r.AddFactory("temp", () => new TempPdu());
            r.Register(new TempPduHandler());
    
            // we've recieved a PDU called "temp". 
            r.FakeReceive("temp");
        }
    

    【讨论】:

      【解决方案4】:

      避免使用 swith 语句的原因不是因为 if 结构更好(当使用 switch 时,一堆 if 会使情况变得更糟,而不是更好),主要是因为问题没有以 OO 的方式解决。

      从 OO 的角度来看,使用多态几乎总是比使用 switch 语句更好。

      在您的示例中,最好使用工厂方法为您的包类型提供适当的处理程序。

      【讨论】:

        【解决方案5】:

        不确定这是否正是重点,但根据 ID 处理不同的不同实例实际上是创建子类(表示先前存储在 ID 中的信息的子类的选择)的情况,例如 BasePdu 和让编译器确定使用哪种方法。如果您通过切换来做到这一点,则意味着您没有充分利用通过子类化构建代码的优势。

        【讨论】:

        • 这里的问题是,给定一堆具有 ID 的数据,如何为该数据使用哪个子类。
        • @Gabe:创建一个 PDUParserFactory,它使用 PDU 类型标识符上的 switch 语句创建基于 PDU 类型的解析器
        • @Chris:您应该将其发布为答案,而不是评论。
        猜你喜欢
        • 1970-01-01
        • 2015-08-12
        • 1970-01-01
        • 1970-01-01
        • 2019-01-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多