我知道“班级有一个改变的理由”。现在,那到底是什么?是否有一些气味/迹象可以表明该班级没有单一责任?或者真正的答案是否可以隐藏在 YAGNI 中,并且只在你的类第一次更改时重构为单一职责?
13 回答
写一个简短但准确的描述类做什么。
如果描述包含单词“and”,则需要对其进行拆分。
单一职责原则
有很多明显的例子,例如CoffeeAndSoupFactory
。同一设备中的咖啡和汤可能会导致令人不快的结果。在此示例中,设备可能会分解为 aHotWaterGenerator
和某种Stirrer
. 然后可以从这些组件构建一个新的CoffeeFactory
并且SoupFactory
可以避免任何意外混合。
在更微妙的情况下,数据访问对象 (DAO) 和数据传输对象 (DTO) 之间的紧张关系非常普遍。DAO 与数据库对话,DTO 可序列化以在进程和机器之间传输。通常 DAO 需要对您的数据库框架的引用,因此它们在您的富客户端上不可用,这些客户端既没有安装数据库驱动程序,也没有访问数据库的必要权限。
代码气味
类中的方法开始按功能区域分组(“这些是
Coffee
方法,这些是Soup
方法”)。实现许多接口。
嗯,这个原理是要和一些盐一起使用的……以避免类爆炸。
单一职责不会转化为单一方法类。这意味着存在的唯一理由……对象为其客户提供的服务。
留在路上的好方法...使用对象作为人的隐喻...如果对象是人,我会要求谁这样做?将该职责分配给相应的类。但是,您不会要求同一个人来管理文件、计算工资、发放薪水和验证财务记录……为什么要一个对象来完成所有这些工作?(如果一个类承担多个职责,只要它们都是相关且连贯的,就可以了。)
- 如果您使用 CRC 卡,这是一个很好的微妙指南。如果您无法在 CRC 卡上获取该对象的所有职责,那么它可能做的太多了……最多 7 个可以作为一个很好的标记。
- 重构书中的另一个代码味道是巨大的类。霰弹枪手术将是另一种......对一个班级中的一个区域进行更改会导致同一班级的不相关区域出现错误......
- 发现您一次又一次地为不相关的错误修复对同一个类进行更改是另一个表明该类做得太多的迹象。
检查单一职责(不仅是类,还包括类的方法)的一种简单实用的方法是名称选择。当你设计一个类时,如果你很容易找到一个准确地指定它定义的类的名称,那么你就走对了。选择一个名字的困难几乎总是糟糕设计的征兆。
你的类中的方法应该是有凝聚力的......它们应该一起工作并在内部使用相同的数据结构。如果你发现你有太多看起来并不完全相关的方法,或者似乎对不同的事物进行操作,那么很可能你没有一个好的单一职责。
通常最初很难找到责任,有时您需要在几个不同的上下文中使用该类,然后在您开始看到区别时将该类重构为两个类。有时您会发现这是因为您将抽象和具体的概念混合在一起。它们往往更难看到,同样,在不同的环境中使用将有助于澄清。
明显的迹象是当你的类最终看起来像一个大泥球时,这实际上与 SRP(单一责任原则)相反。
基本上,所有对象的服务都应该专注于执行单一职责,这意味着每次您的类更改并添加不尊重这一点的服务时,您知道您正在“偏离”“正确”路径;)
原因通常是由于匆忙添加到类中以修复某些缺陷的一些快速修复。因此,您更改课程的原因通常是检测您是否即将打破 SRP 的最佳标准。
Martin 的C# 中的敏捷原则、模式和实践对我掌握 SRP 帮助很大。他将 SRP 定义为:
一个类应该只有一个改变的理由。
那么是什么推动了变革?
马丁的回答是:
[...] 每项责任都是变革的轴心。(第116页)
并进一步:
在 SRP 的上下文中,我们将责任定义为变更的原因。如果你能想到改变一个班级的不止一个动机,那么这个班级就有不止一个责任(第117页)
事实上,SRP 正在封装变化。如果发生变化,它应该只会产生局部影响。
雅格尼在哪里?
YAGNI 可以与 SRP 很好地结合:当你应用 YAGNI 时,你会等到一些变化实际发生。如果发生这种情况,您应该能够清楚地看到从更改原因推断出的责任。
这也意味着责任可以随着每个新的要求和变化而发展。进一步思考 SRP 和 YAGNI 将为您提供思考灵活设计和架构的方法。
也许比其他气味更具技术性:
- 如果你发现你需要几个“朋友”类或函数,那通常是不好的 SRP 的好味道——因为所需的功能实际上并没有被你的类公开。
- 如果你最终得到一个过于“深”的层次结构(一长串派生类,直到你到达叶类)或“广泛”的层次结构(很多很多类从单个父类中派生出来)。这通常表明父类做得太多或太少。什么都不做是限制,是的,我在实践中已经看到,使用“空”父类定义只是将一堆不相关的类组合在一个层次结构中。
我还发现重构为单一职责很困难。当你最终解决它时,类的不同职责将在客户端代码中交织在一起,这使得很难在不破坏另一件事的情况下将一件事分解出来。我宁愿自己犯“太少”而不是“太多”的错误。
如果您最终使用MethodA
那个用途MemberA
并且MethodB
那个用途MemberB
并且它不是某些并发或版本控制方案的一部分,那么您可能违反了 SRP。
如果您注意到您有一个类只是将调用委托给许多其他类,您可能会陷入代理类地狱。如果您最终在任何地方都实例化代理类,而您可以直接使用特定类,则尤其如此。我见过很多这样的。思考ProgramNameBL
和ProgramNameDAL
类作为使用存储库模式的替代品。
这里有一些事情可以帮助我弄清楚我的班级是否违反了 SRP:
- 填写关于类的 XML 文档注释。如果你使用 if、and、but、except、when 等词,你的类可能做得太多了。
- 如果您的类是域服务,则名称中应包含动词。很多时候你有像“OrderService”这样的类,它可能应该被分解成“GetOrderService”、“SaveOrderService”、“SubmitOrderService”等。
我也一直在尝试了解OOD 的 SOLID 原则,特别是单一责任原则,即 SRP(作为旁注,与 Jeff Atwood、Joel Spolsky 和“Uncle Bob”的播客值得一听)。对我来说最大的问题是:SOLID 试图解决什么问题?
OOP 是关于建模的。建模的主要目的是以使我们能够理解和解决问题的方式呈现问题。建模迫使我们专注于重要的细节。同时我们可以使用封装来隐藏“不重要”的细节,这样我们只需要在绝对必要的时候处理它们。
我想你应该问自己:你的班级试图解决什么问题?解决这个问题所需的重要信息是否浮出水面?是否隐藏了不重要的细节,以便您仅在绝对必要时才考虑它们?
考虑这些事情会导致程序更容易理解、维护和扩展。我认为这是 OOD 和 SOLID 原则的核心,包括 SRP。
我想提出的另一条经验法则如下:
如果您觉得需要在测试用例中编写某种笛卡尔积,或者如果您想模拟类的某些私有方法,则违反了单一职责。
我最近通过以下方式获得了这个:我有一个协程的 cetain 抽象语法树,稍后将生成到 C 中。现在,将节点视为 Sequence、Iteration 和 Action。序列链两个协程,迭代重复一个协程,直到用户定义的条件为真,Action 执行某个用户定义的动作。此外,可以使用代码块来注释动作和迭代,这些代码块定义了在协程前进时要评估的动作和条件。
有必要对所有这些代码块进行某种转换(对于那些感兴趣的人:我需要用实际的实现变量替换概念性用户变量,以防止变量冲突。了解 lisp 宏的人可以想到 gensym 的实际应用: ))。因此,最简单的工作是一个访问者,它在内部知道操作,只需在访问时操作和迭代的注释代码块上调用它们并遍历所有语法树节点。但是,在这种情况下,我不得不在我的测试代码中为 visitAction-Method 和 visitIteration-Method 复制断言“应用了转换”。换句话说,我必须检查产品测试用例的职责 Traversion (== {traverse iteration, traverse action, traverse sequence}) x Transformation (well, 代码块转换,它爆炸成迭代转换和动作转换)。因此,我很想使用 powermock 删除转换方法并将其替换为一些 'return "I was changed!";'-Stub。
但是,根据经验,我将类拆分为一个类 TreeModifier,其中包含一个 NodeModifier-instance,它提供了 modifyIteration、modifySequence、modifyCodeblock 等方法。因此,我可以很容易地测试遍历的职责,调用 NodeModifier 和重构树并分别测试代码块的实际修改,从而消除了对产品测试的需要,因为现在职责分离了(遍历和重构和具体修改)。
有趣的是,稍后我可以在各种其他转换中大量重用 TreeModifier。:)
如果您在扩展类的功能时遇到麻烦,而不必担心最终可能会破坏其他东西,或者如果不修改其大量选项就无法使用类,这些选项会修改其行为,这听起来就像您的类做的太多了。
一旦我使用具有方法“ZipAndClean”的遗留类,这显然是压缩和清理指定的文件夹......