代码之家  ›  专栏  ›  技术社区  ›  oxbow_lakes

有人不同意“使用开关是糟糕的OOP风格”这句话吗?

  •  26
  • oxbow_lakes  · 技术社区  · 16 年前

    我已经看到它在stackoverflow的多个线程/注释中使用 switch 就是糟糕的OOP风格。我个人不同意这一点。

    在许多情况下,您不能将代码(即方法)添加到 enum 您希望打开的类,因为您不控制它们,可能它们在第三方JAR文件中。在其他情况下,将功能 在枚举本身上 这是一个坏主意,因为它违反了一些关注点分离的考虑,或者它实际上是其他事物的函数 以及枚举 .

    最后,开关简洁明了:

    boolean investable;
    switch (customer.getCategory()) {
        case SUB_PRIME:
        case MID_PRIME:
            investible = customer.getSavingsAccount().getBalance() > 1e6; break;
        case PRIME:
            investible = customer.isCeo(); break;
    }
    

    我不是在为 转换 我并不是说这总是我们要走的路。但在我看来,像“开关是一种代码味道”这样的说法是错误的。其他人同意吗?

    22 回复  |  直到 16 年前
        1
  •  17
  •   Dave Sherohman    16 年前

    进行后续工作:

    如果这仅仅是希望获得商业贷款的客户的“可投资性”逻辑呢?也许一个客户对另一个产品的不可归属性决定真的很不同…此外,如果有新产品一直在推出,每个产品都有不同的投资决策,我不想每次都更新我的核心客户群,那又会怎样呢?

    还有你的一条评论:

    我不完全确定是否将逻辑保存在它运行的数据附近。现实世界不是这样运作的。当我申请贷款时,银行决定我是否符合资格。他们不要求我自己做决定。

    就这一点而言,你是对的。

    boolean investable = customer.isInvestable();
    

    不是你所说的灵活性的最佳解决方案。然而,最初的问题没有提到单独的产品基类的存在。

    考虑到现有的其他信息,最佳解决方案似乎是

    boolean investable = product.isInvestable(customer);
    

    进行可投资性决策(多态性!)通过产品,根据您的“现实世界”论点,它还避免了每次添加产品时都必须创建新的客户子类。该产品可以使用任何它想要基于客户公共接口做出决定的方法。我仍然怀疑是否有适当的添加可以在客户的接口上消除切换的需要,但这可能仍然是最不邪恶的。

    不过,在提供的特定示例中,我会尝试执行以下操作:

    if (customer.getCategory() < PRIME) {
        investable = customer.getSavingsAccount().getBalance() > 1e6;
    } else {
        investable = customer.isCeo();
    }
    

    我发现这比在一个开关中列出所有可能的类别更清晰,我怀疑它更可能反映“现实世界”的思维过程(“它们低于质数吗?”与“它们是次素数还是中素数?”)如果在某一点上添加了super_-prime名称,则可以避免重新访问此代码。

        2
  •  56
  •   community wiki Bill the Lizard    16 年前

    我认为陈述像

    使用switch语句是糟糕的oop样式。

    几乎可以用多态性替换case语句。

    太简单了。事实是,正在打开的case语句 类型 是糟糕的OOP风格。这些是您想要用多态性替换的。接通A 价值 很好。

        3
  •  15
  •   eljenso    16 年前

    当在纯OO代码中使用时,开关是一种代码味道。这并不意味着他们的定义是错误的,只是你需要三思而后行。要格外小心。

    这里对switch的定义还包括if-then-else语句,这些语句可以很容易地重写为switch语句。

    开关可能是一个信号,表明您没有定义接近其操作的数据的行为,并且没有利用子类型多态性。

    当使用OO语言时,您不会被强制以OO方式编程。因此,如果您选择使用更具功能性或基于对象的编程风格(例如,使用仅包含数据但不包含行为的DTO,而不是更丰富的域模型),那么使用交换机没有任何问题。

    最后,在编写OO程序时,当有东西从非OO外部世界进入OO模型,并且需要将这个外部实体转换为OO概念时,在OO模型的“边缘”处,开关非常方便。你最好尽早这样做。例如:可以使用开关将数据库中的int转换为对象。

    int dbValue = ...;
    
    switch (dbValue)
    {
      case 0: return new DogBehaviour();
      case 1: return new CatBehaviour();
      ...
      default: throw new IllegalArgumentException("cannot convert into behaviour:" + dbValue);  
    }
    

    编辑 在阅读了一些回答之后。

    Customer.isInvestable :太好了,多态性。但是现在您将这个逻辑与客户联系起来,您需要为每种类型的客户提供一个子类,以实现不同的行为。上一次我检查时,继承不应该这样使用。您希望客户类型是 Customer 或者具有可以决定客户类型的功能。

    双调度:多态性两次。但是你的访客类本质上仍然是一个大的转换,它有一些和上面解释的相同的问题。

    此外,根据OP的例子,多态性应该在客户的类别上,而不是在 顾客 本身。

    打开一个值是很好的:好的,但是switch语句在大多数情况下用于测试单个 int , char , enum ,…值,而不是if-then-else,在这里可以测试范围和更奇异的条件。但是,如果我们在这个值上调度,而它并不像上面所解释的那样处于OO模型的边缘,那么它似乎经常用于类型上的调度,而不是值上的调度。或者:如果你能 用一个开关替换if-then-else的条件逻辑,那么您可能是正常的,否则您可能不是。因此,我认为OOP中的开关是代码气味,并且声明

    打开类型是糟糕的OOP样式, 打开一个值是可以的。

    太简单了。

    回到起点: switch 不错,只是不总是很糟糕。你不必用OO来解决你的问题。如果你确实使用OOP,那么开关就是你需要特别注意的东西。

        4
  •  14
  •   Pete Kirkham    16 年前

    这是糟糕的OOP风格。

    并不是所有的问题都能通过OO得到最好的解决。有些你想要模式匹配,哪个开关是穷人的版本。

        5
  •  12
  •   Jim Arnold    16 年前

    如果有什么不同的话,我已经受够了人们描述这种编程方式的人了——在这种方式中,一堆getter被添加到“低挂起”类型(客户、帐户、银行)中,有用的代码被喷在“控制器”、“助手”和“实用程序”类中的系统周围——以对象为导向。像这样的代码 一个OO系统的气味,你应该问 为什么? 而不是被冒犯。

        6
  •  7
  •   snarf    16 年前

    当然,oo开关很差,不应该在函数的中间放一个返回,magic值很差,引用不应该为空,条件语句必须用大括号括起来,但这些都是指导原则。他们不应该被虔诚地跟随。可维护性、可重构性和可理解性都是非常重要的,但都是实际完成工作的第二步。有时我们没有时间成为一个编程理想主义者。

    如果任何一个程序员被认为是有能力的,应该假设他可以遵循指导方针并使用可用的工具。 慎重 应该承认,他不会总是做出最好的决定。他可能会选择一个不太理想的路由,或者犯错误,并遇到一个难以调试的问题,因为他选择了一个开关,而可能他不应该有或传递过多的空指针。这就是生活,他从错误中吸取教训,因为他能干。

    我不虔诚地遵循编程原则。作为一个程序员,我在自己的背景下考虑指导原则,并在合理的情况下应用它们。我们不应该喋喋不休地讨论这些编程实践,除非它们是解决当前问题的基础。如果你想对好的编程实践发表自己的观点,最好在博客或适当的论坛(如这里)上发表。

        7
  •  6
  •   toolkit    16 年前

    罗伯特·马丁的文章 Open Closed Principle 提供另一个视角:

    软件实体(类、模块、功能等) 应打开进行扩展,但关闭用于 修改。

    在代码示例中,您有效地打开了客户的“类别类型”

    boolean investible ;
    switch (customer.getCategory()) {
        case SUB_PRIME:
        case MID_PRIME:
            investible = customer.getSavingsAccount().getBalance() > 1e6; break;
        case PRIME:
            investible = customer.isCeo(); break;
    }
    

    在当前的气候下,新的客户类别可能正在涌现;-)。这意味着必须打开这个类,并不断地修改它。如果您只有一个switch语句,这可能是正常的,但是如果您想在其他地方使用类似的逻辑,会发生什么。

    而不是其他建议, isInvestible 是在 Customer 我想说,cagtegory应该成为一个成熟的阶级,并用于做出这些决定:

    boolean investible ;
    CustomerCategory category = customer.getCategory();
    investible = category.isInvestible(customer);
    
    class PrimeCustomerCategory extends CustomerCategory {
        public boolean isInvestible(Customer customer) {
            return customer.isCeo();
        }
    }
    
        8
  •  5
  •   Ryan Emerle    16 年前

    有些情况下,您需要根据几个选项做出决定,而多态性是过度杀伤力(yagni)。在这种情况下,开关正常。开关只是一种工具,可以像其他工具一样容易使用或滥用。

    这取决于你想做什么。然而,关键是,当使用开关时,你应该三思而后行。 可以 表明设计不好。

        9
  •  5
  •   Erik Engheim    16 年前

    我相信打开类型是一种代码味道。然而,我在代码中分享了您对关注点分离的关注。但是,这些问题可以通过许多方式解决,使您仍然可以使用多态性,例如 访问者模式 或者类似的东西。读起来 “设计模式” 四个帮派。

    如果你的核心对象 顾客 大部分时间保持不变,但操作经常更改,然后可以将操作定义为对象。

        interface Operation {
          void handlePrimeCustomer(PrimeCustomer customer);
          void  handleMidPrimeCustomer(MidPrimeCustomer customer);
          void  handleSubPrimeCustomer(SubPrimeCustomer customer);    
        };
    
        class InvestibleOperation : public Operation {
          void  handlePrimeCustomer(PrimeCustomer customer) {
            bool investible = customer.isCeo();
          }
    
          void  handleMidPrimeCustomer(MidPrimeCustomer customer) {
            handleSubPrimeCustomer(customer);
          }
    
          void  handleSubPrimeCustomer(SubPrimeCustomer customer) {
            bool investible = customer.getSavingsAccount().getBalance() > 1e6;    
          }
        };
    
        class SubPrimeCustomer : public Customer {
          void  doOperation(Operation op) {
            op.handleSubPrimeCustomer(this);
          }
        };
    
       class PrimeCustomer : public Customer {
          void  doOperation(Operation op) {
            op.handlePrimeCustomer(this);
          }
        };
    

    这看起来有点过分杀伤力,但是当您需要将操作作为集合来处理时,它可以很容易地为您节省大量的代码。例如,将它们全部显示在列表中,并让用户选择一个。如果操作被定义为函数,那么很容易得到许多硬编码的开关盒逻辑,每次添加另一个操作时需要更新的多个位置,或者 产品 正如我在这里提到的。

        10
  •  3
  •   EnocNRoll - AnandaGopal Pardue    16 年前

    我将switch语句视为if/else块的更可读的替代方法。

    我发现,如果您可以将逻辑归结为一个可以整体评估的结构,那么代码很可能提供OOP中所需的封装级别。

    在某种程度上,必须编写真实(杂乱)的逻辑,以便发布实际的程序。Java和C语言不是严格的OOP语言,因为它们是从C继承的。如果你想严格执行OOP代码,那么你需要使用一种不提供违反该思想的成语的语言。我的观点是Java和C语言都是灵活的。

    让VB6如此成功的原因之一是它是基于对象的,而不是面向对象的。所以,我想说,实用主义程序员将永远结合概念。只要已经编程了合适的封装,交换机还可以产生更易于管理的代码。

        11
  •  2
  •   jrockway    16 年前

    在您的库周围工作也是一种代码味道。你可能没有选择,但这并不意味着这是一个好的实践。

        12
  •  2
  •   JaredPar    16 年前

    我发现在OO代码中使用switch语句没有任何错误。我唯一的批评是,我会对名为isinvestible的客户提出一种新的方法,隐藏了这种逻辑。使用switch语句作为此方法的内部实现时出错0。正如您所说,不能向枚举添加方法,但可以向客户添加更多方法。

    如果您没有访问源代码的权限,我认为非instrunce方法是可以的。一个最纯粹的OOP需要一个全新的对象,但在这种情况下,这似乎是多余的。

        13
  •  2
  •   nes1983    16 年前

    知道你来自哪里。有些语言强迫你这样做。

    String str = getStr();
    switch(str) {
    case "POST" : this.doPost(); break;
    case "GET" : this.doGet(); break;
    //and the other http instructions
    }
    

    现在呢?当然,有一个很好的OOP方法:

    str.request(this);
    

    太糟糕了,字符串无法扩展,现在您正在考虑为每个httpinstruction编写一个包含8个子类的httpinstruction类。老实说,特别是在谈到解析器时,这是非常困难的。

    当然,这不是很好的OOP,但是好的代码并不总是…可能的。

    让我丢下一会儿。我在写论文。我个人不喜欢递归函数的常规设置。通常具有like funcrc(arg1,arg)和func(arg1):=func(funcrc(arg1,0));

    所以我在论文中用默认参数定义了它。不是每个人都知道默认参数的概念。我的论文使用了伪代码,但是教授让我把算法改为传统的方法,因为你不会经常遇到默认参数,所以不要使用它们。不要不必要地给读者惊喜。我认为他是对的。

    但结果是,现在我陷入了一个函数的困境,这个函数的唯一目的是围绕默认参数传递——这可能更漂亮。

    因此,底线是:真正漂亮的程序需要优秀的库、优秀的代码浏览器和工具、具有FogBugz质量的bugTracker,至少,更好的集成、Git质量的版本管理等等。还有,嗯,你周围的人可以使用所有这些东西,知道如何处理所有这些东西。最重要的是:一种beaauuuutiful语言,允许优雅地解决棘手问题。

    所以,很有可能,你被Java所困扰,这使得在所有情况下很难想出一个很好的交换机。赛尔夫会有一个优雅的解决方案。但你不是在利用自我,如果是这样,你的同事就无法阅读它,所以忘了这一点。

    现在找到一个折衷办法。

    我知道这很难过。

        14
  •  2
  •   Jeffrey Cameron    16 年前

    我认为switch语句是否是糟糕的OOP实践取决于您使用switch语句的位置。

    例如,在工厂方法中,它可能是编写复杂且可能有缺陷的基于反射的代码的非常简单的替代方法。

    不过,在大多数情况下,我认为开关只是简单的设计。通常,使用相同的方法在不同对象中隐藏操作复杂性会导致代码更易于理解,甚至可能更快。例如,如果您有一个执行 许多 这样,预先打包的对象实际上可以节省一些CPU周期。

        15
  •  2
  •   Loren Pechtel    16 年前

    来自外部源的数据本身不能真正面向对象,因为您没有引入代码。如果里面有箱子,你就会有箱子。时期。

    除此之外,OOP不是银弹。有时是答案,有时不是。

        16
  •  1
  •   hallidave    16 年前

    几乎可以用多态性替换case语句。

    public class NormalCustomer extends Customer {
        public boolean isInvestible() {
            return getSavingsAccount().getBalance() > 1e6;
        }
    }
    
    public class PreferredCustomer extends Customer {
        public boolean isInvestible() {
            return isCeo();
        }
    }
    

    这种方法将简化客户机代码。客户代码不必知道“可投资性”是如何计算的细节,也不再需要破坏 Law of Demeter 通过挖掘客户对象的状态。

        17
  •  1
  •   TMOSS    16 年前

    现在呢?当然,有个不错的 OOP方式:

    str.请求(this);

    太糟糕了,字符串无法扩展 现在你正在考虑写一篇 带8的http指令类 每个http指令的子类。 说实话,尤其是在谈话时 关于解析器,它只是 非常困难。

    有没有尝试过C扩展方法?字符串可以扩展。

        18
  •  1
  •   patros    16 年前

    是的,我受够了有人告诉你这是不好的款式。

    编辑:在问题解决之前,这更有意义。

        19
  •  1
  •   Steve Mitcham    16 年前

    我对switch语句的问题是,在现实应用程序中,很少有单独存在的switch语句。

    在我公司的代码库中,许多需要重构的代码将使整个类中充满了多个switch语句,因此您必须了解每个switch语句的存在。

    最后,最干净的方法是将整个系统重构成一个策略模式,由工厂根据switch语句的单个剩余副本控制策略的创建。

    由于时间限制,我们没有走得更远,因为这满足了我们的需要。仍然有一个巨大的switch语句,但是只有一个,所以添加额外的策略只需要IPMLement接口和将创建步骤添加到master switch语句。

        20
  •  1
  •   Daniel Daranas    16 年前

    首先,您的目标不应该是实现“良好的OO风格”,而是实现良好的代码。“好”的意思是至少正确、清晰、易读且尽可能简单。

    因此,我将把队列重新表述为: “使用开关是错误代码的标志吗?” 因为这才是我真正关心的。现在我继续回答。

    嗯,这是个好问题。) 通常 ,使用一次开关并不是错误代码的标志。但是,如果你在课堂上的几个点上打开同一个东西,那么最好考虑一种替代设计,在这种设计中,你用子类来表示交换替代品——当你考虑到这一点时,特别要问你自己,像这样创建的类是否会是当前类的一种专门化,并且会有一种IS-A关系。如果是这样,这就为使用继承提供了更多的点。

    最后一条评论:“使用[语言功能X] 完全 “是坏的”是危险的接近“语言设计者愚蠢地将[语言功能X]包括在其中”。

        21
  •  0
  •   oxbow_lakes    16 年前

    几乎总是可以用多态性替换case语句。

    而且

    boolean investable = customer.isInvestable();
    

    由于对IsInvestable的调用是多态的,因此用于进行调用的实际算法由客户类型决定。

    我想你们都是 错误的 . 如果这仅仅是希望获得商业贷款的客户的“可投资性”逻辑呢?也许一个客户对于另一个产品的不可归属性决定是完全不同的,可能不是基于“类别”,而是他们生活在哪里,他们是否结婚,他们在哪个工作部门工作?

    另外,如果有新产品一直在推出,每个产品都有不同的投资决策,我不想更新我的核心 Customer 每次都上课?

    就像我说的,我不是说 switch 总是很好的-但同样地,这是完全合理的。如果使用得当,它可能是编写应用程序逻辑的一种非常清晰的方法。

        22
  •  0
  •   TofuBeer    16 年前

    “此外,如果一直有新产品推出,每个产品都有不同的投资决策,我不想每次都更新我的核心客户类别,那该怎么办?”

    我突然想到:

    interface Investable
    {
        boolean isIvestible(Customer c);
    }
    
    class FooInvestible 
        implements Investible
    {
        public boolean isInvestible(final Customer c)
        {
            // whatever logic, be it switch or other things
        }
    }
    

    最初使用swtich并添加新类型的决策的“问题”在于,您可能最终会遇到一些无法以理智的方式维护的庞大的代码窝。将决策分为多个类强制将决策分为多个类。然后,即使使用switch,代码也可能保持健全和可维护性。