单一责任原则。 听起来并不简单

图片 单一责任原则,他就是单一责任原则,
他是统一可变性的原理-一个非常易懂的家伙,在程序员的采访中如此紧张。


第一次认真认识这个原则是在第一年年初,那时我们被带出了年轻,绿色的森林,从幼虫中培养出真正的学生。


在森林里,我们分为8-9人一组,每组比赛-快喝一瓶伏特加酒,前提是该组中的第一个人将伏特加倒入玻璃杯中,第二个人喝第三口。 完成操作后,该单元将位于组队列的末尾。


队列大小是三的倍数的情况,是SRP的良好实现。


定义1.单一责任。


单一责任原则(SRP)的官方定义表明,每个对象都有其自己的责任和存在的原因,而这种责任只有一个。


考虑Tippler对象。
为了实现SRP原则,我们将职责分为三部分:


  • 一倒( PourOperation
  • 一种饮料DrinkUpOperation
  • 一小食( TakeBiteOperation

该过程的每个参与者都对过程的一个组成部分负责,也就是说,它具有一种基本的责任-喝酒,倾倒或叮咬。


酒是这些操作的基础:


lass Tippler { //... void Act(){ _pourOperation.Do() //  _drinkUpOperation.Do() //  _takeBiteOperation.Do() //  } } 

图片

怎么了


人类程序员为猴子人编写代码,而猴子人则不专心,愚蠢,总是在某个地方赶时间。 他一次可以掌握并理解3至7个学期。
在喝酒的情况下,这些术语是三个。 但是,如果我们只写一张代码,那么手,眼镜,屠杀和关于政治的无休止辩论将出现在其中。 而所有这些将在一种方法的主体中。 我相信您在实践中已经看到了这样的代码。 这不是对心理的最人道考验。


另一方面,猴子人因在其头部模拟现实世界对象而被监禁。 在他的想象中,他可以将它们组合在一起,从它们中收集新物体并以相同的方式分解它们。 想象一下一辆旧汽车模型。 您可以凭自己的想象打开门,旋开门饰,然后查看车窗升降器机构,其中将有齿轮。 但是您无法一次列出一个列表,同时看到了计算机的所有组件。 至少“猴子人”不能。


因此,人类程序员将复杂的机制分解为一组不太复杂且工作的元素。 但是,分解可以通过不同的方式进行:在许多旧车中-管道从车门伸出,而在现代车中-锁电子装置的故障阻止了发动机的启动,该发动机在维修期间交付。


因此, SRP是解释如何分解(即在何处绘制分隔线)的原理


他说,分解应该基于“责任”分离的原则,即根据各种对象的任务。


图片

让我们回到猴子在分解时的酒水和好处:


  • 该代码在每个级别上都变得非常清晰。
  • 几个程序员可以一次编写代码(每个程序员编写一个单独的元素)
  • 自动化测试得到简化-元素越简单,测试就越容易
  • 在这三个操作中,将来,您可以添加一个 (仅使用TakeBitOperation ),一个酒精饮料(仅直接从瓶子中使用DrinkUpOperation )并满足许多其他业务需求。

当然,缺点是:


  • 将不得不创建更多类型。
  • 饮酒者比他晚了几个小时会第一次喝酒

定义2.统一可变性。


允许先生们! 饮酒班还履行一项责任-喝酒! 通常,“责任”一词是一个非常模糊的概念。 有人负责人类的命运,有人负责饲养企鹅。


考虑两个宾果游戏实现。 上面提到的第一类包括三个类别-倒酒,喝酒和咬一口。


第二种是通过“前向和仅前向”方法编写的,包含了Act方法中的所有逻辑:


 //      .    lass BrutTippler { //... void Act(){ //  if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity)) throw new OverdrunkException(); //  if(!_hand.TryDrink(from: _glass, size: _glass.Capacity)) throw new OverdrunkException(); // for(int i = 0; i< 3; i++){ var food = _foodStore.TakeOrDefault(); if(food==null) throw new FoodIsOverException(); _hand.TryEat(food); } } } 

从外部观察者的角度来看,这两个类看起来完全相同,并承担“饮用”的单一责任。


不好意思!


然后,我们在Internet上浏览并找到SRP的另一种定义-统一可变性原理。


该定义指出:“ 模块只有一个更改原因 。” 也就是说,“责任是变革的机会”。


现在一切都准备就绪。 另外,您可以更改倾倒,饮酒和咬人的程序,在酒水本身中,我们只能更改操作的顺序和组成,例如,在喝酒之前先移动小吃或添加面包吐司。


在前向和仅前向方法中,所有可以更改的内容仅在Act方法中更改。 在逻辑很少,很少更改的情况下,这是可读且有效的,但通常以每行500行的可怕方法结束,if数超过俄罗斯加入北约所需的if数。


定义3.变更的本地化。


饮酒者常常不明白为什么他们在别人的公寓里醒来,或者他们的手机在哪里。 现在该添加详细的日志记录了。


让我们从浇注过程开始记录:


 class PourOperation: IOperation{ PourOperation(ILogger log /*....*/){/*...*/} //... void Do(){ _log.Log($"Before pour with {_hand} and {_bottle}"); //Pour business logic ... _log.Log($"After pour with {_hand} and {_bottle}"); } } 

将其封装在PourOperation中 ,我们在责任和封装方面采取了明智的行动,但是现在有了可变性原则,我们现在很尴尬。 除了操作本身(可能会更改)之外,日志记录本身也变得可变。 我们必须为浇注操作分离并制作一个特殊的记录器:


 interface IPourLogger{ void LogBefore(IHand, IBottle){} void LogAfter(IHand, IBottle){} void OnError(IHand, IBottle, Exception){} } class PourOperation: IOperation{ PourOperation(IPourLogger log /*....*/){/*...*/} //... void Do(){ _log.LogBefore(_hand, _bottle); try{ //... business logic _log.LogAfter(_hand, _bottle"); } catch(exception e){ _log.OnError(_hand, _bottle, e) } } } 

细心的读者会注意到LogAfterLogBeforeOnError也可以分别更改,并且与前面的步骤类似,它将创建三个类: PourLoggerBeforePourLoggerAfterPourErrorLogger


并记住,一次狂欢有3种操作-我们得到了9类日志。 结果,整个酒会由14个(!!!)类组成。


夸张? 几乎没有! 有分解手榴弹的猴子人将把“倾倒者”压碎成一个倾析器,一个玻璃杯,一个倾倒的操作员,一个供水服务,一个分子碰撞的物理模型,下一个季度将尝试解开没有全局变量的依赖关系。 相信我-他不会停止。


正是在这一点上,许多人得出的结论是,SRP是粉红色王国的传说,而留下来扭曲面条……


...永远不知道Srp的第三个定义的存在:


与变更相似的事物应存储在一个地方 。” 或“ 一起改变的东西必须放在一个地方


也就是说,如果我们更改操作日志记录,那么我们必须在处更改它。


这是非常重要的一点-因为上面所有的SRP解释都说类型应该在拆分时进行拆分,也就是说,对对象的大小施加了“最高限制”,现在我们正在谈论“下限” 。 换句话说, SRP不仅需要“边粉碎边粉碎”,而且也不要过度使用-“不要粉碎链接的东西” 。 不要不必要地复杂化。 这是奥卡姆剃须刀与猴人的伟大战斗!


图片

现在酒会更容易了。 除了不将IPourLogger记录器分为三类之外,我们还可以将所有记录器组合为一种类型:


 class OperationLogger{ public OperationLogger(string operationName){/*..*/} public void LogBefore(object[] args){/*...*/} public void LogAfter(object[] args){/*..*/} public void LogError(object[] args, exception e){/*..*/} } 

并且,如果将第四种操作类型添加到了我们,则日志记录已准备就绪。 而且操作代码本身是干净的,没有基础结构噪音。


因此,我们提供了5个解决饮酒问题的课程:


  • 浇注操作
  • 酒后操作
  • 卡纸操作
  • 记录仪
  • 傻瓜的门面

他们每个人都严格负责一种功能,并且有更改的理由。 所有类似于变更的规则都在附近。


现实生活中的例子


序列化和反序列化

作为数据传输协议开发的一部分,有必要将某种类型的“用户”序列化和反序列化为字符串。


 User{ String Name; Int Age; } 

您可能认为序列化和反序列化需要在单独的类中完成:


 UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} } 

因为每个人都有自己的责任和改变的原因之一。


但是它们有一个共同的改变理由-“改变数据序列化的格式”。
并且当更改此格式时,序列化和反序列化将始终更改。


根据本地化变更的原则,我们必须将变更归为一类:


 UserSerializer{ String deserialize(User){...} User serialize(String){...} } 

这使我们免于不必要的复杂性,并且无需记住,每次更改串行器时,都需要记住解串器。


计算并保存

您需要计算公司的年收入并将其保存在文件C:\ results.txt中。


我们使用一种方法快速解决此问题:


 void SaveGain(Company company){ //     //   } 

从任务的定义来看,很明显有两个子任务-“计算收入”和“保存收入”。 它们每个都有一个更改的原因-“计算方法的更改”和“保存格式的更改”。 这些更改不会重叠。 另外,我们不能单音回答以下问题:“ SaveGain方法有什么作用?”。 此AND方法计算收入保存结果。


因此,您需要将此方法分为两种:


 Gain CalcGain(Company company){..} void SaveGain(Gain gain){..} 

优点:


  • 可以单独测试CalcGain
  • 更容易本地化错误并进行更改
  • 代码可读性提高
  • 由于简化,降低了每种方法的错误风险

复杂的业务逻辑

一旦我们编写了自动注册b2b客户端的服务。 有一种GOD方法,包含200行内容相似的内容:


  • 转到1C并获得一个帐户
  • 有了这个帐户,转到付款模块并到达那里
  • 检查是否尚未在主服务器中创建具有该帐户的帐户
  • 建立一个新帐户
  • 支付模块中的注册结果,编号1c添加到注册结果服务中
  • 将帐户信息添加到此表
  • 在点服务中为此客户创建一个点号。 给该服务帐号1s。

此列表上还有大约10多个具有可怕连接性的业务运营。 几乎每个人都需要该帐户对象。 一半的通话中需要点ID和客户名称。


经过一个小时的重构,我们能够将基础结构代码和使用该帐户的一些细微差别分离到单独的方法/类中。 God方法变得更容易了,但是剩下的100行代码不想被分解。


仅仅几天后,人们就意识到这种“缓解”方法的本质是业务算法。 而且传统知识的最初描述相当复杂。 试图将这种方法分解为若干部分,这将违反SRP,反之亦然。


形式主义。


现在是时候让我们的豪饮独处了。 擦干眼泪-我们一定会以某种方式返回到眼泪。 现在,我们将本文的知识形式化。


形式主义1. SRP的定义


  1. 分离元素,以便每个元素负责一件事。
  2. 责任代表“变革的原因”。 也就是说,就业务逻辑而言,每个元素只有一个更改的原因。
  3. 潜在的业务逻辑更改。 必须本地化。 一起可变的物品必须在附近。

形式主义2.自我检查的必要标准。


我没有满足执行SRP的充分标准。 但是有必要条件:


1)问自己一个问题-这个类/方法/模块/服务是做什么的。 您必须用一个简单的定义来回答。 (感谢Brightori


说明

但是,有时候很难找到一个简单的定义


2)修复错误或添加新功能会影响文件/类的最小数量。 理想情况下,一个。


说明

由于责任(针对功能或错误)封装在单个文件/类中,因此您确切地知道在哪里查找和编辑内容。 例如:更改操作日志输出的功能仅需要更改记录器。 不需要在其余代码中运行。


另一个示例是添加了一个类似于以前的新UI控件。 如果这迫使您添加10个不同的实体和15个不同的转换器-看来您已经“破产”。


3)如果多个开发人员正在处理项目的不同功能,则合并冲突的可能性(即,多个开发人员将同时更改同一文件/类的可能性)最小。


说明

如果在添加新操作“将伏特加酒倒在桌子下”时需要触摸记录仪(即饮酒和倒酒的操作),那么责任似乎就被歪曲了。 当然,这并不总是可能的,但是您需要尝试减少这个数字。


4)(从开发人员或经理那里)澄清有关业务逻辑的问题时,您必须严格遵守一个类/文件并仅从那里接收信息。


说明

功能,规则或算法都紧凑地写在一个地方,并且不会在整个代码空间中被标记分散。


5)命名很清楚。


说明

我们的类或方法对一件事情负责,并且责任反映在其名称中。


AllManagersManagerService-最有可能的,神级的
LocalPayment-可能不是


形式主义3. Occam-first的开发方法。


在设计之初,猴人并不知道并且不会感觉到所解决问题的所有细微之处,并且会犯错。 您可以通过不同的方式犯错误:


  • 通过承担不同的责任使对象过大
  • 拆分,将单个职责划分为许多不同的类型
  • 错误界定责任范围

记住以下规则很重要:“最好犯一个大错误”或“不确定-不要分裂”。 例如,如果您的班级承担了两个责任,那么它仍然是可以理解的,并且可以在客户端代码发生最小变化的情况下将其拆分为两个。 由于上下文分散在多个文件中,并且客户端代码中缺少必要的依赖性,因此从玻璃碎片中收集玻璃通常更加困难。


是时候四舍五入了


SRP的范围不限于OOP和SOLID。 它适用于方法,功能,类,模块,微服务和服务。 它适用于“ figax-figax-in-prod”和“ rocket-sainz”开发,使世界各地都变得更好。 如果您考虑一下,这几乎是所有工程学的基本原理。 机械工程,控制系统,甚至所有复杂系统都是由组件构建的,“不完整的碎片”使设计人员失去了灵活性,“碎片”(效率)以及不正确的界限(理性和安心)。


图片

SRP不是自然界发明的,也不是确切科学的一部分。 它摆脱了我们的生物学和心理限制,这只是使用人类猴子的大脑来控制和开发复杂系统的一种方式。 他告诉我们如何分解系统。 最初的措词需要相当多的心灵感应,但我希望本文能稍微消除烟幕。

Source: https://habr.com/ru/post/zh-CN454290/


All Articles