单一责任原则规定,一个班级只能做一件事情。有些情况很明确。但是,其他方法则很困难,因为在给定的抽象级别上查看时看起来像“一件事”,而在较低级别上查看时可能是多个事物。我还担心,如果在较低级别上遵循“单一责任原则”,则会导致过度分离,冗长的馄饨代码,而不是实际解决手头的问题,因为花更多的时间在所有内容上创建微型类并收集信息。
您将如何描述“一件事”的含义?有什么具体迹象表明一个班级确实比“一件事情”做得更多?
单一责任原则规定,一个班级只能做一件事情。有些情况很明确。但是,其他方法则很困难,因为在给定的抽象级别上查看时看起来像“一件事”,而在较低级别上查看时可能是多个事物。我还担心,如果在较低级别上遵循“单一责任原则”,则会导致过度分离,冗长的馄饨代码,而不是实际解决手头的问题,因为花更多的时间在所有内容上创建微型类并收集信息。
您将如何描述“一件事”的含义?有什么具体迹象表明一个班级确实比“一件事情”做得更多?
Answers:
我非常喜欢Robert C. Martin(鲍勃叔叔)重申“单一责任原则”的方式(链接到PDF):
改变班级的理由不应该只有一个以上
它与传统的“只能做一件事”的定义有细微的区别,我喜欢这一点,因为它迫使您改变对课堂的看法。您不必考虑“这是一件事情吗?”,而是考虑可以进行哪些更改以及这些更改如何影响您的班级。例如,如果数据库发生更改,您的类是否需要更改?如果输出设备发生更改(例如屏幕,移动设备或打印机),该怎么办?如果您的班级由于其他许多方面的变化而需要更改,则表明您的班级职责过多。
在链接的文章中,鲍勃叔叔得出以下结论:
SRP是最简单的原则之一,也是最难解决的原则之一。共同承担责任是我们自然要做的事情。查找和分离这些责任是软件设计真正要解决的大部分问题。
我一直在问自己,SRP试图解决什么问题?SRP何时能帮助我?这是我想出的:
在以下情况下,您应该在类之外重构职责/功能:
1)您已经复制了功能(DRY)
2)您发现您的代码需要另一层次的抽象才能帮助您理解它(KISS)
3)您发现您的域专家将功能部分理解为属于不同组件的一部分(无所不在的语言)
在以下情况下,您不应该在班级之外重构责任:
1)没有任何重复的功能。
2)该功能在您的类上下文之外没有意义。换句话说,您的类提供了一个上下文,在其中可以更轻松地理解功能。
3)您的域专家没有这种责任感。
在我看来,如果将SRP广泛地应用,我们就将一种复杂性(试图使一类的头或尾在内部进行过多的事情)与另一种复杂性(试图保持所有协作者/级别的直接弄清楚这些类的实际作用)。
如有疑问,请不要理会!如果有明确的理由,您以后可以随时进行重构。
你怎么看?
您所定义的责任最终为您提供了界限。
例:
我有一个组件负责显示发票->在这种情况下,如果我开始添加更多内容,那我就违反了原理。
另一方面,如果我说过处理发票的责任->添加多个较小的功能(例如,打印发票,更新发票)都在该范围之内。
但是,如果该模块开始处理发票以外的任何功能,那么它将不在该边界之内。
我总是在两个层面上查看它:
因此,类似于名为Dog的域对象:
Dog
是我的班,但是狗可以做很多事情!我可能有方法,如walk()
,run()
和bite(DotNetDeveloper spawnOfBill)
(抱歉无法抗拒; P)。
如果Dog
变得笨拙,那么我会考虑如何在另一个类(例如Movement
包含我walk()
和run()
方法的类)中一起建模这些方法的组。
没有硬性规定,您的OO设计将随着时间而发展。我尝试使用明确的接口/公共API以及简单的方法,这些方法可以很好地完成一件事和一件事。
我更多地按照类的观点看待它,它仅代表一件事。适当@ Karianna的例子,我有我的Dog
类,它有方法walk()
,run()
和bark()
。我不会添加方法meaow()
,squeak()
,slither()
或fly()
因为这些都不是东西,狗做的。它们是其他动物所做的事情,这些其他动物将具有自己的类来表示它们。
(顺便说一句,如果您的狗确实飞了,那么您可能应该停止将其扔出窗外)。
SeesFood
作为的特征DogEyes
,Bark
如一些由完成DogVoice
,并Eat
作为东西被完成DogMouth
,那么逻辑一样if (dog.SeesFood) dog.Eat(); else dog.Bark();
会变得if (eyes.SeesFood) mouth.Eat(); else voice.Bark();
,失去的是眼睛,嘴巴和语音都连接到一个entitity身份的任何意义。
Dog
该类之内,则可能与它Dog
有关。如果没有,那么您可能最终会得到类似myDog.Eyes.SeesFood
而不仅仅是的东西eyes.SeesFood
。另一种可能性是Dog
公开ISee
要求Dog.Eyes
属性和SeesFood
方法的接口。
Eye
班级处理机制可能是有意义的,但是狗应该“看到”使用它的眼睛,而不是仅仅拥有可以看到的眼睛。狗不是眼睛,也不是单纯的眼神。这是“至少可以尝试看到的东西”,应通过接口进行描述。甚至可以问瞎狗是否能看见食物。这不会很有用,因为狗总是会说“不”,但是问问没有任何害处。
这是关于拥有一个独特的角色。
每个类都应使用角色名称恢复。角色实际上是与上下文相关联的(一组)动词。
例如 :
文件提供对文件的访问。FileManager管理文件对象。
文件中一种资源的资源保留数据。ResourceManager拥有并提供所有资源。
在这里您可以看到某些动词(例如“ manage”(管理))暗示着一组其他动词。在大多数情况下,动词比类更好地被视为函数。如果动词暗示太多具有各自共同上下文的动作,则它本身应该是一个类。
因此,这个想法只是让您通过定义一个独特的角色来简单地了解类的作用,该角色可能是几个子角色(由成员对象或其他对象执行)的集合。
我经常构建其中包含几个其他不同类的Manager类。像工厂,注册处等。请参见经理类,例如某种乐队负责人,乐团负责人,指导其他人共同努力以实现高水平的想法。他扮演一个角色,但暗示他在内部扮演其他独特角色。您还可以像公司的组织结构一样看到它:CEO并不是纯粹的生产力水平上的有生产力的人,但是如果他不在那儿,那么任何人都无法正常工作。那是他的角色。
在设计时,请确定唯一的角色。对于每个角色,再次查看它是否不能兼任其他几个角色。这样,如果您需要简单地更改Manager构建对象的方式,只需更改Factory并放心。
SRP不仅涉及划分类别,还涉及委托功能。
在上面使用的Dog示例中,不要使用SRP作为理由来拥有3个单独的类,例如DogBarker,DogWalker等(内聚性低)。相反,请查看类方法的实现并确定它们是否“了解太多”。您仍然可以拥有dog.walk(),但也许walk()方法应该将如何完成步行的细节委托给另一个类。
实际上,我们允许Dog类具有更改的一个原因:因为Dogs更改。当然,将其与其他SOLID原理相结合,您将扩展Dog以获得新功能,而不是更改Dog(打开/关闭)。然后将注入依赖项,例如IMove和IEat。当然,您需要制作这些单独的接口(接口隔离)。只有在我们发现错误或Dogs发生了根本变化时,Dog才会更改(Liskov Sub,请不要扩展然后删除行为)。
SOLID的最终效果是,与修改现有代码相比,我们可以更频繁地编写新代码,这是一个很大的胜利。
这完全取决于职责的定义以及该定义将如何影响代码的维护。一切都归结为一件事,这就是您的设计将如何帮助您维护代码。
就像有人说的那样,“只要给定规格,就可以轻松地按照给定的规格进行水上工作和设计软件”。
因此,如果我们以更具体的方式定义责任,则无需更改它。
有时责任很明显,但是有时责任可能很微妙,我们需要明智地决定。
假设,我们向Dog类添加了另一个责任,名为catchThief()。现在,它可能会导致其他不同的责任。明天,如果警察局必须更改Dog抓小偷的方式,则必须更改Dog的等级。在这种情况下,最好创建另一个子类并将其命名为ThiefCathcerDog。但是从不同的角度来看,如果我们确定在任何情况下它都不会改变,或者catchThief的实现方式取决于某些外部参数,那么完全可以承担此责任。如果责任不是很奇怪,那么我们必须根据用例明智地决定责任。
“改变的一个理由”取决于谁在使用该系统。确保每个角色都有用例,并列出最可能的更改,对于用例的每种可能更改,请确保该更改仅影响一个类。如果要添加一个全新的用例,请确保只需要扩展一个类即可。