问题描述
|
我一直在阅读一本名为《清洁代码:敏捷软件技巧手册》的书。该书的作者鼓励应避免使用switch语句,如果不能避免,则应使用工厂方法。我有一个连接对象,正在接收各种PDU(协议数据单元)。 PDU有所不同,可以以任何顺序接收它们。因此,如果我有一个方法例如:
public BasePdu ReceiveNext();
因为在完全接收到数据包之前,我无法确定它是什么。 PDU的标头中有一个标识符,说明它应该是哪种类型。这意味着调用方法将要弄清PDU的类型是什么,并基于该调用来处理相关的方法。这听起来像是switch语句的完美示例。理想情况下,包含连接的对象希望有两个线程。一个用于接收PDU,另一个用于服务要发送的PDU队列。
现在我知道您不能遵循所有好的建议,并且在某些情况下是该规则的例外。这是其中之一吗?还是有一种我还没有想到的解决方法。
更新:
我听到了很多人通过创建响应处理程序的子类说的话。问题在于,包含对象具有许多上下文以及处理程序将需要的其他信息,例如查找和限制等。将所有这些信息注入到处理程序的子类中将是一件很麻烦的事情,并且还会拆分当感觉更好地封装在当前对象中时,需要进行大量的逻辑处理。
解决方法
就我个人而言,我不会对此太担心。如果看起来是进行switch语句的好地方,请使用一个。另一方面,如果每种PDU类型是由类而不是方法处理的,则这似乎也可以使用工厂方法。而且,按照您的书,您可以使用switch语句
,只需创建一个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 }
}
然后在接收器中为每个“ 4”注册处理程序,并让处理程序对数据进行任何所需的处理。
如果不知道您到底想对收到的数据包做什么,很难给出更具体的建议。
,如果我正确理解您的问题,您实际上有两个问题:
在不使用ѭ5的情况下收到名称后如何创建正确的PDU。
使用字典Dictionary<string,Func<PduBase>>
创建一个简单的工厂
调用public BasePdu ReceiveNext();
的方法如何在不使用switch
的情况下正确处理
不要使用RecieveNext
方法。为接收所有PDU的类创建一个“ 10”方法。将所有处理程序存储在类型为键的字典中:Dictionary<Type,Delegate>
存储委托是一种技巧,因为您不能在接收类中使用类型化的接口。
更新资料
此解决方案不会违反使用“ 5”的所有实现所能做到的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\");
}
,避免使用swith语句的原因并不是因为结构更好(使用开关时,如果使用一堆if会使情况变得更糟,而不是更好),主要原因是无法以OO方式解决问题。
从面向对象的角度来看,使用多态性几乎总是比使用switch语句更好。
在您的示例中,最好使用工厂方法为您的包类型提供适当的处理程序。
,不知道这是否是关键点,但是实际上要创建一个实例,例如创建BasePdu和让编译器找出使用哪种方法。如果通过切换来执行此操作,则意味着您没有充分利用子类化来构造代码的优势。