阐明单一责任原则


64

单一责任原则规定,一个班级只能做一件事情。有些情况很明确。但是,其他方法则很困难,因为在给定的抽象级别上查看时看起来像“一件事”,而在较低级别上查看时可能是多个事物。我还担心,如果在较低级别上遵循“单一责任原则”,则会导致过度分离,冗长的馄饨代码,而不是实际解决手头的问题,因为花更多的时间在所有内容上创建微型类并收集信息。

您将如何描述“一件事”的含义?有什么具体迹象表明一个班级确实比“一件事情”做得更多?


6
+1代表最上方的“馄饨代码”。在我职业生涯的早期,我就是那些太过分了的人之一。不仅是类,而且还有方法模块化。我的代码中充斥着大量的简单方法,它们的目的很简单,只是为了将问题分解为小块,而这些小块可以在屏幕上显示而无需滚动。显然,这常常太过分了。
鲍比桌

Answers:


50

我非常喜欢Robert C. Martin(鲍勃叔叔)重申“单一责任原则”的方式(链接到PDF)

改变班级的理由不应该只有一个以上

它与传统的“只能做一件事”的定义有细微的区别,我喜欢这一点,因为它迫使您改变对课堂的看法。您不必考虑“这是一件事情吗?”,而是考虑可以进行哪些更改以及这些更改如何影响您的班级。例如,如果数据库发生更改,您的类是否需要更改?如果输出设备发生更改(例如屏幕,移动设备或打印机),该怎么办?如果您的班级由于其他许多方面的变化而需要更改,则表明您的班级职责过多。

在链接的文章中,鲍勃叔叔得出以下结论:

SRP是最简单的原则之一,也是最难解决的原则之一。共同承担责任是我们自然要做的事情。查找和分离这些责任是软件设计真正要解决的大部分问题。


2
我也喜欢他的陈述方式,似乎更容易检查与变更相关的时间,而不是抽象的“责任”。
Matthieu M.

这实际上是一个很好的表达方式。我喜欢那个。需要注意的是,我通常倾向于认为SRP在方法上的应用更为广泛。有时,一个类只需要做两件事(也许是该类在两个域之间架起了桥梁),但是一个方法几乎绝不能做超过其类型签名可以简要描述的事情。
CodexArcanum

1
刚刚向我的研究生展示了这本书-读得很好,对我自己也有很好的提醒作用。
Martijn Verburg

1
好的,当您将其与非过度设计的代码应仅计划在可预见的将来可能发生的更改而不是对所有可能的更改进行规划的想法结合使用时,这很有意义。然后,我稍微重申一下这一点,因为“在可预见的将来,应该只有一种原因可能导致课程改变”。这样可以鼓励在设计中不太可能更改的部分中简化操作,并在可能更改的部分中解耦。
dsimcha 2011年

18

我一直在问自己,SRP试图解决什么问题?SRP何时能帮助我?这是我想出的:

在以下情况下,您应该在类之外重构职责/功能:

1)您已经复制了功能(DRY)

2)您发现您的代码需要另一层次的抽象才能帮助您理解它(KISS)

3)您发现您的域专家将功能部分理解为属于不同组件的一部分(无所不在的语言)

在以下情况下,您不应该在班级之外重构责任:

1)没有任何重复的功能。

2)该功能在您的类上下文之外没有意义。换句话说,您的类提供了一个上下文,在其中可以更轻松地理解功能。

3)您的域专家没有这种责任感。

在我看来,如果将SRP广泛地应用,我们就将一种复杂性(试图使一类的头或尾在内部进行过多的事情)与另一种复杂性(试图保持所有协作者/级别的直接弄清楚这些类的实际作用)。

如有疑问,请不要理会!如果有明确的理由,您以后可以随时进行重构。

你怎么看?


尽管这些准则可能有用也可能没有帮助,但它实际上与SOLID中定义的SRP没有任何关系,是吗?
萨拉

谢谢。我不敢相信所谓的SOLID原则所涉及的一些疯狂之处,因为它们没有充分的理由就使非常简单的代码变得复杂一百倍。您提供的观点描述了实施SRP的现实原因。我认为您在上面给出的原则应该成为他们自己的缩写,并且抛弃“ SOLID”,弊大于利。正如您在引导我到这里的话题中所指出的那样,确实是“建筑宇航员”
Nicholas Petersen

4

我不知道它是否有一个客观的规模,但要放弃的将是方法-不是很多方法,而是各种功能。我同意您可以将分解做得太远,但是我不会遵循任何严格的规则。


4

答案在于定义

您所定义的责任最终为您提供了界限。

例:

我有一个组件负责显示发票->在这种情况下,如果我开始添加更多内容,那我就违反了原理。

另一方面,如果我说过处理发票的责任->添加多个较小的功能(例如,打印发票,更新发票)都在该范围之内。

但是,如果该模块开始处理发票以外的任何功能,那么它将不在该边界之内。


我想这里的主要问题是“处理”一词。定义单一职责过于笼统。我认为最好是由一个负责打印发票的组件和另一个负责更新发票的组件,而不是一个组件处理{打印,更新,以及-为什么不这样做?-显示,无论}发票
马查多

1
OP实质上是在问“您如何定义责任?” 所以当您说责任是您定义的责任时,似乎只是在重复问题。
Despertar

2

我总是在两个层面上查看它:

  • 我确保我的方法只会做一件事并且做得很好
  • 我将类视为这些方法的逻辑(OO)分组,它们很好地表示了一件事

因此,类似于名为Dog的域对象:

Dog是我的班,但是狗可以做很多事情!我可能有方法,如walk()run()bite(DotNetDeveloper spawnOfBill)(抱歉无法抗拒; P)。

如果Dog变得笨拙,那么我会考虑如何在另一个类(例如Movement包含我walk()run()方法的类)中一起建模这些方法的组。

没有硬性规定,您的OO设计将随着时间而发展。我尝试使用明确的接口/公共API以及简单的方法,这些方法可以很好地完成一件事和一件事。


Bite应该确实采用Object的实例,而DotNetDeveloper应该是Person的子类(通常,无论如何!)
Alan Pearce 2010年

@Alan-那里-为您解决了这一问题:-)
Martijn Verburg 2010年

1

我更多地按照类的观点看待它,它仅代表一件事。适当@ Karianna的例子,我有我的Dog类,它有方法walk()run()bark()。我不会添加方法meaow()squeak()slither()fly()因为这些都不是东西,狗做的。它们是其他动物所做的事情,这些其他动物将具有自己的类来表示它们。

(顺便说一句,如果您的狗确实飞了,那么您可能应该停止将其扔出窗外)。


+1表示“如果您的狗确实飞了,那么您应该停止将其扔出窗外”。:)
Bobby Tables 2010年

除了应该代表什么的问题之外,实例还代表什么?如果一个人的问候SeesFood作为的特征DogEyesBark如一些由完成DogVoice,并Eat作为东西被完成DogMouth,那么逻辑一样if (dog.SeesFood) dog.Eat(); else dog.Bark();会变得if (eyes.SeesFood) mouth.Eat(); else voice.Bark();,失去的是眼睛,嘴巴和语音都连接到一个entitity身份的任何意义。
2014年

@supercat是一个公平的观点,尽管上下文很重要。如果您提到的代码在Dog该类之内,则可能与它Dog有关。如果没有,那么您可能最终会得到类似myDog.Eyes.SeesFood而不仅仅是的东西eyes.SeesFood。另一种可能性是Dog公开ISee要求Dog.Eyes属性和SeesFood方法的接口。
JohnL

@JohnL:如果实际的观察机制是由狗的眼睛以与猫或斑马的本质相同的方式处理的,那么由Eye班级处理机制可能是有意义的,但是狗应该“看到”使用它的眼睛,而不是仅仅拥有可以看到的眼睛。狗不是眼睛,也不是单纯的眼神。这是“至少可以尝试看到的东西”,应通过接口进行描述。甚至可以问瞎狗是否能看见食物。这不会很有用,因为狗总是会说“不”,但是问问没有任何害处。
2014年

然后,您将使用我在评论中描述的ISee界面。
JohnL 2014年

1

从自己的抽象层次来看,一个类应该做一件事。毫无疑问,它将在不太抽象的级别上做很多事情。这就是类如何使程序更易于维护的方式:如果您不需要仔细检查它们,它们将隐藏实现细节。

我使用类名对此进行测试。如果我不能给班级一个简短的描述性名称,或者该名称中带有类似“ And”的字眼,则可能违反了“单一责任原则”。

以我的经验,将这一原则保持在较低的层次上会更容易些,在较低的层次上事情会更加具体。


0

这是关于拥有一个独特的角色

每个类都应使用角色名称恢复。角色实际上是与上下文相关联的(一组)动词。

例如 :

文件提供对文件的访问。FileManager管理文件对象。

文件中一种资源的资源保留数据。ResourceManager拥有并提供所有资源。

在这里您可以看到某些动词(例如“ manage”(管理))暗示着一组其他动词。在大多数情况下,动词比类更好地被视为函数。如果动词暗示太多具有各自共同上下文的动作,则它本身应该是一个类。

因此,这个想法只是让您通过定义一个独特的角色来简单地了解类的作用,该角色可能是几个子角色(由成员对象或其他对象执行)的集合。

我经常构建其中包含几个其他不同类的Manager类。像工厂,注册处等。请参见经理类,例如某种乐队负责人,乐团负责人,指导其他人共同努力以实现高水平的想法。他扮演一个角色,但暗示他在内部扮演其他独特角色。您还可以像公司的组织结构一样看到它:CEO并不是纯粹的生产力水平上的有生产力的人,但是如果他不在那儿,那么任何人都无法正常工作。那是他的角色。

在设计时,请确定唯一的角色。对于每个角色,再次查看它是否不能兼任其他几个角色。这样,如果您需要简单地更改Manager构建对象的方式,只需更改Factory并放心。


-1

SRP不仅涉及划分类别,还涉及委托功能。

在上面使用的Dog示例中,不要使用SRP作为理由来拥有3个单独的类,例如DogBarker,DogWalker等(内聚性低)。相反,请查看类方法的实现并确定它们是否“了解太多”。您仍然可以拥有dog.walk(),但也许walk()方法应该将如何完成步行的细节委托给另一个类。

实际上,我们允许Dog类具有更改的一个原因:因为Dogs更改。当然,将其与其他SOLID原理相结合,您将扩展Dog以获得新功能,而不是更改Dog(打开/关闭)。然后将注入依赖项,例如IMove和IEat。当然,您需要制作这些单独的接口(接口隔离)。只有在我们发现错误或Dogs发生了根本变化时,Dog才会更改(Liskov Sub,请不要扩展然后删除行为)。

SOLID的最终效果是,与修改现有代码相比,我们可以更频繁地编写新代码,这是一个很大的胜利。


-1

这完全取决于职责的定义以及该定义将如何影响代码的维护。一切都归结为一件事,这就是您的设计将如何帮助您维护代码。

就像有人说的那样,“只要给定规格,就可以轻松地按照给定的规格进行水上工作和设计软件”。

因此,如果我们以更具体的方式定义责任,则无需更改它。

有时责任很明显,但是有时责任可能很微妙,我们需要明智地决定。

假设,我们向Dog类添加了另一个责任,名为catchThief()。现在,它可能会导致其他不同的责任。明天,如果警察局必须更改Dog抓小偷的方式,则必须更改Dog的等级。在这种情况下,最好创建另一个子类并将其命名为ThiefCathcerDog。但是从不同的角度来看,如果我们确定在任何情况下它都不会改变,或者catchThief的实现方式取决于某些外部参数,那么完全可以承担此责任。如果责任不是很奇怪,那么我们必须根据用例明智地决定责任。


-1

“改变的一个理由”取决于谁在使用该系统。确保每个角色都有用例,并列出最可能的更改,对于用例的每种可能更改,请确保该更改仅影响一个类。如果要添加一个全新的用例,请确保只需要扩展一个类即可。


1
更改的原因与用例的数量,参与者或更改的可能性无关。您不应该列出可能的更改。一项更改只影响一个班级是正确的。能够扩展课程以适应这种变化是件好事,但这是开放式封闭原则,而不是SRP。
candied_orange

为什么我们不列出最可能发生的变化?投机设计?
kiwicomb123,2013年

因为它无济于事,也不会是完整的,并且有比尝试预测更有效的应对方式。只是期望。隔离决策,其影响将是最小的。
candied_orange 2016年

好的,我知道,它违反了“您不需要它”的原则。
kiwicomb123,2013年

实际上,yagni也可以推得更远。这实际上是要阻止您实施推测性用例。不要阻止您隔离已实现的用例。
candied_orange
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.