大开关语句:糟糕的OOP?
我一直认为,大开关语句是不良OOPdevise的一个症状。 过去,我已经阅读了一些讨论这个话题的文章,他们提供了基于OOP的方法,通常基于多态来实例化正确的对象来处理这个问题。
我现在处于一种基于来自TCP套接字的数据stream的怪异开关语句的情况,其中协议基本上由换行符终止的命令组成,接着是数据行,随后是结束标记。 该命令可以是100个不同的命令之一,所以我想find一种方法来减less这个怪物开关语句更可pipe理。
我已经做了一些search,以find我记得的解决scheme,但不幸的是,谷歌已经成为许多种查询这些天无关结果的荒原。
有这种问题的模式吗? 任何可能的实施build议?
一个想法是使用字典查找,将命令文本匹配到要实例化的对象types。 这具有很好的优点,只需创build一个新的对象并在表中插入一个新的命令/types就可以获得任何新的命令。
但是,这也有types爆炸的问题。 我现在需要100个新的类,再加上我必须find一种方法来将它们干净地连接到数据模型。 “一个真正的转换声明”是否真的要走?
我会感激你的想法,意见或评论。
你可能从命令模式中获得一些好处。
对于OOP,如果行为变化足够小,可以将几个类似的命令折叠成一个类,以避免完全的类爆炸(是的,我可以听到OOP的大师已经尖叫了)。 但是,如果系统已经是面向对象的,而且每一百个命令都是独一无二的,那么就让它们成为唯一的类并利用inheritance来巩固常见的东西。
如果系统不是OOP,那么我不会为此添加OOP …您可以通过简单的字典查找和函数指针轻松使用Command Pattern,甚至可以根据命令名称dynamic生成函数调用,具体取决于语言。 然后,您可以将逻辑上相关的函数分组到代表类似命令集合的库中,以实现可pipe理的分离。 我不知道这种实现是否有一个好的术语…我一直认为它是基于MVC处理URL的“调度器”风格。
我发现有两个 switch语句作为非OOdevise的一个症状,其中enumtypes的开关可能被replace为提供抽象接口的不同实现的多个types; 例如,以下…
switch (eFoo) { case Foo.This: eatThis(); break; case Foo.That: eatThat(); break; } switch (eFoo) { case Foo.This: drinkThis(); break; case Foo.That: drinkThat(); break; }
也许应该改写为…
IAbstract { void eat(); void drink(); } class This : IAbstract { void eat() { ... } void drink() { ... } } class That : IAbstract { void eat() { ... } void drink() { ... } }
但是, 一个转换语句并不是一个强有力的指示,转换语句应该被别的东西替代。
该命令可以是100个不同的命令之一
如果你需要做100个不同的事情,你不能避免有一个100路分支。 您可以在控制stream(switch,if-elseif ^ 100)或数据(从string到命令/工厂/策略的100元素映射)中对其进行编码。 但它会在那里。
你可以尝试将100路分支的结果与不需要知道结果的事情分开。 也许只有100种不同的方法是好的。 没有必要发明你不需要的东西,如果这使得代码笨重。
我认为这是less数情况下大交换机是最好的答案,除非一些其他的解决scheme出现的情况之一。
我看到了战略模式。 如果我有100个不同的策略…就这样吧。 巨大的开关语句是丑陋的。 所有的命令都是有效的类名吗? 如果是这样,只需使用命令名称作为类名称,并使用Activator.CreateInstance创build策略对象。
谈到一个大的switch语句时,有两件事情浮现在脑海中:
- 这违反了OCP – 你可以继续保持一个很大的function。
- 你可能会有不好的performance:O(N)。
另一方面,一个映射实现可以符合OCP,并且可能执行O(1)。
我会说这个问题不是大的开关语句,而是包含在其中的代码泛滥,滥用了错误的范围variables。
我自己在一个项目中体验了这个,当越来越多的代码进入交换机,直到它变得不可维护。 我的解决scheme是定义参数类,其中包含命令的上下文(名称,参数,在开关之前收集的任何内容),为每个case语句创build一个方法,并从case中调用该参数对象。
当然,一个完全的OOP命令调度器(基于像Java Activation这样的魔法reflection或者机制)更加美观,但是有时候你只是想修复一些东西而完成工作;)
你可以使用一个字典(如果你用Java编码的话,也可以使用散列图)(Steve McConnell称之为表驱动开发)。
我看到你可以改进这种方式,使你的代码驱动的数据,所以例如每个代码你匹配处理它的东西(函数,对象)。 您也可以使用reflection映射表示对象/函数的string,并在运行时parsing它们,但是您可能需要做一些实验来评估性能。
处理这个特定问题的最好方法是:序列化和协议干净利落地使用IDL,并用switch语句生成封送代码。 因为无论你尝试使用哪种模式(原型工厂,命令模式等),你都需要初始化一个命令ID /string和类/函数指针之间的映射,不知何故,它会比开关语句慢,因为编译器可以使用完美的哈希查找开关语句。
是的,我认为大的病例陈述是一个症状,可以改善代码…通常通过实施更加面向对象的方法。 例如,如果我发现自己正在评估switch语句中类的types,那几乎总是意味着我可能会使用generics来消除switch语句。
你也可以在这里采取一种语言的方法,并用语法中的相关数据定义命令。 然后,您可以使用生成器工具来分析语言。 为此,我使用了反讽 。 或者,您可以使用解释器模式。
在我看来,目标不是要构build最纯粹的面向对象模型,而是要创build一个灵活的,可扩展的,可维护和强大的系统。
我最近有一个类似的问题与一个巨大的开关语句,我摆脱了丑陋的开关最简单的解决scheme查找表和一个函数或方法返回您所期望的值。 命令模式是很好的解决scheme,但有100课是不好,我认为。 所以我有这样的东西:
switch(id) case 1: DoSomething(url_1) break; case 2: DoSomething(url_2) break; .. .. case 100 DoSomething(url_100) break;
我改变了:
string url = GetUrl(id); DoSomthing(url);
GetUrl可以去DB,并返回你正在寻找的url,或者可能是一个字典在内存中的100个url。 我希望这可以帮助任何人在那里取代巨大的怪异开关语句。
想想Windows最初是如何写在应用程序消息泵中的。 它吸了。 应用程序将运行更慢,您添加更多的菜单选项。 随着search的命令越来越接近switch语句的底部,响应的等待时间越来越长。 长时间切换语句是不可接受的。 我做了一个AIX守护进程作为一个POS命令处理程序,它可以处理256个独特的命令,而不必知道通过TCP / IP接收到的请求stream中的内容。 stream的第一个字符是一个函数数组的索引。 任何未使用的索引被设置为默认的消息处理程序; login并说再见。