单一责任的困难原则

背景知识


在过去的几年中,我参加了大量的采访。 我向他们分别询问了申请人唯一责任的原则(以下简称SRP)。 而且大多数人对此原理一无所知。 即使是那些能读懂该定义的人,也几乎没人能说出他们如何在工作中使用这一原则。 他们不能说SRP如何影响他们编写的代码或同事的代码审查。 他们中的一些人还误以为SRP与整个SOLID一样,仅与面向对象的编程有关。 而且,通常人们无法识别出明显的违反该原则的情况,仅仅是因为代码是按照众所周知的框架推荐的样式编写的。
Redux是其准则违反SRP的框架的主要示例。

SRP很重要


我想从这个原则的价值及其带来的好处开始。 我还要指出,该原则不仅适用于OOP,而且适用于过程编程,功能性甚至声明性。 作为后者的代表,HTML可以并且也应该被分解,尤其是当它由UI框架(如React或Angular)控制时。 此外,该原理还适用于其他工程领域。 不仅工程,军事主题中也有这样一种表达:“分而治之”,这在很大程度上是同一原则的体现。 复杂性会杀死人,将其分成几部分,您将获胜。
关于其他工程领域,在轮毂上,有一篇有趣的文章,关于正在开发的飞机如何使引擎失效,而不是在飞行员的指挥下切换为倒车。 问题是他们误解了机箱的状态。 引擎控制器无需依赖控制底盘的系统,而直接读取位于底盘中的传感器,限位开关等。 文章中还提到,发动机甚至在原型飞机上之前都必须经过冗长的认证。 在这种情况下,违反SRP显然会导致以下事实:更改底盘的设计时,需要修改并重新认证引擎控制器中的代码。 更糟糕的是,违反这一原则几乎值得飞机和飞行员的生命。 幸运的是,我们的日常编程不会威胁到此类后果,但是,您仍然不应忽略编写良好代码的原则。 这就是为什么:

  1. 代码的分解降低了其复杂性。 例如,如果解决一个问题要求您编写循环复杂度为4的代码,则负责同时解决两个此类问题的方法将需要复杂度为16的代码。如果将其分为两种方法,则总复杂度将为8。当然,这并不总是如此归结为工作量,但是趋势将大致相同。
  2. 分解代码的单元测试得到简化,效率更高。
  3. 分解的代码对更改的抵抗力较小。 进行更改时,出错的可能性较小。
  4. 该代码的结构越来越好。 用文件和文件夹中排列的代码搜索内容要比用一块大脚垫轻松得多。
  5. 将样板代码与业务逻辑分开会导致这样的事实,即代码生成可以应用于项目中。

所有这些符号都在一起,它们是同一代码的符号。 例如,您不必在经过良好测试的代码和结构良好的代码之间进行选择。

现有定义不起作用


定义之一是:“更改代码(类或函数)应该只有一个原因”。 该定义的问题在于它与SOLID原则组中的第二个原则Open-Close原则冲突。 它的定义是:“代码必须开放才能扩展,而封闭则不能更改”。 变更与全面禁止变更的原因之一。 如果我们更详细地揭示这里的含义,那么就会发现原理之间没有冲突,但是模糊定义之间肯定存在冲突。

第二个更直接的定义是:“代码仅应承担一项责任。” 这个定义的问题在于,概括一切都是人类的本性。

例如,有一个农场种鸡,那时农场只负责一项工作。 因此,也决定在那里也繁殖鸭子。 本能地,我们将其称为家禽养殖场,而不是承认现在有两个责任。 在那儿放羊,现在这是一个宠物农场。 然后,我们要在那里种植西红柿或蘑菇,并提出以下更通用的名称。 变更的“一个原因”也是如此。 这个理由可以概括为想象力就足够了。

另一个示例是空间站管理器类。 他什么也没做,他只管理空间站。 您对这门课的责任感如何?
并且,由于我在求职者熟悉这项技术时提到过Redux,因此我也要问一个问题,典型的SRP减速器是否违反?

我记得,reducer包含switch语句,并且碰巧它会增长到数十甚至数百种情况。 Reducer的唯一责任是管理应用程序的状态转换。 即,从字面上看,一些申请人回答了。 而且没有任何迹象可以使这种观点脱离现实。

总体而言,如果某种代码似乎满足了SRP原理,但同时又闻起来不愉快-请知道为什么会发生这种情况。 因为“代码必须负有责任”的定义根本行不通。

更合适的定义


从反复试验中,我有了一个更好的定义:
代码责任不应太大

是的,现在您需要“衡量”类或功能的职责。 如果太大,则需要将此大责任分解为几个较小的责任。 回到农场的例子,即使是饲养鸡的责任也可能太大,例如,以某种方式将肉鸡与蛋鸡分开是有意义的。

但是如何衡量它,如何确定该代码的责任太大呢?

不幸的是,我没有数学上准确的方法,只有经验方法。 而且,所有这些大多数都是凭经验而来的,新手开发人员根本无法分解代码,更高级的开发人员则更擅长拥有代码,尽管他们不能总是描述他们为什么这样做以及它如何像SRP这样的理论。

  1. 公制圈复杂度。 不幸的是,有一些方法可以掩盖该指标,但是如果您收集它,那么就有可能在应用程序中显示最脆弱的位置。
  2. 函数和类的大小。 无需读取800行功能即可了解该功能有问题。
  3. 大量进口。 当我在一个相邻团队的项目中打开一个文件,并看到整个导入屏幕时,请向下按页面,然后再次在屏幕上只有导入。 仅在第二次按下后,我才看到代码的开头。 您可以说所有现代IDE都可以将导入内容隐藏在“加号”下,但是我说好的代码不需要隐藏“气味”。 另外,我需要重用一小段代码,并将其从该文件中删除到另一个文件中,四分之一甚至三分之一的导入都移到了该代码后面。 该代码显然不属于该代码。
  4. 单元测试。 如果您仍然难以确定职责量,请强迫自己编写测试。 如果您需要针对功能的主要目的编写两打测试,而不计算临界情况等,则需要分解。
  5. 测试开始时太多的准备步骤以及结束时进行的检查也是如此。 顺便说一下,在互联网上,您可以找到乌托邦式的声明,即所谓的 测试中应该只有一个断言。 我认为,提出任何绝对好的主意,都可能变得荒谬不切实际。
  6. 业务逻辑不应直接依赖于外部工具。 Oracle驱动程序Express路由,最好将所有这些与业务逻辑分开和/或隐藏在接口后面。

要点:

当然,正如我已经提到的,硬币有一个缺点,一行上的800种方法可能并不比800行上的一种方法好,所有方面都应该保持平衡。

第二点-我不讨论根据责任将这个或那个代码放在哪里的问题。 例如,有时开发人员在将太多逻辑引入DAL层时也遇到困难。

第三,我没有提出任何具体的硬性限制,例如“每个功能不超过50行”。 这种方法仅涉及开发人员甚至团队的发展方向。 他为我工作,他必须为他人赚钱。

最后,如果您经历了TDD,仅此一项就一定会使您分解代码很久,然后再编写20个带有20个断言的测试。

将业务逻辑与样板代码分开


谈论好的代码规则,您不能没有示例。 第一个示例是关于分离样板代码的。



此示例演示了通常如何编写后端代码。 人们通常使用指示Web服务器Express参数(例如URL,请求方法等)参数的代码来编写难解的逻辑。

我将业务逻辑标记为绿色标记,将红色的散布代码与查询参数交互(红色)。

我总是以这种方式分担这两个责任:



在此示例中,与Express的所有交互都在单独的文件中。

乍一看,第二个示例似乎没有带来任何改进,有2个文件而不是1个,出现了以前没有的附加行-类名和方法签名。 然后,这种代码分离带来了什么? 首先,“应用程序入口点”不再是Express。 现在,这是一个常规的Typescript函数。 还是一个javascript函数,无论是C#,谁就用什么编写WebAPI。

反过来,这允许您执行第一个示例中不可用的各种操作。 例如,您无需提高Express即可编写行为测试,而无需在测试内部使用http请求。 甚至无需进行任何润湿,都可以用“测试”对象替换Router对象,现在可以直接从测试中直接调用应用程序代码。

此分解提供的另一个有趣的功能是,您现在可以编写一个代码生成器,它将解析userApiService并使用它生成将此服务与Express连接的代码。 我打算在以后的出版物中指出以下几点:代码生成将不会节省编写代码的时间。 由于您现在不需要复制此样板,因此无法收回代码生成器的成本。 生成代码不需要支持这一事实,将使代码生成获得回报,这将节省时间,最重要的是,从长远来看,这将节省开发人员的神经。

分而治之


这种编写代码的方法已经存在很长时间了,我自己并没有发明它。 我得出的结论是,编写业务逻辑时非常方便。 为此,我想出了另一个虚拟的示例,向您展示了如何快速,轻松地编写可立即分解并通过命名方法自记录的代码。

假设您从业务分析师那里得到一项任务,以制定一种将员工报告发送给保险公司的方法。 为此:

  1. 数据必须从数据库中获取
  2. 转换为所需格式
  3. 发送结果报告

此类要求并非总是明确地写出,有时可能会通过与分析师的对话来隐含或阐明这样的顺序。 在实施该方法的过程中,不要急于打开与数据库或网络的连接,而应尝试将此简单算法转换为“按原样”的代码。 像这样:

async function sendEmployeeReportToProvider(reportId){ const data = await dal.getEmployeeReportData(reportId);​ const formatted = reportDataService.prepareEmployeeReport(data);​ await networkService.sendReport(formatted);​ } 

通过这种方法,它看起来是相当简单,易于阅读和测试的代码,尽管我认为该代码是微不足道的,不需要测试。 此方法的责任是不发送报告,它的责任是将这个复杂的任务分为三个子任务。

接下来,我们返回到要求,并发现该报告应包括薪金部分和带工作时间的部分。

 function prepareEmployeeReport(reportData){ const salarySection = prepareSalarySection(reportData);​ const workHoursSection = prepareWorkHoursSection(reportData);​ return { salarySection, workHoursSection };​ } 

依此类推,我们继续分解任务,直到实施接近平凡的小方法为止。

与开闭原理的相互作用


在本文的开头,我说过SRP和Open-Close原则的定义相互矛盾。 第一个说必须有一个更改的原因,第二个说必须为更改关闭代码。 这些原则本身不仅彼此不矛盾,相反,它们彼此协同工作。 所有这5条SOLID原则都旨在实现一个良好的目标-告诉开发人员哪个代码“不好”,以及如何对其进行更改以使其变为“好”。 具有讽刺意味的是-我只是将5个职责替换为另一个职责。
因此,除了前面将报告发送给保险公司的示例外,还可以想象有一位业务分析师来找我们,并说现在我们需要为该项目添加第二个功能。 必须打印相同的报告。
想象有一个开发人员认为SRP“与分解无关”。
因此,该原理并没有向他指示分解的必要,并且他在一个功能中实现了整个第一任务。 在完成任务后,他将这两项职责合而为一,因为 他们有很多共同点,并概括了它的名字。 现在,此职责称为“服务报告”。 这样的实现看起来像这样:
 async function serveEmployeeReportToProvider(reportId, serveMethod){ /* lots of code to read and convert the report */ switch(serveMethod) { case sendToProvider: /* implementation of sending */ case print: /* implementation of printing */ default: throw; } } 

提醒您项目中的一些代码? 正如我所说,SRP的两个直接定义都不起作用。 它们不会将无法编写此类代码的信息传送给开发人员。 以及可以编写什么代码。 开发人员更改此代码的原因仍然只有一个。 他只是简单地重新调用了先前的原因,增加了切换并且保持冷静。 此处出现了“打开-关闭”原理,这直接表明无法修改现有文件。 有必要编写代码,以便在添加新功能时有必要添加新文件,而不编辑现有文件。 即,从两个原理的角度来看,这样的代码是不好的。 如果第一个没有帮助看到它,第二个应该帮助。

分而治之方法如何解决相同的问题:
 async function printEmployeeReport(reportId){ const data = await dal.getEmployeeReportData(reportId);​ const formatted = reportDataService.prepareEmployeeReport(data);​ await printService.printReport(formatted);​ } 

添加新功能。 我有时称它们为“脚本函数”,因为它们不带有实现;它们确定了调用已分解的职责范围的顺序。 显然,前两行,前两个分解的职责与先前实现的功能的前两行重合。 就像业务分析师描述的两个任务的前两个步骤是一致的。
因此,为了向项目添加新功能,我们添加了新的脚本方法和新的printService。 旧文件未更改。 也就是说,从两个原则的角度来看,这种编写代码的方法是正确的。 和SRP和开闭

另类


我还想提到一种替代性的竞争方法,以得到看起来像这样的分解良好的代码-首先,我们将代码写在“额头上”,然后使用各种技术对其进行重构,例如,根据福勒(Fowler)的著作“重构”(Refactoring)。 这些方法使我想起了国际象棋的数学方法,在这种方法中,您不了解自己在策略上到底在做什么,您只计算位置的“权重”,并尝试通过移动来最大化它。 由于一个小原因,我不喜欢这种方法-为方法和变量命名已经很困难,并且当它们不具有业务价值时,就变得不可能。 例如,如果这些技术建议您需要从这里和那里选择6条相同的线,然后突出显示它们,那么您应该怎么称呼此方法? someSixIdenticalLines()?
我想预约-我不认为这种方法不好,我只是无法学习如何使用它。

合计


遵循该原则,您会发现好处。

“必须有一种责任”的定义不起作用。

有一个更好的定义和许多间接特征,即所谓的 代码闻到信号表明需要分解。

“分而治之”的方法使您可以立即编写结构良好且具有自说明性的代码。

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


All Articles