你如何定义单一职责原则?

40

我知道“类只有单一责任原则”。那到底是什么?有哪些迹象可以告诉我们,这个类不仅仅承担了一个职责?或者真正的答案是隐藏在YAGNI中,只有当你的类第一次发生变化时才重构为单一职责?


相关:https://dev59.com/d7Tna4cB1Zd3GeqPB-hw - jaco0646
13个回答

26

单一职责原则

有许多明显的例子,例如 CoffeeAndSoupFactory。在同一个容器中放入咖啡和汤可能会导致非常不好的结果。在这个例子中,容器可以被分解为 HotWaterGenerator 和某种类型的 Stirrer。然后可以从这些组件构建新的 CoffeeFactorySoupFactory,避免任何意外的混合。

在更微妙的情况下,数据访问对象(DAO)和数据传输对象(DTO)之间的紧张关系非常普遍。 DAO 与数据库对话,DTO 可序列化以在进程和计算机之间传输。通常,DAO 需要引用您的数据库框架,因此它们无法在既没有安装数据库驱动程序也没有必要权限访问数据库的富客户端上使用。

代码异味

  • 一个类中的方法开始按功能区域进行分组("这些是咖啡方法,这些是汤方法")。

  • 实现许多接口。


25

写一个简短但准确的描述该类的功能。

如果描述中包含单词 "and",则需要进行拆分。


12

好的,这个原则要加一些限制条件...以避免类爆炸。

单一职责并不意味着每个方法都应该独立成类。它是指一个对象存在的单一原因...为其客户提供的一项服务。

保持正确的编程方向的好方法... 就是将对象视为人的隐喻... 如果这个对象是一个人,我会找谁来完成这个任务?将该职责分配给相应的类。但是您不会让同一个人处理文件管理、计算工资、发放薪水和核实财务记录... 您为什么想让一个对象承担所有这些任务呢?(如果一个类承担多个相关且连贯的职责,那么也可以接受)

  • 如果您使用CRC卡片作为辅助工具,这是一个不错的细微指南。如果您在CRC卡片上无法列出该对象的所有职责,那么它可能包含了过多... 最好不超过7个职责。
  • 《重构》一书中的另一个代码异味是巨大的类。散弹手术是另一个... 对类的一个区域进行更改会导致同一类的其他不相关区域出现错误...
  • 如果发现您一遍又一遍地为不相关的错误修复同一个类,则表明该类包含的职责过多。

你说“它意味着存在的单一原因” - 我想要表达的是,“一个改变的单一原因”。 - Joris Timmermans

10

一个简单实用的检查单一职责原则(不仅仅是类,还包括类中的方法)的方法是选择名称。当您设计一个类时,如果您能够轻松地为该类找到一个能够准确指定其定义的名称,那么您就是朝着正确的方向前进了。

选择名称时遇到的困难几乎总是糟糕设计的症状。


6

你的类中的方法应该具有内聚性...它们应该共同工作并在内部使用相同的数据结构。如果你发现有太多似乎不完全相关或似乎在操作不同事物的方法,那么很可能你没有一个好的单一职责。

通常,最初找到职责是很困难的,有时你需要在几个不同的上下文中使用类,然后将类重构为两个类,从而开始看到区别。有时你会发现这是因为你将抽象和具体概念混合在一起。它们往往更难看出来,再次使用不同的上下文将有助于澄清。


6
明显的标志是,当你的类最终看起来像一个大泥球时,这实际上是SRP(单一责任原则)的相反。基本上,所有对象的服务都应该专注于执行单一职责,这意味着每次你的类改变并添加不符合该标准的服务时,你就知道你正在“偏离”“正确”的道路 ;)。通常,造成这种情况的原因是为了修复一些缺陷而匆忙添加到类中的一些快速修复措施。因此,你更改类的原因通常是检测是否即将违反SRP的最佳标准。

3

Martin的C#中的敏捷原则、模式和实践帮助我很好地掌握了SRP。他将SRP定义为:

一个类应该只有一个改变的原因。

那么是什么驱动着变化呢?

Martin的答案是:

[...]每个职责都是一个变化的轴。(第116页)

并且进一步解释道:

在SRP的背景下,我们将职责定义为变更的原因。如果您可以想到多个更改类的动机,则该类具有多个职责(第117页)。

事实上,SRP是封装变化的。如果发生变化,它应该只对局部产生影响。

那么YAGNI在哪里呢?

YAGNI可以与SRP很好地结合使用:当您应用YAGNI时,您会等待直到实际发生某些更改。如果发生这种情况,则应能清楚地看到从更改原因推导出的职责。
这也意味着每个新要求和更改都可以使职责发展。进一步思考SRP和YAGNI将为您提供以灵活的设计和架构思考的手段。

2

我一直在努力理解面向对象设计的SOLID原则,特别是单一职责原则,也就是SRP(顺便说一句,与Jeff Atwood、Joel Spolsky和“Uncle Bob”合作的播客值得一听)。对我来说最重要的问题是:SOLID试图解决什么问题?

OOP的全部内容都是关于建模。建模的主要目的是以一种使我们能够理解并解决问题的方式呈现问题。建模迫使我们专注于重要的细节。同时,我们可以使用封装来隐藏“不重要”的细节,这样我们只需在绝对必要时才必须处理它们。

我想你应该问自己:你的类正在尝试解决什么问题?你需要解决这个问题的重要信息是否已经浮出水面?不重要的细节是否被隐藏起来,这样你只需要在绝对必要时才考虑它们?

思考这些事情会导致更易于理解、维护和扩展的程序。我认为这是面向对象设计(OOD)和SOLID原则,包括SRP的核心。

2

或许比其他代码更加技术化的一些警示:

  • 如果你发现需要多个"friend"类或函数,那通常是SRP原则糟糕的迹象 - 因为你的类实际上没有公开所需的功能。
  • 如果你最终得到一个过于“深”或“宽”的层次结构(在获取叶节点之前有长列表的派生类,或在单个父类上浅层派生许多类),通常表明父类要么做太多要么太少。其中什么都不做是这个问题的极限。是的,我已经在实践中看到了这种情况,只是空的父类定义,仅将一堆不相关的类组合在单个层次结构中。

我也发现将类重构为单一职责很难。当你最终开始进行时,类的不同职责已经交织在客户端代码中,使得在不破坏其他东西的情况下分离出一个内容变得困难。 我更喜欢自己犯“太少”的错误而不是“太多”的错误。


2
如果你最终得到了使用MemberAMethodA和使用MemberBMethodB,并且它不是某种并发或版本控制方案的一部分,那么你可能正在违反SRP原则。
如果你注意到你有一个类只是将调用委托给许多其他类,你可能陷入了代理类地狱。特别是当你可以直接使用特定的类时,你到处实例化代理类时尤其如此。我见过很多这样的情况。将ProgramNameBLProgramNameDAL类视为使用存储库模式的替代方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接