如何重构具有多个切换案例的应用程序?


10

我有一个应用程序,它使用整数作为输入,并基于输入调用不同类的静态方法。每次添加新数字时,我们需要添加另一种情况,并调用不同类的不同静态方法。交换机中现在有50个案例,每当我需要添加另一个案例时,我都会发抖。有一个更好的方法吗。

我做了一些思考,想到了这个主意。我使用策略模式。我没有交换条件,而是有一个策略对象映射,键为输入整数。调用方法后,它将查找对象并为该对象调用通用方法。这样,我可以避免使用switch case结构。

你怎么看?


2
当前代码的实际问题是什么?
菲利普·肯德尔

当您必须进行以下更改之一时会发生什么?您是否必须switch在复杂系统中添加一个案例并调用一个预先存在的方法,还是必须同时发明该方法及其调用?
Kilian Foth,

@KilianFoth我已经作为维护开发人员继承了这个项目,并且还没有进行任何更改。但是,我很快会进行更改,因此我想立即进行重构。但是要回答您的问题,请回答是。
Kaushik Chakraborty

2
我认为您需要显示一个正在发生的事的简明示例。
whatsisname

1
@KaushikChakraborty:然后从记忆中组成一个例子。在某些情况下,使用250多个案例的超级开关是合适的,并且在某些情况下,无论案例多么少,切换都是不好的。细节在于魔鬼,我们没有细节。
whatsisname 2017年

Answers:


13

交换机中现在有50个案例,每当我需要添加另一个案例时,我都会发抖。

我喜欢多态。我喜欢SOLID。我喜欢纯粹的面向对象编程。我讨厌看到这些代表不好的表现,因为它们被教条地应用了。

您没有很好的理由来重构策略。重构有一个名字。这称为用多态替换条件

我从c2.com找到了一些有关您的建议:

实际上,只有经常重复相同或非常相似的条件测试才有意义。对于简单的,很少重复的测试,用多个类定义的冗长程度替换简单的条件语句,并可能将所有这些条件从实际上需要条件操作的代码移开,这将导致教科书示例中发生代码混淆。相对于教条式的纯洁性,它更喜欢清晰度。-丹·穆勒

您有一个带有50个盒子的开关,您的替代选择是生产50个对象。哦,还有50行的对象构造代码。这不是进步。为什么不?因为此重构没有任何作用将数字从50减少。当您需要在其他地方的同一输入上创建另一个switch语句时,可以使用此重构。那时,这种重构非常有用,因为它可以将100重新变成50。

只要您像唯一的开关一样指代“开关”,我就不建议这样做。现在从重构中获得的唯一好处是,它减少了一些愚蠢的人复制和粘贴50格开关的机会。

我建议对这50种情况进行仔细研究,以找出可以排除的共同点。我的意思是50?真?您确定需要很多情况吗?您可能在这里尝试做很多事情。


我同意你的意思。该代码具有很多冗余,可能甚至很多情况下都没有必要,但是从粗略的眼光来看似乎并非如此。每个案例都调用一个方法,该方法调用多个系统并汇总结果并返回到调用代码。每个班级都是自给自足的,只做一份工作,恐怕我要减少案件数量,否则会违反高凝聚力原则。
Kaushik Chakraborty

2
我可以在不违反高凝聚力的情况下获得50分,并使一切保持独立。我只是用一个号码做不到。我需要2、5和5。这就是为什么将其称为分解。认真地,检查一下整个体系结构,看看是否无法从当前的50种情况中找出出一条出路。重构就是消除错误的决策。不要以新形式延续它们。
candied_orange

现在,如果您看到一种使用此重构来减少50的方法,那就去吧。要充分利用Doc Browns的想法:一张地图可以使用两个键。需要考虑的事情。
candied_orange

1
我同意蜜饯的评论。问题不在于switch语句中的50种情况,而是高级架构设计导致您调用需要在50个选项之间进行选择的函数。我已经设计了一些非常大型和复杂的系统,但从未遇到过这种情况。
Dunk

@Candied“当您发现需要在其他地方的同一输入上创建另一个switch语句时,可以使用此重构。”您能详细说明一下吗?因为我有一个类似的情况,其中我有切换用例,但是在不同的层上,就像我们在我们的层中一样项目首先进行授权,验证,CRUD程序,然后确定。因此,在每一层中,相同的输入(即整数)上都有切换用例,但它们执行不同的功能,例如auth,valid。那么我们应该为每个具有不同方法的类型创建一个类吗?通过“在相同的输入上重复相同的开关”,我们的案例是否符合您要说的内容?
Siddharth Trikha '17

9

单独的策略对象图,在代码的某些功能中初始化,其中有几行代码看起来像

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

要求您和您的同事以更统一的方式实现在单独的类中调用的功能/策略(因为您的策略对象都必须实现相同的接口)。这样的代码通常比

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

但是,无论何时需要添加新的数字,它仍然不会使您摆脱编辑此代码文件的负担。这种方法的真正好处是不同的:

  • 现在,地图的初始化与实际调用与特定编号相关联的函数的调度代码分离了,而后者不再包含这50个重复,它看起来像myMap[number].DoIt(someParameters)。因此,无论何时有新号码到达,都无需触摸此调度代码,并且可以根据“打开-关闭”原理来实现。而且,当您获得需要扩展调度代码本身的要求时,您将不必再更改50个位置,而只需更改一个。

  • 映射的内容是在运行时确定的(而switch构造的内容是在编译时确定的),因此这使您有机会使初始化逻辑更加灵活或可扩展。

是的,有一些好处,这无疑是迈向更多SOLID代码的一步。但是,如果重构值得回报,那么您或您的团队将必须自行决定。如果您不希望更改派发代码,不更改初始化逻辑并且的可读性switch不是真正的问题,那么现在的重构可能就不再那么重要了。


尽管我不愿意用多态性盲目地替换每个开关,但我会说,使用Doc Doc Doc所建议的地图过去对我来说非常有效。当您实现相同的接口时,请用具有许多不同实现的一种方法替换Doit1Doit2Doit
candied_orange

并且,如果您可以控制用作键的输入符号的类型,则可以通过制定doTheThing()输入符号的方法来进一步进行操作。然后,您无需维护地图。
凯文•克鲁姆维德

1
@KevinKrumwiede:您的建议是简单地在程序中传递策略对象本身,以代替整数。但是,当程序从某个外部数据源获取整数作为输入时,至少在系统的一个位置必须有一个从整数到相关策略的映射。
布朗

扩展Doc Brown的建议:如果您决定采用这种方式,您还可以创建一个工厂,其中包含创建策略对象的逻辑。就是说,CandiedOrange提供的答案对我来说最有意义。
弗拉基米尔·斯托基奇(Fladimir Stokic)'17

@DocBrown这就是“如果您可以控制输入符号的类型”的意思。
凯文·克鲁姆维德

0

我强烈支持@DocBrown的答案中概述的策略。

我将建议对答案进行改进。

来电

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

可以分发。您不必回到同一文件即可添加另一个策略,该策略甚至更好地遵循了“开放-封闭”原则。

假设您Strategy1在文件Strategy1.cpp中实现。您可以在其中包含以下代码块。

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

您可以在每个StategyN.cpp文件中重复相同的代码。如您所见,这将是很多重复的代码。为了减少代码重复,您可以使用模板,该模板可以放在所有Strategy类都可以访问的文件中。

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

之后,您在Strategy1.cpp中唯一需要使用的是:

static StrategyHelper::Initializer<1, Strategy1> initializer;

StrategyN.cpp中的相应行是:

static StrategyHelper::Initializer<N, StrategyN> initializer;

通过为具体的Strategy类使用类模板,可以将模板的使用提高到另一个层次。

class Strategy { ... };

template <int N> class ConcreteStrategy;

然后,而不是Strategy1使用ConcreteStrategy<1>

template <> class ConcreteStrategy<1> : public Strategy { ... };

将helper类更改为将Strategys 注册到:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

将Strateg1.cpp中的代码更改为:

static StrategyHelper::Initializer<1> initializer;

将StrategN.cpp中的代码更改为:

static StrategyHelper::Initializer<N> initializer;
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.