你如何定义一个单一的责任?
我知道“有单一理由的class级”。 那么究竟是什么呢? 有没有一些气味/迹象可以说明class级没有一个单一的责任? 或者真正的答案可以隐藏在YAGNI中,只有在class级第一次改变时才重构为单一责任?
单一责任原则
有很多明显的例子,例如CoffeeAndSoupFactory
。 咖啡和汤在同一个家电可能会导致相当不愉快的结果。 在这个例子中,设备可能会被分解成一个HotWaterGenerator
和一些Stirrer
。 然后,可以从这些组件构build新的CoffeeFactory
和SoupFactory
并且可以避免任何意外混合。
在更微妙的情况中,数据访问对象(DAO)和数据传输对象(DTO)之间的张力非常普遍。 DAO与数据库交谈,DTO可序列化以在进程和机器之间传输。 通常,DAO需要引用数据库框架,因此它们在您的胖客户端上不可用,它们既没有安装数据库驱动程序,也没有访问数据库的必要特权。
代码嗅觉
-
类中的方法开始按function区分组(“这些是
Coffee
方法,这是Soup
方法)”。 -
实现许多接口。
写一个简短的,但准确的描述什么课程。
如果描述中包含“and”这个词,那么它就需要被分割。
一个简单而实用的方法来检查单个责任(不仅是class级,而且是class级方法)是名称的select。 当你devise一个类的时候,如果你很容易地find一个类的名字,指定它所定义的名字,那么你的方式是正确的。 select一个名字的困难几乎总是一个糟糕的devise的症状。
那么,这个原则就是要用一些盐来避免课堂爆炸。
单个职责不转化为单一的方法类。 这意味着存在的一个单一的原因…对象提供给客户的服务。
一个很好的方式留在路上…使用对象作为人隐喻…如果对象是一个人,我会问谁这样做? 将责任分配给相应的class级。 但是,你不会要求同一个人做你的pipe理文件,计算工资,发放工资,并validation财务logging……你为什么要一个单一的对象来做所有这些? ( 只要一个class级相互关联,一个class级承担多重责任即可 )。
- 如果你使用CRC卡,这是一个不错的指导方针。 如果你在CRC卡上无法承担这个对象的所有责任,那么可能是太多了…最多7个可以作为一个好的标记。
- 重构书中的另一个代码气味是巨大的类。 霰弹枪手术将是另一个…在一个class级的一个领域的变化导致在同一class级的无关领域的错误…
- 发现你一次又一次地对无关的错误修复修改同一个类是另一个表明这个类正在做太多的事情。
在你的类中的方法应该是内聚的…他们应该一起工作,并在内部使用相同的数据结构。 如果你发现有太多的方法看起来并不完全相关,或者看起来不一样,那么很可能你就没有一个好的单一责任。
通常很难最初find责任,有时你需要在几个不同的上下文中使用这个类,然后在你开始看到区别时把这个类重构成两个类。 有时候你会发现这是因为你正在把抽象的和具体的概念混合在一起。 他们往往很难看,而且,在不同的情况下使用,将有助于澄清。
显而易见的迹象是,当你的class级变得像一个大泥球 ,这是真正的SRP(单一责任原则)相反。
基本上,所有对象的服务都应该集中在执行单一的责任上,这意味着每当你的class级发生变化并增加一项不尊重的服务时,你就知道自己正在“偏离”“正确”的道路;)
原因通常是由于为了修复一些缺陷而急速增加了一些快速修复。 所以你改变class级的原因通常是检测你是否要打破SRP的最好的标准。
也许比其他的气味更具技术性:
- 如果你发现你需要几个“朋友”类或function,这通常是坏SRP的好气味 – 因为所需的function实际上并没有公开地暴露在你的课堂上。
- 如果最后得到的是过深的层次结构(直到获得叶类)或“广义”层次结构(许多从单个父类中浅层获得的类)的最后一个派生类。 这通常是父类太多或太less的标志。 什么都不做是有限的,是的,我已经看到在实践中,用一个“空的”父类的定义就是把一堆不相关的类组合在一起。
我也发现重构单一的责任是很难的。 当你最终解决这个问题的时候,这个class级的不同职责将会与客户代码纠缠在一起,很难把一件事情排除在外而不会破坏其他事情。 我宁愿自己的“太less”而不是“太多”。
以下是一些帮助我弄清楚我的class级是否违反SRP的事情:
- 填写一个类的XML文档注释。 如果你使用if和if这样的单词,除了when,etc.等,你的课程可能会做得太多。
- 如果你的class级是一个域名服务,那么它的名字中应该有一个动词。 很多时候你有像OrderService这样的类,它们可能被分解成“GetOrderService”,“SaveOrderService”,“SubmitOrderService”等等。
如果最终MethodA
使用MemberA
和使用MemberB
,而不是某种并发或版本控制scheme的一部分,则可能违反了SRP。
如果你注意到你有一个只是把调用委托给其他很多类的类,你可能会被困在代理类的地狱里。 如果您最终只是直接使用特定的类,则最终实例化代理类时尤其如此。 我见过很多这个。 认为ProgramNameBL
和ProgramNameDAL
类可以替代使用Repository模式。
Martin 在C#中的敏捷原则,模式和实践帮助我掌握了SRP。 他将SRP定义为:
一个class级应该只有一个改变的理由。
那么驱动变革是什么?
马丁的回答是:
每个责任都是一个变化的轴心。 (第116页)
并进一步:
在SRP的背景下,我们将责任定义为变化的原因。 如果你可以想到改变class级的动机不止一个,那么这个class级就有多个责任(第117页)
其实SRP封装了变化。 如果发生变化,就应该有一个本地的影响。
YAGNI在哪里?
YAGNI可以和SRP很好地结合在一起:当你使用YAGNI时,你需要等到实际发生的变化。 如果发生这种情况,您应该能够清楚地看到根据变更原因推断的责任。
这也意味着责任可以随着每个新的要求和变化而变化。 进一步思考SRP和YAGNI将为您提供思考灵活devise和架构的方法。
我也一直试图让自己的头脑围绕在OOD的固体原则 ,特别是单一责任原则,又名SRP(作为一个旁注,与杰夫·阿特伍德,乔尔·斯波斯基和“叔叔鲍勃”的播客值得一听。 对我来说最大的问题是:SOLID试图解决什么问题?
面向对象是关于build模的。 build模的主要目的是以某种方式呈现问题,使我们能够理解并解决问题。 build模迫使我们专注于重要的细节。 同时我们可以使用封装来隐藏“不重要”的细节,这样我们在绝对必要的时候只需要处理它们。
我想你应该问自己:你们class正在努力解决什么问题? 你需要解决这个问题的重要信息浮出水面吗? 这些不重要的细节是隐藏起来的,所以在绝对必要的时候你只需要考虑一下它们呢?
思考这些事情会导致更容易理解,维护和扩展的程序。 我认为这是OOD和SOLID原则的核心,包括SRP。
我想提出的另一个经验法则是:
如果您觉得需要在testing用例中编写某种笛卡尔积的产品,或者如果您想要模拟该类的某些私有方法,则会违反“单一责任”。
我最近有这样一个方法:我有一个协程的cetain抽象语法树,稍后将生成C语言。 现在,将节点看作序列,迭代和动作。 序列链两个协程,迭代重复协程直到用户定义的条件为真,并且动作执行特定的用户定义的动作。 此外,可以使用代码块来注释动作和迭代,这些代码块定义了在协程前进时评估的动作和条件。
有必要对所有这些代码块进行一定的转换(对于那些感兴趣的人:我需要用实际的实现variables来replace概念上的用户variables,以防止variables冲突。那些知道lispmacros的人可以想到gensym的作用: ))。 因此,最简单的工作就是访问者在内部知道操作,并在访问的Action和Iteration的注释代码块上调用它们,并遍历所有语法树节点。 然而,在这种情况下,我不得不在testing代码中重复visitAction-Method和visitIteration-Method的assertion“transformation is applied”。 换句话说,我必须检查产品testing用例Traverse(== {遍历迭代,遍历动作,遍历序列})x变换(以及代码块变换,它被转化为迭代变换和动作变换)。 因此,我试图使用powermock来移除转换方法,并用一些“返回”来replace它,我被转换了!“ – ” – 存根。
然而,根据经验法则,我将该类拆分为一个包含NodeModifier-instance的类TreeModifier,它提供了方法modifyIteration,modifySequence,modifyCodeblock等。 因此,我可以很容易地testing遍历的责任,调用NodeModifier并重构树并分别testing代码块的实际修改,从而消除了对产品testing的需要,因为现在职责分离了(遍历和重build具体修改)。
稍后还会注意到,我可以在其他各种转换中重复使用TreeModifier。 🙂
如果您发现扩展类的function而不担心可能最终导致其他问题,或者您不能修改其行为的选项,而不能修改其行为,就像您的类太多了一样。
一旦我正在使用方法“ZipAndClean”,显然是压缩和清理指定的文件夹的传统类…