大型Switch语句:糟糕的OOP?


77

我一直认为,大型switch语句是OOP设计不良的征兆。过去,我已经阅读了讨论该主题的文章,他们提供了基于OOP的替代方法,通常基于多态来实例化处理案例的正确对象。

我现在处在一个奇怪的switch语句的情况下,该语句基于来自TCP套接字的数据流,该协议中的协议基本上由换行符终止命令,随后的数据行和结束标记组成。该命令可以是100个不同命令中的一个,因此我想找到一种方法来将此怪物切换语句减少为更易于管理的内容。

我已经进行了一些谷歌搜索来找到我记得的解决方案,但是可悲的是,如今,对于许多类型的查询,谷歌已经变成了无关紧要的结果。

有没有解决此类问题的模式?关于可能的实现有什么建议吗?

我曾经想到的是使用字典查找,将命令文本与要实例化的对象类型进行匹配。这具有仅创建一个新对象并为任何新命令在表中插入新命令/类型的优点。

但是,这也存在类型爆炸的问题。现在,我需要100个新类,此外,我还必须找到一种将它们与数据模型完全接口的方法。“一个真正的转换声明”真的可行吗?

非常感谢您的想法,意见或评论。


使用表情怎么样?因此,通过函数名称查找函数并调用它。名称可以很容易地与传递的变量匹配,因此我们不需要使用switch。
创作者

Answers:


34

您可以从Command Pattern中获得一些好处。

对于OOP,如果行为变化足够小,则可以将几个类似的命令折叠到一个类中,以避免整个类爆炸(是的,我已经听说过OOP专家已经对此大喊大叫了)。但是,如果系统已经是OOP,并且100多个命令中的每一个都是真正唯一的,则只需使它们成为唯一的类并利用继承的优势来合并常用的东西。

如果系统不是OOP,那么我不会仅为此添加OOP ...您可以轻松地通过简单的字典查找和函数指针使用Command Pattern,甚至可以根据命令名称动态生成函数调用,具体取决于语言。然后,您可以将逻辑上关联的函数分组到代表相似命令集合的库中,以实现可管理的分离。我不知道这种实现是否有很好的用语……我一直认为它是一种基于MVC处理URL的“调度程序”风格。


同意字典和代表的想法。在大型switch语句上使用Dictionary <string,Action>可能还会获得性能改进。困难在于初始化它,尽管可以根据需要使用反射将其自动化。
Zooba

也许可以从配置中取消初始化。
davogones,2009年

反射的问题是,许多Web主机以信任级别运行,不允许您使用它:(

有趣的是,如果分支之间的唯一区别是它们是基础的不同子类,那么您基本上是在描述工厂。
Thedric Walker,2009年

24

我看到有两个switch语句是非OO设计的症状,其中enum-switch类型可以用提供抽象接口的不同实现的多种类型替换;例如,以下...

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() { ... }
}

但是,一个switch语句不是imo的有力指示,因此该switch语句应替换为其他内容。


8
问题是您不知道您是否通过网络收到“此”或“该”命令。您必须在某个地方决定是调用新的This()还是新的That()。之后,用OO进行抽象是小菜一碟。

17

该命令可以是100个不同命令之一

如果您需要从100种不同的事情中完成一项,则无法避免拥有100条分支。您可以在控制流(开关,if-elseif ^ 100)或数据(从字符串到命令/工厂/策略的100个元素的映射)中对其进行编码。但是它将在那里。

您可以尝试将100向分支的结果与不需要知道该结果的事物区分开。也许只有100种不同的方法都可以。如果这会使代码变得笨拙,则无需发明不需要的对象。


10
+1。当听到人们抱怨交换机臭味时,我会感到沮丧,但随后提出了一个替代方案,它实际上只是抽象的交换机行为在整个系统中抹去了,而不是整齐的堆栈。

1
这可能是很古怪的,但是地图(字符串->命令)真的是一个分支吗?从概念上讲,可能会发生多种情况,具体取决于变量的值,但是当我想到分支时,我想到的是明确检查变量中可能值的事情。而地图是直接查找。换句话说,条件与结果的关联在开始时(在地图总体上)执行一次,而不是每次(通过测试变量)进行。在该定义下,命令映射确实删除了该分支。
卡姆·杰克逊

1
@CamJackson:当然,如果您使用地图,则可以将其与本身不做任何分支的代码一起使用-它已将分支外包给map_lookup它调用的任何函数(该函数将进行分支)。我不确定这种观察能为您带来什么。“如果发生在其他地方,它实际上不是分支”?输入和输出之间的关联是在编译时为switch(和其他控制流)语句完成的,而在运行时为数据结构(很可能是)完成的。并不是开关的形状在运行时发生变化。两种查找都需要(并且一个是)分支。希望对您
有所

@JonasKölker很好。我想我没有仔细考虑实际的数据结构及其工作方式。因此,删除分支的唯一真实方法是将您的100个命令方便地恰好称为0、1、2等,以便您可以将它们直接用作索引。
坎克·杰克逊

@CamJackson:我想我同意-在那种情况下,唯一会发生分支的原因是,存储在100个元素的数组中的内容将被放入程序计数器中(在Java中,这是间接发生在后面的场景)。在这里,我定义分支是指在程序计数器假定一个特定值之后,它可以假定多个不同的下一个值(在正常程序执行下,没有宇宙射线)。(总挑剔:如果您的命令分别命名为0..17和19..100,则可能具有101个元素的数组A,其中A [18]是错误处理程序。您可以进一步推广。)
JonasKölker


2

我看到了策略模式。如果我有100种不同的策略,那就这样吧。巨大的转换声明是丑陋的。所有命令都是有效的类名吗?如果是这样,只需将命令名称用作类名称,然后使用Activator.CreateInstance创建策略对象。


2

在谈论大型switch语句时,有两件事要想到:

  1. 它违反了OCP-您可能会不断维护重要功能。
  2. 您的性能可能很差:O(n)。

另一方面,映射实现可以符合OCP,并且可能会以O(1)执行。


1
“一个映射[可以是O(1)]” -如果查找读取k位,则它可以区分2 ** k个不同的键。因此,对于n个键,您至少需要读取log(n)位。log(n)是无界的,如何在O(1)时间内做到这一点?使用哪种型号?
JonasKölker2013年

@JonasKölker:可以使用哈希表实现地图,该哈希表可以使用找到正确的密钥O(1)。看到这个答案:stackoverflow.com/a/1055261/4834
quamrana 2013年

哈希函数仍然必须处理这些log(n)位。哈希表(和某些类型的查找树)相对于所包含的值是O(1),但相对于键值的位大小则是O(n)或更糟。
CA McCann 2013年

4
任何体面的编译器都使用具有连续大小写值的switch语句表或二进制决策树(在这种情况下不起作用,或者基于对处理器体系结构的充分了解而使二进制决策树更快)使用表。对于具有许多相距很远的case语句,一个好的编译器会对该值进行散列以将其简化为一个表,如果这样做是有好处的。无论如何,编译器将使用以最快的方式实现switch语句的策略。因此,对不起,但是出于性能原因拒绝switch语句是愚蠢的。
gnasher729 2014年

1

我要说的问题不是大的switch语句,而是其中包含的代码激增以及滥用范围错误的变量。

我自己在一个项目中经历了这一点,当时越来越多的代码进入开关,直到变得难以维护。我的解决方案是在参数类上定义该类,其中包含命令的上下文(名称,参数等,在切换之前收集的内容),为每个case语句创建一个方法,然后从案例中使用参数对象调用该方法。

当然,完全的OOP命令分派器(基于诸如反射之类的魔术或Java激活之类的机制)会更漂亮,但有时您只想修复问题并完成工作;)


1

您可以使用字典(如果使用Java进行编码,则可以使用哈希图)(史蒂夫·麦康奈尔称之为表驱动开发)。


0

我认为可以改进的一种方法是使代码由数据驱动,例如,对于每个代码,您都匹配处理它的对象(函数,对象)。您还可以使用反射来映射表示对象/功能的字符串并在运行时解析它们,但是您可能需要进行一些实验以评估性能。


0

处理此特定问题的最佳方法:干净地进行序列化和协议操作是使用IDL并使用switch语句生成封送处理代码。因为无论您尝试使用其他哪种模式(原型工厂,命令模式等),您都需要初始化命令id /字符串和类/函数指针之间的映射,因此它的运行速度比switch语句慢,因为编译器可以对switch语句使用完美的哈希查找。


我很好奇什么编译器使用完美的哈希实现字符串的switch语句?
paxos1977

0

是的,我认为大写陈述是人们可以改善自己代码的一种征兆……通常可以通过实施一种更加面向对象的方法来实现。例如,如果我发现自己在switch语句中评估类的类型,那几乎总是意味着我可能可以使用泛型来消除switch语句。


1
问题在于,这些数据来自其他地方,不能一成不变地成为面向对象的。需要做一些事情才能将其转换为面向对象,通常这是一个很大的转换。
罗伦·佩希特尔

0

您还可以在此处采用一种语言方法,并在语法中定义带有相关数据的命令。然后,您可以使用生成器工具来解析语言。我已将Irony用于该目的。另外,您可以使用解释器模式。

在我看来,目标不是建立最纯粹的OO模型,而是创建一个灵活,可扩展,可维护且功能强大的系统。


0

最近,我在巨大的switch语句中遇到了类似的问题,并且通过最简单的解决方案(查找表)和返回所需值的函数或方法摆脱了丑陋的开关。命令模式是很好的解决方案,但我认为有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可以转到数据库并返回您要查找的URL,或者可以是内存中包含100个URL的字典。我希望这可以在替换庞大的switch语句时为任何人提供帮助。


0

想想Windows是如何最初在应用程序消息泵中编写的。糟透了 使用添加的更多菜单选项,应用程序将运行缓慢。随着命令搜索的结束越来越接近switch语句的底部,等待响应的时间越来越长。较长的switch语句,句点是不可接受的。我做了一个AIX守护程序作为POS命令处理程序,它可以处理256个唯一命令,甚至不知道通过TCP / IP接收到的请求流中包含什么。流的第一个字符是函数数组的索引。任何未使用的索引均设置为默认消息处理程序;登录并说再见。


2
那根本不是真的。Switch语句使用跳转表,因此不再像2000年那样打开2个项目。它是单个表查找。
Erik Funkenbusch 2014年

即使switch语句比较字符串值?
罗伯特·阿赫曼

好吧,显然这取决于字符串值的长度,但是除此之外,是的。Windows使用C,无论如何都无法打开字符串,但是在C#中可以。它仍然是一个查询表。
Erik Funkenbusch
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.