如何摆脱工具类
英文原文: How to get rid of helper and utils classes
无论是进行代码 review 还是紧急编码调整,你总会发现:你又搞出了一个帮助类(helper class)。代码运行一切正常,进度又必须跟上,发布任务一个接一个,因此那个帮助类逐渐变成了一个提供了很多静态(static)方法的“怪兽类”(monster class),在它的 utils 包内不受控制地增长。utils 包长久以来就是一个技术争议的荒蛮之地,面向对象设计理念连半步都不敢踏入。“工具类是功能集中,并且逻辑毫不重复(Do not repeat yourself)” 一些开发人员会这样喊道 ,通常就是他们编写了这些工具类。因为所有都是静态的,所以它很快 - 团队里面的另外一些人这样说,也许就是是添加另外一些静态方法的人。它很容易使用,我们使这些代码很简洁 — 你可以在这个空间内听到这样的言论,但这又是 另外一个对 KISS 的误解了。
我们会争论到:通常帮助类和工具类都很简单,特别是当我们不能修改新功能的目标类(例如外部依赖库)或我们不能找到使用的目标(不清晰的领域模型,PoC,需求缺失),或者我们只是不想去找它(懒,这也是帮助类的最主要原因)。但是最大的问题在于这很明显不是面向对象的解决方案,并且随着时间的推移(缺少团队沟通,资源重用,快速修复和一些其他的东西)它会导致一些包含无尽静态方法的容器和令人头疼的维护(你想要做到 DRY,但你却是用 10 个方法来提供几乎相同的功能,尽管不是完全一样;你想要快速,但你现在不能方便地添加一个 cache 机制到那个静态类中或者你遇到了并发的麻烦;你想使事情变得简单,但现在你的 IDE 提供了一长列的各种各样的方法,这并不能简化你的工作)。但不要担心,我们会尝试着去解决它。
让我们来重构帮助类
首先,我们需要定义我们的问题:一个只提供静态方法的无状态类(有 Helper 或 Utils 后缀),它没有明确的职责,在项目中也不会被初始化为对象。
接着,我们需要一个几乎明确的方案来解决问题。这几乎就代表了例外和项目特性:最后的决定当然是根据具体的情况来了,任何被称为通用解决方案的基本上都可以忽略。我们最后需要分析一下给出的类,尝试着:
- 找到一个确定静态方法从属的目标类
- 或找到这个类实际提供的目标业务实体,然后把它迁移到相关的组件,重命名并且删除静态方法(替换它们)
- 或者通过面向对象方式添加一个提供一个或多个行为(之前存在的静态方法)新类。
上面的任何方案都可以提供一个更好的模型。然后我们再依据下面的步骤(假设根据下面的步骤进行项目重构):
- 为了使我们的任务简单些,我们删掉项目的帮助类中没用的方法(你的 IDE 将会帮你大忙)。
- 接下来我们把 class 定义为 final。你看到项目中有编译错误了吗?如果有,为什么帮助类或工具类需要被继承呢?你也许已经有一个目标:子类。如果子类是另外一个帮助类(真的吗?),把它和父类合并吧。
- 如果不存在,我们为该类添加一个私有构造函数。你看到项目中出现了编译错误了吗?那么肯定在哪个地方初始化了这个类,所以这并不是单纯的帮助类或者它没有被正确使用。看一下那些调用方,你会发现一个或一系列方法都可能属于这个目标类(或者实体)。
- 让我们通过一定规则类似的签名来分组类方法,将它们拆分到更小的帮助类中(从繁杂到有共性的方法,那个共性也许就是我们需要的目标实体了)。通常到了这一步,我们会从一个大的工具类向更轻量的帮助类过渡(提示:这时候不要害怕创建一个只有一个方法的类),同时我们的范围缩小了(从
ProjectUtils
到CarHelper
,EngineHelper
,WheelHelper
等等)。(好,你的代码难道看起来不是更简洁了吗?) -
如果这些新类只有一个方法,我们需要看一下它的用途。如果我们只有一个调用者,那么恭喜你,那就是我们的目标类了!你可以把方法移到类中,作为 behavior 或私有方法(保持它的 static 标识或者利用内部状态)。这个帮助类就消失了。
-
我们目前得到的帮助类(但是它确实可以成为你的起点)确定了这些关联方法的一个通用状态。提示:看一下那些方法中的大部分通用参数(例如,所有方法都接收一个
Car
对象),这表明,这些方法可能应该作为方法属于Car
类(或者扩展?封装类?)。否则,这些通用的参数应该是一个可以传给构造函数并且被所有(非静态和其他的)方法使用的类的属性,状态。那个属性应该会使你想起类的前缀,方法的归类可以使你想起一系列行为的类(CarValidator
,CarReader
,CarConverter
等等)。那么这个帮助类又可以去掉了。 - 如果这堆方法根据可选的输入和一些相同输入参数来使用不同的参数,那么考虑通过使用建造者模式(Builder pattern)定义可变的接口来转换这个帮助类:从一系列类似
Helper.calculate (x)
,calculate (x, y)
,calculate (x, z)
,calculate (y, z)
的静态方法我们可以简单地想到如newBuilder () .with (x) .with (y) .calculate ()
。帮助类会提供 behaviours,减少业务方法列表,并且提供更好的扩展性。调用方可以把它当作内部属性来重用或者在需要的时候再初始化。这个帮助类(我们所知的)又可以去掉了。 -
如果帮助类提供的方法确实是供不同的参数使用的(但,在这个时候,都是用于同一对象的),可以考虑使用命令模式(Command pattern):调用方实际上创建必须的命令(处理必须的输入和提供必要的操作),在确定的上下文情况下会有一个调用者进行执行。你也许可以获取到每个静态方法的命令实现,你的代码也从
Helper.calculate (x,y)
,calculate (z)
变成了invoker.calculate (new Action (x, y))
。帮助类再见。 -
如果帮助类提供的方法接收相同的参数,但处理不同的逻辑,可以考虑使用策略模式(
Strategy pattern
):每一个静态方法都可以简单地变成一个策略实现,从而消除原来的帮助类(取而代之的是上下文组件)。 - 如果需要处理的多个静态方法涉及到一个类层次或一系列的组件,可以考虑使用访问者模式(
Visitor pattern
):你可以根据不同的访问方法得到几个访问者实现,这也许可以替换部分或所有之前存在的静态方法。 - 如果之前的情况都不符合你的情况,那可以使用三个最重要的指标:你的经验,你的项目能力和直觉。
总结
过程很简单,找到对的实体和合理的目标类或者通过一种采用面向对象设计的标准方法来重构给定的帮助类(但会在代码复杂度上有所增加,值得吗?)。过一下上面提到的场景列表,也许当你尝试理解怎么去实现重构时会有多于一个将会为你提供灵感;特定的限制也许会限制已确定的解决方案;复杂的静态方法和相关的流程也许需要几个重构的步骤,可以一直优化它直到得到可接受的结果。或者你可以选择在某种程度上以代码可读性和 简单性的名义来维持原来的帮助类(希望能满足上面至少 5 个步骤)。帮助类并不都是有害的,但绝大多数情况下你并不需要它们。
参考: 如休整脱离帮助类和工具类参考自我们的 JCG 成员 Antonio Di Matteo 重构的建议。
翻译: ImportNew.com - 陈晓舜
译文链接: http://www.importnew.com/11593.html