可以/应该将单一责任原则应用于新法规吗?


20

将该原理定义为具有更改理由的模块。我的问题是,在代码实际开始更改之前,这些更改原因肯定是未知的吗?几乎每一段代码有许多原因,它可能会可能改变,但肯定尝试预见所有的这些设计和代码考虑到这一点最终会很差代码。仅在开始请求更改代码时才真正开始应用SRP,这不是一个更好的主意吗?更具体地说,当一段代码由于多个原因而多次更改时,因此证明它具有多个更改原因。尝试猜测更改的原因听起来很反敏捷。

一个示例是一段打印文档的代码。提出了将其更改为打印为PDF的请求,然后再次请求将其更改为对文档应用某些不同的格式。在这一点上,您有证据证明有多个原因需要更改(并且违反了SRP),并且应该进行适当的重构。


6
@Frank-实际上通常是这样定义的-参见例如en.wikipedia.org/wiki/Single_responsibility_principle
Joris Timmermans

1
表达方式不是我理解SRP定义的方式。
Pieter B

2
每行代码都有(至少)被更改的两个原因:导致错误或干扰新要求。
巴特·范·英根·谢瑙

1
@BartvanIngenSchenau:大声笑;-)如果您这样看,则SRP无法在任何地方应用。
布朗

1
@DocBrown:如果不将SRP与更改的源代码结合使用,则可以。例如,如果您将SRP解释为能够在不使用单词and的情况下全面说明类/函数在一个句子中的功能,并且(并且没有狡猾的措辞来解决该限制)。
Bart van Ingen Schenau,

Answers:


27

当然,YAGNI原则会告诉您不要在真正需要它之前应用SRP。但是您应该问自己的问题是:我是否需要首先并且仅在必须实际更改代码时才应用SRP?

以我的经验,SRP的应用为您带来了更早的好处:当您必须找出在何处以及如何在代码中应用特定更改时。对于此任务,您必须阅读并理解现有的函数和类。当您所有的函数和类都负有特定责任时,这将变得非常容易。因此,恕我直言,每当它使您的代码更易于阅读,每一次使您的函数更小,更易于描述时,都应应用SRP。因此答案是肯定的,即使对于新代码也应应用SRP。

例如,当您的打印代码读取文档,格式化文档并将结果打印到特定设备时,这是3个明确的可分离职责。因此,至少要使用其中3个功能,并按名称命名。例如:

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

现在,当您收到更改文档格式的新要求或将另一格式打印为PDF的新要求时,您确切地知道必须在代码中的哪些功能或位置中进行更改,甚至更重要的是,无需更改。

因此,每当您使用某个功能时,由于该功能“太多”而无法理解,并且不确定是否在何处应用更改,然后考虑将该功能重构为单独的较小的功能。不要等到您必须更改某些内容。读取代码的频率是更改代码的10倍,而较小的函数更易于读取。以我的经验,当功能具有一定的复杂性时,您始终可以将功能划分为不同的职责,而不必知道将来会发生哪些变化。鲍勃·马丁(Bob Martin)通常会更进一步,请参阅下面我在评论中给出的链接。

编辑:征求您的意见:上面示例中外部功能的主要职责不是打印到特定设备,也不是格式化文档-而是集成了打印工作流程。因此,在外部函数的抽象级别上,诸如“不应再格式化文档”或“应邮寄文档而不是打印文档”之类的新要求仅仅是“相同原因”,即“打印工作流程已更改”。如果我们谈论这样的事情,那么坚持正确的抽象水平很重要。


我通常总是使用TDD进行开发,因此在我的示例中,由于无法进行测试,因此我实际上无法将所有逻辑都保留在一个模块中。这只是TDD的副产品,而不是因为我故意应用SRP。我的示例具有相当明确的职责,因此可能不是一个很好的示例。我想我要问的是,您可以编写任何代码并明确地说,是的,这不违反SRP吗?业务不是本质上定义的“变革理由”吗?
SeeNoWeevil

3
@thecapsaicinkid:是的,您可以(至少通过立即重构)。但是您将获得非常非常小的功能-并非每个程序员都喜欢。参见以下示例:sites.google.com/site/unclebobconsultingllc/…–
布朗

如果您通过预期更改的原因来应用SRP,在您的示例中,我仍然可以说它不仅仅是一个原因更改。该企业可以决定他们不再要格式化文档,然后再决定要通过电子邮件而不是打印该文档。编辑:只需阅读链接,虽然我并不特别喜欢最终结果,但“提取直到您无法提取更多内容”才有意义,而且比“只有一个改变的理由”就更没有歧义。虽然不是很务实。
SeeNoWeevil

1
@thecapsaicinkid:看我的编辑。外部功能的主要职责不是打印到特定设备或格式化文档,而是集成打印工作流程。当此工作流程更改时,这就是该功能将更改的唯一原因
Doc Brown

您关于坚持正确的抽象级别的评论似乎是我所缺少的。例如,我有一个类,我将其描述为“从JSON数组创建数据结构”。听起来对我来说是一个责任。循环遍历JSON数组中的对象,并将它们映射到POJO。如果我坚持与描述相同的抽象水平,那么很难说它有一个以上的变化理由,即“ JSON如何映射到对象”。作为较少抽象的我可以说有多种原因如我如何映射日期字段的变化,数值如何映射到天等
SeeNoWeevil

7

我认为您误会了SRP。

更改的唯一原因不是更改代码,而是更改代码。


3

我认为,SRP的定义是“有一个改变的理由”,正是因为这个原因。正视其面值:“单一责任原则”说,一个类或功能应完全具有一个责任。仅有一个改变的理由是一开始就只做一件事的副作用。没有理由您至少不对代码中的单一职责做出任何努力,而又不知道将来它会如何变化。

这种事情的最好线索之一是当您选择类或函数名称时。如果尚不清楚应该为该类命名的名称,或者名称特别长/复杂,或者名称使用诸如“ manager”或“ utility”之类的通用术语,则可能是违反了SRP。同样,在编写API文档时,如果您基于所描述的功能违反了SRP,它应该会迅速显现出来。

当然,在项目的稍后阶段,您还无法了解SRP的细微差别-似乎一个单一的责任竟然是两个或三个。在这些情况下,您必须重构以实施SRP。但这并不意味着在您收到更改请求之前,不应忽略SRP。违反了SRP的目的!

为了直接说明您的示例,请考虑记录您的打印方法。如果你会说“这个方法格式化数据打印,并将其发送到打印机,”这是什么让你:这不是一个单一的责任,这两项责任:格式化和发送到打印机。如果您认识到这一点并将它们分为两个函数/类,那么当您提出更改请求时,每个部分都只有一个理由需要更改。


3

一个示例是一段打印文档的代码。提出了将其更改为打印为PDF的请求,然后再次请求将其更改为对文档应用某些不同的格式。在这一点上,您有证据证明有多个原因需要更改(并且违反了SRP),并且应该进行适当的重构。

我花了太多时间修改代码以适应这些更改,因此打了myself脚。而不是仅打印该死的愚蠢PDF。

重构以减少代码

一次性使用模式会造成代码膨胀。那里的软件包被小型特定类污染了,这些类创建了无用的垃圾代码堆。您必须打开数十个源文件才能了解它如何到达打印部分。最重要的是,可以执行数百行(甚至数千行)代码,仅执行10行代码即可进行实际打印。

创建靶心

一次性使用模式旨在减少源代码并提高代码重用性。它旨在创建专门化和特定的实现。某种bullseye源代码供您使用go to specific tasks。当打印出现问题时,您确切地知道要去哪里修复它。

一次性使用并不意味着模棱两可

是的,您有已经打印文档的代码。是的,您现在必须更改代码才能同时打印PDF。是的,您现在必须更改文档的格式。

您确定usage发生了很大变化吗?

如果重构导致源代码的各个部分过于笼统。到原来的意图printing stuff不再明确时,您就在源代码中创建了歧义性断裂。

新来的人能很快解决吗?

始终将源代码保持在最容易理解的组织中。

不要成为制表师

我见过很多次开发人员戴上目镜,并专注于细微的细节,以至于一旦它破裂,其他任何人都无法将它们重新组合在一起。

在此处输入图片说明


2

更改的原因最终是规范或有关应用程序运行环境的信息的更改。因此,一个责任原则是告诉您编写每个组件(类,函数,模块,服务...),以便它需要尽可能少地考虑规范和执行环境。

由于您在编写组件时了解规格和​​环境,因此可以应用该原理。

如果考虑打印文档的代码示例。您应该考虑是否可以在不考虑文档最终以PDF格式的情况下定义布局模板。您可以,因此SRP告诉您应该。

当然,YAGNI告诉您不应该。您需要在设计原则之间找到平衡。


2

绒毛朝着正确的方向前进。“单一责任原则”最初适用于程序。例如,丹尼斯·里奇(Dennis Ritchie)会说一个函数应该做一件事并且做得很好。然后,在C ++中,Bjarne Stroustrup会说一个类应该做一件事情并且做得很好。

注意,除了经验法则,这两者在形式上彼此之间几乎没有关系。它们仅满足于用编程语言方便表达的内容。好吧,那是什么。但这与flup所要驱动的完全不同。

现代的(即,敏捷和DDD)实现更多地关注对业务重要的事物,而不是编程语言可以表达的内容。令人惊讶的是,编程语言尚未赶上。类似于FORTRAN的旧语言承担了适合当时主要概念模型的职责:每个卡片通过读卡器时所应用的过程,或者(与C中一样)伴随每个中断的处理。然后是ADT语言,已经发展成熟,可以捕捉到DDD人们后来重新发明的重要性(尽管Jim Neighbors在1968年就已经弄清了其中的大部分内容,并将其付诸实践):今天我们称之为类。(它们不是模块。)

这一步比钟摆摆动少了演变。随着摆锤转向数据,我们失去了FORTRAN固有的用例建模。当您的主要焦点涉及屏幕上的数据或形状时,这很好。对于PowerPoint之类的程序,或者至少由于其简单的操作,它是一个很好的模型。

丢失的是系统责任。我们不出售DDD的元素。而且我们不好用类方法。我们出售系统责任。在某种程度上,您需要围绕单一责任原则设计系统。

因此,如果您看过像Rebecca Wirfs-Brock或我这样的人,他们曾经谈论过类方法,那么我们现在在谈论用例。那就是我们卖的东西。这些是系统操作。用例应负单一责任。用例很少是架构单元。但是每个人都试图假装是。例如,见证SOA人员。

这就是为什么我对Trygve Reenskaug的DCI体系结构感到兴奋的原因,这在上面的精益体系结构书中已有描述。最终,它对以前对“单一责任”的武断和神秘的服从有了某种真实的地位-正如上面大多数论点所发现的那样。这种身材与人类的思维模式有关:最终用户是第一位,程序员是第二位。它涉及业务问题。而且,几乎是偶然的,它随着变化对我们的挑战而封装了变化。

我们所知的单一责任原则要么是起源以来遗留下来的恐龙,要么是我们用来替代理解的业余爱好。您需要抛弃其中的一些爱好来制作出色的软件。这需要开箱即用。仅当问题简单明了时,使事情简单明了才有效。我对这些解决方案并不十分感兴趣:它们不是典型的,也不是挑战所在。


2
在阅读您所写的内容时,我一直完全看不到您在说什么。好的答案不会把这个问题当作在树林中漫步的起点,而是将所有写作联系在一起的明确主题。
Donal Fellows

1
嗯,您就是其中之一,就像我的一位老经理一样。“我们不想了解它:我们想改善它!” 此处的关键主题问题是原则之一:即“ SRP”中的“ P”。如果是正确的问题,也许我会直接回答:不是。您可以与提出这个问题的人一起讨论。
应付

一个好的答案埋在这里的某个地方。我认为...
RubberDuck

0

是的,应将“单一责任原则”应用于新代码。

但!什么是责任?

“打印报告是责任”吗?我相信答案是“也许”。

让我们尝试使用SRP的定义为“只有一个改变的理由”。

假设您有一个打印报告的功能。如果有两个更改:

  1. 更改该功能,因为您的报告需要具有黑色背景
  2. 更改该功能,因为您需要打印为pdf

然后,第一个更改是“更改报告样式”,另一个是“更改报告输出格式”,现在您应该将它们置于两个不同的函数中,因为它们是不同的东西。

但是,如果您的第二个更改是:

2b。更改该功能,因为您的报告需要其他字体

我想说这两个更改都是“更改报表样式”,它们可以保留一个功能。

那那把我们留在哪里呢?和往常一样,您应该尝试使事情保持简单易懂。如果更改背景色表示20行代码,而更改字体表示20行代码,请再次使其具有两个功能。如果每一行都是一行,则保持一行。


0

在设计新系统时,明智的做法是考虑您在其整个生命周期内可能需要进行的更改,以及将要采用的体系结构将给这些更改带来多大的代价。将系统拆分为模块是出错的昂贵决定。

企业领域专家负责人的思维模式是一个很好的信息来源。以文档,格式和pdf为例。领域专家可能会告诉您,他们使用文档模板来格式化其字母。在平稳或在Word中或其他任何形式。您可以在开始编码之前检索此信息并将其用于设计中。

关于这些方面的精彩读物:Coplien的精益建筑


0

“打印”非常类似于MVC中的“查看”。任何了解对象基础知识的人都会理解这一点。

这是系统责任。它作为一种机制MVC来实现,它涉及打印机(视图),正在打印的事物(模块)以及打印机请求和选项(来自控制器)。

尝试将其本地化为类或模块的职责是愚蠢的,并且反映了30年前的思想。从那时起,我们学到了很多东西,这在文献和成熟的程序员的代码中得到了充分的证明。


0

仅在开始请求更改代码时才真正开始应用SRP,这不是一个更好的主意吗?

理想情况下,您已经对代码各部分的职责有所了解。根据您的第一本能将责任划分为责任,可能考虑到您要使用的库要做什么(将任务,职责委派给库通常是一件很重要的事情,只要库可以实际完成任务即可)。然后,根据不断变化的需求来加深对责任的理解。最初对系统的了解越充分,从根本上改变职责分配的需求就越少(尽管有时您发现最好将职责划分为子职责)。

并不是说您应该花很多时间担心它。代码的一个关键功能是可以在以后进行更改,而不必在第一次就完全正确。只要设法随着时间的推移变得更好,就可以了解什么样的造型责任,以便将来减少犯错误。

一个示例是一段打印文档的代码。提出了将其更改为打印为PDF的请求,然后再次请求将其更改为对文档应用某些不同的格式。在这一点上,您有证据证明有多个原因需要更改(并且违反了SRP),并且应该进行适当的重构。

这完全表明总体责任(“打印”代码)具有子责任,应将其分为几部分。这本身并不违反SRP 而是表明可能需要分区(可能分为“格式化”和“渲染”子任务)。您是否可以清楚地描述这些职责,以便在不查看子任务执行情况的情况下,了解子任务的状况?如果可以,它们可能是合理的分割。

如果我们看一个简单的真实示例,它可能也会更清楚。让我们考虑中的sort()效用方法java.util.Arrays。它有什么作用?它对数组进行排序,仅此而已。它没有打印出元素,没有找到最合乎道德的成员,也没有向Dixie吹口哨。它只是对数组排序。您也不必知道如何。排序是该方法的唯一责任。(实际上,出于某种丑陋的技术原因,Java中有许多排序方法与原始类型有关;不过,由于它们都有同等的责任,因此您不必对此进行任何注意。)

使您的方法,您的类,您的模块,使其在生活中具有如此明确的角色。它使您必须立即了解的内容减少了,而这反过来又使您可以处理大型系统的设计和维护。

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.