使用单一责任原则时,什么构成“责任”?


198

显然,“单一责任原则”并不意味着“只做一件事情”。那就是方法的目的。

public Interface CustomerCRUD
{
    public void Create(Customer customer);
    public Customer Read(int CustomerID);
    public void Update(Customer customer);
    public void Delete(int CustomerID);
}

鲍勃·马丁说:“班级应该只有一个改变的理由。” 但是,如果您是SOLID的新手,那么很难下定决心。

我写了另一个问题的答案,在那儿我建议责任就像职位,然后用饭店的比喻来阐明我的观点。但这仍未阐明某人可以用来定义其班级职责的一组原则。

你是怎么做到的?您如何确定每个班级应承担的责任,以及如何在SRP中定义责任?


28
发布于代码审查和被撕开:-D
约尔格W¯¯米塔格

8
@JörgWMittag嘿,现在,不要吓people人:)
Flambino

117
资深会员提出的这样的问题表明,我们试图坚持的规则和原则绝非简单简单的。他们[排序]自相矛盾和神秘 ......任何好的一套规则应该是。而且,我想相信像这样的问题对智者谦虚,并给那些感到绝望无知的人带来希望。谢谢,罗伯特!
svidgen

41
我想知道如果这个问题是由菜鸟发布的,那么这个问题是否会立即被否决+标记为重复:)
Andrejs

9
@rmunn:换句话说-大代表吸引了更多的代表,因为没有人取消关于stackexchange的基本偏见
Andrejs

Answers:


117

解决这个问题的一种方法是想象未来项目中潜在的需求变更,并问问自己需要做什么才能使它们实现。

例如:

新业务要求:位于加利福尼亚的用户将获得特别折扣。

“好”更改的示例:我需要在计算折扣的类中修改代码。

不良更改的示例:我需要修改User类中的代码,并且该更改将对使用User类的其他类(包括与折扣无关的类,例如注册,枚举和管理)产生级联效果。

要么:

新的非功能性要求:我们将开始使用Oracle而不是SQL Server

良好更改的示例:只需在数据访问层中修改一个类即可确定如何将数据持久保存在DTO中。

坏更改:我需要修改所有业务层类,因为它们包含特定于SQL Server的逻辑。

这样做的目的是最大程度地减少将来可能发生的更改的占用空间,将代码修改限制在每个更改区域仅一个代码区域。

至少,您的课程应该将逻辑关注点与物理关注点分开。一个伟大的一套例子可以在发现System.IO命名空间:有,我们可以找到一个各种物理流(例如FileStreamMemoryStreamNetworkStream)以及各种读者和作家(BinaryWriterTextWriter)上的逻辑电平工作。通过分离他们这样,我们才能避免组合学爆炸:不是需要FileStreamTextWriterFileStreamBinaryWriterNetworkStreamTextWriterNetworkStreamBinaryWriterMemoryStreamTextWriter,和MemoryStreamBinaryWriter,你只挂钩作家和流,你可以有你想要的东西。然后,稍后我们可以添加一个XmlWriter,而无需分别为内存,文件和网络重新实现它。


34
尽管我同意未来的想法,但存在YAGNI之类的原则,而TDD thar之类的方法则提出了相反的建议。
罗伯特·哈维

87
YAGNI告诉我们不要制造我们今天不需要的东西。它没有告诉我们不要以可扩展的方式构建东西。另请参见“ 打开/关闭”原则,该原则指出“软件实体(类,模块,功能等)应为扩展而开放,但为修改而应封闭”。
约翰·吴

18
@JohnW:仅对您的YAGNI评论+1。我不敢相信我必须向人们解释,YAGNI 并不是建立无法应对变化的僵化,僵化的系统的借口-具有讽刺意味的是,这与SRP和开放/封闭原则的目的正好相反。
格雷格·伯格哈特

36
@JohnWu:我不同意,YAGNI告诉我们完全不要制造我们今天不需要的东西。例如,可读性和测试是程序始终“需要”的东西,因此YAGNI从来都不是不添加结构和注入点的借口。但是,一旦“可扩展性”增加了可观的成本,而“今天”所带来的好处并不明显,则YAGNI意味着避免这种可扩展性,因为后者会导致工程过度。
布朗

9
@JohnWu我们确实从SQL 2008切换到了2012。总共有两个查询需要更改。以及从SQL Auth到受信任的?为什么还要更改代码?更改配置文件中的connectionString就足够了。再次,YAGNI。YAGNI和SRP有时会引起竞争,因此您需要判断哪个具有更好的成本/收益。
安迪

76

实际上,责任受那些可能会改变的事物的束缚。因此,不幸的是,没有科学或公式化的方法可以得出构成责任的内容。这是一个判断电话。

根据您的经验,这可能会改变。

我们倾向于以双曲线的,直译的,热心的愤怒来应用该原理的语言。我们倾向于对类进行拆分,因为它们可能会发生变化,或者按照简单的方式帮助我们分解问题。(后一个原因并非天生就不好。)但是,SRP并不是出于自身的原因而存在;用于创建可维护的软件。

同样,如果划分不是由可能的变化所驱动,那么如果YAGNI更适用,它们就不会真正为SRP 1服务。两者都有相同的最终目标。而这两个是判断的问题-希望经验丰富的判断。

当鲍伯叔叔写这本书时,他建议我们将“责任”视为“谁要求改变”。换句话说,我们不希望甲方失去工作,因为乙方要求改变。

在编写软件模块时,您要确保在请求更改时,这些更改只能来自一个人,或者是代表一个狭义定义的业务职能的紧密联系的一群人。您想将模块与整个组织的复杂性隔离开来,并设计系统,使每个模块仅负责(响应)该一个业务功能的需求。(鲍勃叔叔-单一责任原则

优秀且经验丰富的开发人员将对可能发生的变化有所了解。而且该心理清单会因行业和组织而有所不同。

在您的特定应用程序中,在您的特定组织中构成责任的,最终取决于经验丰富的判断。关于可能发生的变化。从某种意义上讲,它与谁拥有模块的内部逻辑有关。


1.要明确一点,这并不意味着它们是糟糕的部门。它们可能是可以极大地提高代码可读性的部门。这仅表示它们不受SRP驱动。


11
最佳答案,实际上引用了Bob叔叔的想法。至于可能发生的变化,每个人都在I / O上做了大量工作,“如果我们更改数据库该怎么办?” 或“如果从XML转换为JSON会怎样?” 我认为这通常是错误的。真正的问题应该是“如果我们需要将此int更改为float,添加一个字段,并将此String更改为Strings,该怎么办?”
user949300 '17

2
这是作弊。单一责任本身只是“变更隔离”的一种提议方式。说明,您需要隔离更改以使职责“单一”,这并不建议如何执行此操作,仅说明要求的由来。
巴思列夫斯

6
@Basilevs我正在努力磨练您在此答案中看到的不足之处,更不用说Bob叔叔的答案了!但是,也许我需要澄清一下,SRP 并不是要确保“更改”仅影响1类。关于确保每个班级仅对“一个更改”做出响应。...这是关于尝试从每个班级向单个所有者绘制箭头。不是从每个所有者到单个班级。
svidgen

2
感谢您提供务实的回应!甚至Bob叔叔也警告不要坚决遵守敏捷建筑中的 SOLID原则。我没有引号,但他基本上说,分担职责会固有地增加代码中的抽象级别,并且所有抽象都是以成本为代价的,因此请确保遵循SRP(或其他原则)的好处大于成本添加更多抽象。(续下一条评论)
Michael L.

4
这就是为什么我们应该尽早并尽可能合理地将产品展示在客户面前,这样他们将迫使我们的设计发生变化,并且我们可以看到该产品可能在哪些领域发生变化。此外,他警告说,我们不能保护自己免受各种变化的影响。对于任何应用程序,将很难进行某些类型的更改。我们需要确保这些是最不可能发生的更改。
Michael L.

29

我遵循“班级应该只有一个改变的理由”。

对我来说,这意味着考虑我的产品所有者可能想出的繁琐方案(“我们需要支持移动设备!”,“我们需要上云服务!”,“我们需要支持中文!”)。好的设计将把这些方案的影响限制在较小的范围内,并使它们相对容易实现。错误的设计意味着需要编写大量代码并进行大量风险更改。

我发现只有经验才能正确评估这些疯狂方案的可能性-因为简化一个方案可能会使另外两个方案变得更难-并评估设计的优劣。经验丰富的程序员可以想象他们需要做些什么来更改代码,躺在床上如何咬它们,以及用什么技巧使事情变得容易。经验丰富的程序员对产品所有者索要疯狂物品时的感觉很不满意。

实际上,我发现单元测试在这里有所帮助。如果您的代码不灵活,将很难进行测试。如果您无法注入模拟或其他测试数据,则可能无法注入该SupportChinese代码。

另一个粗略的度量标准是电梯间距。传统的电梯推销方式是“如果您与投资者一起乘坐电梯,您能以一个想法出售他吗?”。初创企业需要对自己的工作-重点是什么做一个简短的简短描述。同样,类(和功能)应该有他们一下简单的介绍。不是“该类实现了一些附加功能,因此您可以在这些特定情况下使用它”。您可以告诉其他开发人员:“此类创建用户”。如果您无法与其他开发人员进行交流,那么您遇到错误。


有时,您去实现您认为是一团糟的更改,结果变得很简单,或者小的重构使之变得简单,并同时添加了有用的功能。但是,是的,通常您会发现麻烦来了。

16
我是“电梯沥青”理念的大力倡导者。如果很难用一两句话来解释一个班级的表现,那您就处于危险境地。
伊凡(Ivan)

1
您谈到了一个重要的观点:那些疯狂计划的可能性从一个项目所有者到下一个项目所有者都大不相同。您不仅要依靠一般经验,还要依靠您对项目所有者的了解程度。我曾为那些希望将我们的冲刺减少到一周的人工作,但仍然无法避免在冲刺中改变方向。
凯文·克鲁姆维德

1
除了明显的好处外,使用“电梯间距”记录代码还可以帮助您使用自然语言来思考您的代码在做什么,我发现这种语言有助于发现多种职责。
亚历山大

1
@KevinKrumwiede这就是“鸡被砍断头部跑来跑去”和“野鹅追逐”方法学的目的!

26

没人知道。或者至少,我们无法就一个定义达成共识。这就是使SPR(和其他SOLID原则)引起很大争议的原因。

我认为能够弄清是什么责任还是什么不是责任是软件开发人员在其职业生涯中必须学习的技能之一。您编写和查看的代码越多,确定某项是单个或多个职责的经验就越丰富。或者,如果单一责任分散在代码的不同部分中。

我认为SRP的主要目的不是硬性规定。这是在提醒我们注意代码中的内聚性,并始终在确定哪些代码具有内聚力和什么不是内聚力时付出一些自觉的努力。


20
新程序员确实倾向于将SOLID视为一套法律,而事实并非如此。这只是一组好的想法,可以帮助人们在课堂设计上变得更好。people,人们往往过于重视这些原则。我最近看到一份职位公告,其中提到SOLID是职位要求之一。
罗伯特·哈维

9
最后一段+42。正如@RobertHarvey所说,诸如SPR,SOLID和YAGNI之类的东西不应被视为“ 绝对规则 ”,而应被视为“良好建议”的一般原则。他们之间(和其他人)的建议有时会矛盾,但是平衡建议(而不是遵循严格的规则)将(随着时间的流逝,随着经验的增长)将指导您开发更好的软件。在软件开发中应该只有一个“绝对规则”:“ 没有绝对规则 ”。
TripeHound17年

这是对SRP的一个很好的说明。但是,即使SOLID原则不是硬性规定,如果没有人理解它们的含义,它们也就没有多大的价值-甚至在您声称“没人知道”的说法是真的的情况下,价值也没有!...让他们很难理解是有道理的。如同任何技巧,还有一些是从区分好好!但是……“没人知道”使它更像是一种令人讨厌的仪式。(而且我不认为这是SOLID的意图!)
svidgen

3
通过“没人知道”,我希望@Euphoric只是意味着没有精确的定义适用于每个用例。这需要一定的判断力。我认为确定职责所在的最佳方法之一是快速迭代并让您的代码库告诉您。寻找“气味”,指出您的代码不容易维护。例如,当对单个业务规则的更改开始对看似不相关的类产生级联影响时,您可能违反了SRP。
Michael L.

1
我衷心地赞同@TripeHound和其他指出第二个“规则”的人,并不存在定义开发的“真正的宗教”,而是增加了开发可维护软件的可能性。要十分小心遵循“最佳实践”的,如果你不能解释它是如何促进维护的软件,提高了质量或提高开发效率..
迈克尔L.

5

我认为“责任”一词可作为隐喻使用,因为它使我们能够使用软件来调查软件的组织程度。我特别关注两个原则:

  • 责任与权力相称。
  • 没有两个实体应对同一件事负责。

这两个原则使我们有意义地减轻了责任,因为它们相互抵消。如果您要授权一段代码为您做某事,那么它需要对所做的事情负责。这导致班级可能必须增长的责任,将其“改变的一个理由”扩展到越来越广泛的范围。但是,当您扩大范围时,您自然会开始遇到多个实体负责同一件事的情况。这充满了现实生活中的责任问题,因此,毫无疑问,这也是编码中的问题。结果,当您将责任细分为无重复的宗地时,此原则会导致范围缩小。

除了这两个之外,第三个原则似乎是合理的:

  • 责任可以下放

考虑一个新鲜出炉的程序...一块空白的板岩。最初,您只有一个实体,这是程序的整体。它负责一切。自然,在某个时候,您将开始将职责委派给函数或类。至此,前两个规则开始起作用,迫使您平衡这种责任。高层计划仍然负责总体输出,就像经理负责其团队的生产力一样,但是每个子实体都被委以责任,并具有执行该责任的权限。

另外,这使SOLID与可能需要执行的任何公司软件开发特别兼容。地球上的每个公司都有一些关于如何下放责任的概念,他们并不完全同意。如果您以类似于公司自己的授权的方式在软件中委派职责,那么将来的开发人员就可以更轻松地了解您在该公司的工作方式,这将变得容易得多。


我不是100%确信这完全可以解释。但是,我认为对“权威”进行解释“责任”是一种有见地的表达方式!(+1)
svidgen

皮尔西格说:“您倾向于将问题植入机器中”,这让我停下来。

@nocomprende您也倾向于在机器中发挥自己的优势。我认为,当你的优点和缺点相同时,就会变得很有趣。
Cort Ammon

5

在耶鲁大学的这次会议上,鲍伯叔叔举了一个有趣的例子:

在此处输入图片说明

他说,这Employee有三个改变的原因,三个改变需求的来源,并且给出了这种幽默和嘲讽的态度,尽管如此,但仍是说明性的解释:

  • 如果该CalcPay()方法有误,并导致公司损失数百万美元,则CFO将解雇您

  • 如果该ReportHours()方法有误,并导致公司损失数百万美元,则首席运营官将解雇您

  • 如果WriteEmmployee()方法发生错误,导致擦除大量数据并给公司造成数百万美元的损失,那么CTO将开除您

因此,如果有三个不同的C级执行人员可能会在同一个类中因昂贵的错误而解雇您,则意味着该类承担了太多的责任。

他提供的解决方案可以解决违反SRP的问题,但必须解决违反视频中未显示的DIP的问题。

在此处输入图片说明


该示例看起来更像是一个职责错误的类。
罗伯特·哈维

4
@RobertHarvey当一个班级的职责过多时,这意味着额外的职责是错误的职责。
图兰斯·科尔多瓦

5
我听到了你在说什么,但我认为它没有说服力。一个班级承担太多责任,而一个班级做一些根本没有生意的事情,这是有区别的。听起来可能一样,但事实并非如此。数花生不等于称核桃。这是鲍伯叔叔的原则和鲍伯叔叔的例子,但是如果它具有足够的描述性,我们根本不需要这个问题。
罗伯特·哈维

@RobertHarvey,有什么区别?在我看来,那些情况是同构的。
Paul Draper

3

我认为,比“改变原因”更好地细分事物的方法是,首先考虑是否需要执行两个(或多个)动作的代码应该持有一个单独的对象引用是否有意义。每个动作,以及拥有可以执行一个动作但不能执行另一个动作的公共对象是否有用。

如果两个问题的答案都是肯定的,则表明应该由单独的班级来完成这些操作。如果对这两个问题的回答均否,则表明从公众的角度来看应该有一个阶级。如果该代码繁琐,则可以在内部细分为私有类。如果第一个问题的答案为“否”,而第二个问题的答案为“是”,则每个操作应有一个单独的类,再加上一个包含对其他实例的引用的复合类。

如果一个人对收银机的键盘,蜂鸣器,数字读出器,收据打印机和现金抽屉有单独的类,而对于完整的收银机没有复合类,那么原本应该用来处理交易的代码可能最终会意外地被调用从一台机器的键盘输入信息,从第二台机器的蜂鸣器产生噪音,在第三台机器的显示屏上显示数字,在第四台机器的打印机上打印收据,并弹出第五台机器的现金抽屉的方式。这些子函数中的每个子函数可能有用地由一个单独的类处理,但也应该有一个将它们连接在一起的复合类。复合类应将尽可能多的逻辑委托给组成类,

可以说每个类的“职责”要么是合并一些实际的逻辑,要么是为其他多个类提供一个公共的附加点,但是重要的是首先重点关注客户端代码应如何查看一个类。如果让客户端代码将某些内容视为单个对象有意义,那么客户端代码应将其视为单个对象。


这是合理的建议。值得指出的是,您不仅根据SRP,还根据更多标准来划分职责。
约根·

1
打车类比:我不需要知道别人的油箱里有多少汽油,也不需要打开别人的雨刷器。(但这就是互联网的定义)(嘘!您会毁了这个故事)

1
@nocomprende-“我不需要知道别人的油箱中有多少汽油”,-除非您是青少年,否则您想决定下一次旅行要“借用”家庭的哪些汽车...;)
alephzero

3

SRP很难正确解决。这主要是为代码分配“工作”,并确保每个部分都有明确的职责。就像在现实生活中一样,在某些情况下,将工作分配给人们很自然,但在另一些情况下,这可能确实很棘手,特别是如果您不认识他们(或工作)时。

我总是建议您先编写简单的代码,然后再进行一些重构:一段时间后,您将趋向于看代码是如何自然聚集的。我认为在了解代码(或人员)和要完成的工作之前强加责任是错误的。

您会注意到的一件事是,当模块开始执行过多操作且难以调试/维护时。这是重构的时刻。核心工作应该是什么?可以将什么任务交给另一个模块?例如,它应该处理安全检查和其他工作,还是应该首先在其他地方进行安全检查,否则会使代码更复杂吗?

使用太多的间接,将再次变得混乱……就其他原则而言,这一原则将与其他原则(例如KISS,YAGNI等)发生冲突。一切都是平衡问题。


SRP不仅仅是君士坦丁的凝聚力书吗?
尼克·基利

如果您编码足够长的时间,您自然会发现这些模式,但是您可以通过命名它们来加快学习速度,并有助于进行交流...
Christophe Roussy

@NickKeighley我认为这是凝聚力,不是那么大的命令,而是从另一个角度来看的。
sdenham

3

“单一责任原则”也许是一个令人困惑的名称。“只有一个改变的理由”是对原则的更好描述,但仍然容易被误解。我们不是在说什么导致对象在运行时更改状态。我们正在考虑可能导致开发人员将来更改代码的原因。

除非我们要修复错误,否则更改将归因于新的或更改的业务需求。您将不得不在代码本身之外进行思考,并想象哪些外部因素可能导致需求独立变化。说:

  • 税率因政治决定而改变。
  • 市场营销决定更改所有产品的名称
  • 用户界面必须重新设计才能访问
  • 数据库很拥挤,因此您需要进行一些优化
  • 您必须容纳一个移动应用
  • 等等...

理想情况下,您希望独立的因素影响不同的类别。例如,由于税率的变化与产品名称无关,因此变化不应影响相同的类别。否则,您将面临引入税收变更的风险,即产品命名中的错误,这是模块化系统要避免的紧密耦合。

因此,不要仅仅关注可能发生的变化- 可以想象,将来任何事情都会发生变化。专注于可能独立改变的地方。如果更改是由不同的参与者造成的,则更改通常是独立的。

您的职位标题示例是正确的,但您应该更实际地理解它!如果营销可能导致代码更改而财务可能导致其他更改,则这些更改不应影响相同的代码,因为这些职位实际上是不同的职位,因此更改将独立发生。

引用发明该术语的Bob叔叔

在编写软件模块时,您要确保在请求更改时,这些更改只能来自一个人,或者是代表一个狭义定义的业务功能的紧密联系的一群人。您想将模块与整个组织的复杂性隔离开来,并设计系统,使每个模块仅负责(响应)该一个业务功能的需求。

综上所述:“责任”是针对单个业务功能的。如果一个以上的演员可能导致您不得不更换班级,那么该班级可能违反了这一原则。


根据他的《清洁建筑》一书,这是完全正确的。业务规则应该来自一个来源,并且只能来自一个来源。这意味着人力资源,运营和IT部门都需要在“单一职责”中合作制定要求。这就是原则。+1
Benny Skogberg

2

一篇好文章,解释了扎实的编程原理,并给出以下两个代码示例,而不是下面这个原则是https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented-设计

在与SRP有关的示例中,他给出了几个形状类别(圆形和正方形)的示例,以及一个旨在计算多个形状的总面积的类别。

在他的第一个示例中,他创建了面积计算类,并将其输出返回为HTML。后来,他决定要改为将其显示为JSON,并且必须更改其面积计算类。

此示例的问题在于,他的面积计算类负责计算形状的面积并显示该面积。然后,他使用另一个专门为显示区域而设计的类,通过一种更好的方法来完成此任务。

这是一个简单的示例(由于具有代码片段,因此阅读本文更容易理解),但演示了SRP的核心思想。


0

首先,实际上您有两个独立的问题:在类中放入什么方法的问题以及接口膨胀的问题。

介面

您具有以下界面:

public Interface CustomerCRUD
{
  public void Create(Customer customer);
  public Customer Read(int CustomerID);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

大概,您有​​多个符合该CustomerCRUD接口的类(否则就不需要一个接口),而某些函数do_crud(customer: CustomerCRUD)接受一个符合对象。但是您已经打破了SRP:将这四个不同的操作捆绑在一起。

假设稍后您将对数据库视图进行操作。数据库视图具有Read可用的方法。但是您想编写一个函数do_query_stuff(customer: ???),该函数可以在成熟的表或视图上透明地进行操作;Read毕竟,它仅使用该方法。

所以创建一个界面

public Interface CustomerReader {public Customer Read(customerID:int)}

并将您的CustomerCrud界面作为:

public interface CustomerCRUD extends CustomerReader
{
  public void Create(Customer customer);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

但是,没有尽头。可能有一些我们可以创建但无法更新的对象,等等。这个兔子洞太深了。遵守单一职责原则的唯一明智的方法是使您的所有接口完全包含一种方法。Go实际上是按照我所看到的方法遵循的,大多数接口都包含一个函数。如果要指定一个包含两个函数的接口,则必须笨拙地创建一个将两个函数结合在一起的新接口。您很快会看到接口的组合爆炸。

摆脱这种混乱的方法是使用结构子类型化(例如在OCaml中实现),而不是使用接口(这是名义子类型的一种形式)。我们没有定义接口。相反,我们可以简单地编写一个函数

let do_customer_stuff customer = customer.read ... customer.update ...

调用我们喜欢的任何方法。OCaml将使用类型推断来确定我们可以传入实现这些方法的任何对象。在此示例中,将确定customer具有类型<read: int -> unit, update: int -> unit, ...>

班级

这解决了接口混乱的问题。但是我们仍然必须实现包含多个方法的类。例如,我们应该创建两个不同的类,CustomerReaderCustomerWriter?如果我们想更改表的读取方式(例如,现在我们在获取数据之前将响应缓存在redis中),但是现在如何写入它们呢?如果遵循这一推理链得出其逻辑结论,那么您将不可避免地导致函数式编程:)


4
“无意义”有点强。我可以落后于“神秘主义者”或“禅宗”。但是,并非毫无意义!
svidgen

您能否再解释一下为什么结构子类型化是一种解决方案?
罗伯特·哈维

@RobertHarvey大大重组了我的答案
gardenhead'Mar 28'28

4
即使只有一个类来实现它,我也使用接口。为什么?模拟单元测试。
Eternal21 '17

0

在我看来,最接近SRP的是使用流程。如果您没有任何给定类的明确使用流程,则可能是您的类具有设计异味。

使用流将是给定的方法调用继承,它将给您带来预期的(因此可测试的)结果。您基本上是使用IMHO的用例定义一个类,这就是为什么所有程序方法都将重点放在接口而不是实现上。


0

是实现多个需求的改变,而不需要您的组件改变

但是,当您第一次了解SOLID时,乍一看就很幸运了。


我看到很多评论说SRPYAGNI可能彼此矛盾,但是TDD (伦敦学校,GOOS)执行的YAGN教会了我从客户的角度考虑和设计组件的想法。我开始通过客户端希望它做的最少事情来设计我的界面,这就是应该做的事。而且该练习可以在没有TDD知识的情况下完成。

我喜欢鲍伯叔叔描述的技术(可悲的是,我不记得从哪里来的),这种技术类似于:

问问自己,这堂课做什么?

您的答案是否包含AndOr

如果是这样,请提取答案的那一部分,这是它自己的责任

这项技术是绝对的,正如@svidgen所说,SRP是一个判断电话,但是当学习新东西时,绝对是最好的,仅总是做某事比较容易。确保不分开的原因是:有根据的估算,而不是因为您不知道如何做。这是一门艺术,需要经验。


我认为,在谈到SRP时,很多答案似乎都在为脱钩论辩。

SRP不能确保的改变不会向下传播的依赖关系图。

从理论上讲,没有SRP,您将没有任何依赖...

一项更改不应导致应用程序中很多地方发生更改,但是我们为此获得了其他原则。但是,SRP确实改善了开放式封闭原则。该原理更多地是关于抽象的,但是,较小的抽象更易于实现

因此,从整体上讲授SOLID时,请谨慎地讲授SRP,当需求发生变化时,您可以更改较少的代码,而实际上,您可以编写较少的代码。


3
When learning something new, absolutes are the best, it is easier to just always do something.-以我的经验,新程序员太教条了。专制主义导致了没有思想的开发人员和不拘一格的编程。只要您了解要与之交谈的人以后必须不学习您教给他们的内容,那么说“做到这一点”就可以了。
罗伯特·哈维

@RobertHarvey,完全正确,它会产生教条式的行为,您在获得经验时必须取消学习/重新学习。这是我的观点。如果一个新的程序员试图进行判断调用而没有任何理由推理他们的决定,那么边界似乎是无用的,因为他们不知道它为什么起作用,何时起作用。通过让人们过度使用它,它教会他们寻找例外,而不是做出无条件的猜测。您所说的关于专制主义的一切都是正确的,这就是为什么它仅应作为起点的原因。
克里斯·沃勒特

@RobertHarvey,一个快速的现实生活示例:您可能会教给孩子永远诚实,但是随着他们长大,他们可能会意识到一些人们不想听他们最诚实的想法的例外情况。期望5岁的孩子对诚实做正确的判断,充其量是乐观的。:)
克里斯·沃勒特

0

没有明确的答案。尽管问题很狭窄,但解释不是。

对我来说,如果您愿意,它就像是Occam的Razor。这是我尝试评估当前代码的理想选择。很难用简单明了的话将其确定下来。另一个比喻是“一个主题”,它与“单一责任”一样抽象,即难以理解。第三个描述是“处理一个抽象级别”。

这实际上意味着什么?

最近,我使用一种编码样式,该编码样式主要包括两个阶段:

第一阶段最好形容为创意混乱。在这一阶段,我记下了思想的流逝,即原始又丑陋的代码。

第二阶段则完全相反。就像飓风过后清理一样。这需要最多的工作和纪律。然后,我从设计师的角度来看代码。

我现在主要在Python中工作,这使我以后可以想到对象和类。第一阶段 -我只编写函数,并且几乎随机地将它们分散在不同的模块中。在第二阶段中,当我掌握了一切之后,我将仔细研究哪个模块处理解决方案的哪一部分。在浏览这些模块时,主题浮出水面。一些功能在主题上相关。这些都是上课的好人选。在将函数转换为类之后-几乎完成了缩进并将其添加self到python中的参数列表中;)-我使用SRP像Occam的Razor一样将功能剥离给其他模块和类。

当前的示例可能是前几天编写小型导出功能

压缩中需要csvexcel和组合的excel表

普通功能分别在三个视图(=功能)中完成。每个函数都使用一种确定过滤器的通用方法和另一种检索数据的方法。然后,在每个功能中,导出的准备工作都已完成,并作为服务器的响应交付。

混合了太多抽象层次:

I)处理传入/传出的请求/响应

II)确定过滤器

III)检索数据

IV)数据转换

简单的步骤是在第一步中使用一个抽象(exporter)处理II-IV层。

唯一剩下的是处理请求/响应的主题。在相同的抽象级别上,可以提取请求参数。所以我有这个观点一个“责任”。

其次,我不得不分解出口商,正如我们所见,出口商至少包括另外三个抽象层。

确定过滤器标准和实际检索几乎处于相同的抽象级别(需要过滤器才能获得数据的正确子集)。这些级别被放入数据访问层之类的东西中。

在下一步中,我分解了实际的导出机制:在需要写入临时文件的地方,我将其分为两个“职责”:一个是将数据实际写入磁盘,另一个是处理实际格式。

随着类和模块的形成,事情变得更清楚了,什么属于哪里。始终是一个潜在的问题,即课堂是否做得太多

您如何确定每个班级应承担的责任,以及如何在SRP中定义责任?

很难给出一个食谱。当然,如果可以帮助的话,我可以重复神秘的“一个抽象层次”的规则。

对我来说,主要是一种“艺术直觉”导致了当前的设计。我对代码进行建模,就像艺术家可以雕刻粘土或绘画一样。

想象我是一个编码鲍勃·罗斯 ;)


0

我试图写遵循SRP的代码的操作:

  • 选择您需要解决的特定问题;
  • 编写解决问题的代码,用一种方法(例如:main)编写所有内容;
  • 仔细分析代码,并根据业务尝试定义在所有正在执行的操作中可见的职责(这是主观部分,也取决于业务/项目/客户);
  • 请注意,所有功能均已实现;接下来是仅代码的组织(此方法从现在开始将不再实现任何附加功能或机制);
  • 根据您在先前步骤中定义的职责(基于业务和“改变的一个原因”定义),为每个步骤提取一个单独的类或方法;
  • 请注意,这种方法只在乎SPR。理想情况下,这里还应该尝试其他步骤以遵循其他原则。

例:

问题:从用户那里获得两个数字,计算它们的总和并将结果输出给用户:

//first step: solve the problem right away
static void Main(string[] args)
{
    Console.WriteLine("Number 1: ");
    int firstNumber = Convert.ToInt32(Console.ReadLine());

    Console.WriteLine("Number 2: ");
    int secondNumber = Convert.ToInt32(Console.ReadLine());

    int result = firstNumber + secondNumber;

    Console.WriteLine("Hi there! The result is: {0}", result);

    Console.ReadLine();
}

接下来,尝试根据需要执行的任务定义职责。从中提取适当的类:

//Responsible for getting two integers from the user
class Input {
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public void Read() {
        Console.WriteLine("Number 1: ");
        FirstNumber = Convert.ToInt32(Console.ReadLine());

        Console.WriteLine("Number 2: ");
        SecondNumber = Convert.ToInt32(Console.ReadLine());
    }
}

//Responsible for calculating the sum of two integers
class SumOperation {
    public int Result { get; set; }
    public void Calculate(int a, int b) {
        Result = a + b;
    }
}

//Responsible for the output of some value to the user
class Output {
    public void Write(int result) {
        Console.WriteLine("Hello! The result is: {0}", result);
    }
}

然后,重构的程序变为:

//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
    var input = new Input();
    input.Read();

    var operation = new SumOperation();
    operation.Calculate(input.FirstNumber, input.SecondNumber);

    var output = new Output();
    output.Write(operation.Result);

    Console.ReadLine();
}

注意:这个非常简单的示例仅考虑了SRP原理。其他原则的使用(例如:“ L”-代码应依赖于抽象而不是依赖)将为代码提供更多好处,并使其对于业务更改更具可维护性。


1
您的示例过于简单,不足以充分说明SRP。在现实生活中没有人会这样做。
罗伯特·哈维

是的,在实际项目中,我编写一些伪代码,而不是像示例中那样编写确切的代码。在伪代码之后,我尝试像在示例中一样拆分职责。无论如何,这就是我要做的。
艾默生·卡多佐

0

罗伯特·马丁斯(Robert C.Martins)于2017年9月10日出版了《清洁架构:软件结构和设计的工匠指南》一书,罗伯特在第62页上写道:

历史上,SRP的描述方式如下:

一个模块应该只有一个理由才能更改

更改软件系统以满足用户和利益相关者的需求;这些用户和利益相关者 “改变的理由”。该原则正在谈论。确实,我们可以改写这样的原则:

一个模块应仅对一个用户或利益相关者负责

不幸的是,“用户”和“利益相关者”一词在这里并不是真正合适的词。想要系统以合理的方式更改的用户或利益相关者可能不止一个。相反,我们实际上是指一个小组-一个或多个需要此更改的人。我们将这个小组称为演员

因此,SRP的最终版本是:

一个模块应仅对一个参与者负责。

因此,这与代码无关。SRP是关于控制需求和业务需求流的,这只能来自一个方面。


我不确定您为什么要区分“这与代码无关”。当然,这与代码有关。这是软件开发。
罗伯特·哈维

@RobertHarvey我的观点是需求流来自一个来源,演员。用户和利益相关者不参与代码,他们参与作为需求而来的业务规则。因此,SRP是控制这些需求的过程,对我而言,这不是代码。它是软件开发(!),但不是代码。
Benny Skogberg '18
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.