该类设计是否违反单一责任原则?


63

今天我和某人吵架。

我正在解释使用富域模型相对于贫血域模型的好处。我用一个简单的类演示了我的观点:

public class Employee
{
    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastname;
    }

    public string FirstName { get private set; }
    public string LastName { get; private set;}
    public int CountPaidDaysOffGranted { get; private set;}

    public void AddPaidDaysOffGranted(int numberOfdays)
    {
        // Do stuff
    }
}

当他为贫乏的模型方法辩护时,他的论据之一是:“我坚信SOLID。您在同一类中代表数据并执行逻辑时,您正在违反单一责任原则(SRP)。”

我发现这种说法确实令人惊讶,因为按照这种推理,任何具有一个属性和一种方法的类都违反了SRP,因此,OOP通常不是SOLID,而函数式编程是通向天堂的唯一途径。

我决定不回答他的许多论点,但我很好奇社区对此问题的看法。

如果我回答了,那么我将首先指出上述悖论,然后指出SRP高度依赖于您要考虑的粒度级别,并且如果将其考虑得足够远,则任何包含多个以上级别的类属性或一种方法违反了它。

你会说什么?

更新:该示例已由guntbert慷慨地进行了更新,以使该方法更加实际,并帮助我们专注于基础讨论。


19
这个类违反了SRP,不是因为它的逻辑混合数据,但由于它具有低凝聚力-潜在的神对象
蚊蚋

1
给员工增加假期的目的是什么?也许应该有一个日历班或有假期的东西。我认为你的朋友是对的。
詹姆斯·布莱克

9
永远不要听别人说“我是X的信徒”。
Stig Hemmer

29
这是否违反了SRP并不仅仅是一个好的建模,还不止于此。假设我是一名雇员。当我问我的经理是否可以花一个较长的周末去滑雪时,我的经理没有给我增加假期。这里的代码与它打算建模的实际情况不符,因此是可疑的。
埃里克·利珀特

2
函数编程的+1是通向天堂的唯一途径。
erip

Answers:


68

单一职责应理解为系统中逻辑任务的抽象。一个班级应该负有(执行所有必要的工作)执行一项特定任务的责任。实际上,根据职责是什么,这可以使很多人进入精心设计的类。例如,运行您的脚本引擎的类可以包含许多处理脚本的方法和数据。

您的同事正在专注于错误的事情。问题不是“该班级有哪些成员?” 但是“此类在程序中执行哪些有用的操作?” 理解之后,您的域模型就可以了。


如果创建了面向对象的程序设计并打算为现实生活中的对象和类(动作+属性)建模,那么为什么不编写具有多个职责(动作)的代码呢?现实世界中的对象可以承担多种责任。例如,一名记者在新闻报纸上撰写社论,并在电视节目中采访政治人物。现实生活对象的两个责任!如果我要写一个班级新闻记者怎么办?
user1451111

41

单一责任原则仅与一段代码(在OOP中,通常我们谈论的是类)是否对一项功能负责有关。我认为您的朋友说功能和数据不能混合在一起并不能真正理解这个想法。如果Employee还包含有关他的工作场所,他的车走多快以及他的狗吃哪种食物的信息,那么我们将有问题。

由于此类仅处理Employee,我认为可以公平地说它没有公然违反SRP,但是人们总是会有自己的见解。

我们可能需要改进的一个地方是将员工信息(例如姓名,电话号码,电子邮件)与他的假期分开。在我看来,这并不意味着方法和数据不能混合在一起,而仅意味着休假功能可能在单独的位置。


20

我认为,如果该类继续代表Employee和,则可能会违反SRP EmployeeHolidays

照原样,如果涉及同行评审,我可能会通过。如果添加了更多的员工特定属性和方法,并且添加了更多的假期特定属性,我可能会建议采用SRP和ISP进行拆分。


我同意。如果代码像这里提供的那样简单,我可能会让它滑动。但是在我看来,员工不应自行负责自己的假期。承担责任的位置似乎没什么大不了,但是应该这样看:如果您是代码基础的新手,并且必须使用假期特定的功能,那么您应该首先看什么?对于假期逻辑,我个人不会先考虑Employee实体。
Niklas H

1
@NiklasH同意。对于我个人,我不会随意寻找并猜测我将在Studio搜索“ Holiday”一词的班级,然后看看它出现了哪些班级。:)
NikolaiDante

4
真正。但是如果在这个新系统中它不是“假期”,而是“假期”或“空闲时间”,该怎么办。但我同意,您通常可以搜索它,或者可以询问同事。我的评论主要是让OP在精神上对责任进行建模,以及最明显的逻辑位置是:-)
Niklas H

我同意你的第一句话。但是,如果要进行同行评审,我认为我不会这样做,因为违反SRP是一个滑坡,这可能是许多破窗事件中的第一个。干杯。
Jim演讲者,

20

已经有很好的答案指出SRP是关于逻辑功能的抽象概念,但是我认为有一些微妙的观点值得补充。

SOLID中的前两个字母SRP和OCP都是关于代码如何根据需求的变化而变化的。我最喜欢的SRP定义是:“模块/类/功能应该只有一个更改的理由。” 争论代码更改的可能原因比争论代码是否为SOLID更有效率。

您的Employee类别必须更改多少原因?我不知道,因为我不知道您使用它的上下文,而且我也看不到未来。我所能做的就是根据我过去的经验来集思广益,并可能主观地评估它们的可能性。如果在“合理可能”和“我的代码已由于该确切原因而发生了更改”之间得分不止一个,那么您正在违反SRP进行此类更改。这是一个:拥有两个以上名字的人加入了您的公司(或架构师阅读了这篇出色的W3C文章)。这是另一个:您的公司更改了假期的分配方式。

请注意,即使您删除AddHolidays方法,这些原因也同样有效。许多贫血领域模型违反了SRP。其中许多只是代码中的数据库表,数据库表有20多个更改原因是很常见的。

需要注意的是:如果您的系统需要跟踪员工薪水,您的Employee类是否会更改?地址?紧急联系方式?如果您对其中两个说“是”(并且“可能发生”),那么即使您的课程中没有代码,您的课程也会违反SRP!SOLID与过程和体系结构一样,也与代码有关。


9

类代表数据不是类的责任,而是私有的实现细节。

班级有责任代表员工。在这种情况下,这意味着它提供了一些公共API,为您提供了与员工打交道所需的功能(AddHolidays是否是一个很好的例子,值得商))。

实现是内部的;碰巧这需要一些私有变量和一些逻辑。这并不意味着该班现在有多个职责。


有趣的思路,非常感谢您的分享
tobiak777 '16

尼斯-表达OOP预期目标的好方法。
user949300'1

5

以任何方式混合逻辑和数据始终是错误的想法,如此荒谬,甚至不值得讨论。但是,示例中确实确实明显违反了单一职责,但这不是因为有属性DaysOfHolidays和函数AddHolidays(int)

这是因为员工的身份与假期管理混在一起,这很糟糕。员工的身份是一件复杂的事情,需要跟踪假期,薪水,加班,代表谁在哪个团队中,链接到绩效报告等。员工还可以更改名字,姓氏或两者,并且保持不变雇员。员工甚至可以使用其姓名的多种拼写,例如ASCII和unicode拼写。人们可以使用0到n个名字和/或姓氏。他们在不同的司法管辖区可能有不同的名称。跟踪员工的身份足以承担一项责任,因此如果不将其称为第二项责任,则不能将假期或假期管理放在首位。


“跟踪员工的身份足以承担一项责任,因此如果不将其称为第二项责任,就不能将假期或假期管理放在首位。” +员工可能有几个名字,等等。模型的重点是着眼于现实世界中有关手头问题的相关事实。存在为此模型最佳的要求。在这些要求中,员工只有在我们可以更改其假期的程度内才对他们感兴趣,而我们对管理其实际生活中的其他方面并不感兴趣。
tobiak777 '16

@reddy“员工只有在我们可以修改他们的假期的情况下才很有趣”-这意味着您需要正确地识别他们。一旦您有了一名雇员,他们就可以由于结婚或离婚而随时更改其姓氏。他们还可以更改其姓名和性别。如果员工的姓氏更改为与另一名员工的名字匹配,您会解雇吗?您现在不会立即添加所有此功能。而是在需要时添加它,这很好。不管实施了多少,标识的责任都保持不变。
彼得

3

“我坚信SOLID。您在同一类中代表数据并执行逻辑时,您在违反单一责任原则(SRP)。”

像其他人一样,我不同意这一点。

我要说的是,如果您在课堂上执行了多个逻辑,就会违反SRP 。在该类中需要存储多少数据才能实现该单一逻辑。


没有!多个逻辑,多个数据或两者的任何组合都不会违反SRP。唯一的要求是对象应坚持其宗旨。其目的可能需要进行许多操作。
马丁·马特

@MartinMaat:是的,很多操作。结果是一种逻辑。我想我们说的是同一件事,只是用不同的用语(我很高兴假设您的是正确的,因为我不经常学习这些东西)
Lightness Races in Orbit

2

这些天来,我讨论关于什么是什么,什么不构成改变的单一责任或单一原因的辩论并没有用。我会提出一个“最低悲伤原则”来代替:

最小悲伤原则:代码应该设法使需要更改的可能性最小化,或者使更改的容易性最大化。

怎么样?不应让火箭科学家弄清楚为什么这可以帮助减少维护成本,并且希望它不会引起无休止的争论,但与SOLID一样,总的来说并不是在所有地方盲目地应用。在权衡取舍时需要考虑的事情。

至于需要更改的可能性,则归结为:

  1. 良好的测试(提高了可靠性)。
  2. 只涉及执行特定操作所需的最低限度的最低代码(这可能包括减少传入耦合)。
  3. 只需按其功能使代码变得糟糕(请参见使坏作法原理)。

至于更改的难度,它与传出的耦合有关。测试引入了传出联轴器,但它提高了可靠性。做得好,它通常弊大于利,并且被“最低悲伤原则”完全接受和推广。

制定坏蛋原则:在很多地方使用的类都很棒。如果与其质量相关,则它们应该可靠,高效。

并且“使坏蛋原则”与“最小悲伤原则”联系在一起,因为坏蛋事物发现改变的可能性比吸吮自己所做事情的事物低。

我将首先指出上述悖论,然后指出SRP高度依赖于您要考虑的粒度级别,并且如果将其考虑得足够多,则任何包含多个属性或一种方法的类都将受到侵犯。它。

从SRP的角度来看,几乎什么都不做的类肯定只有一个(有时为零)更改的原因:

class Float
{
public:
    explicit Float(float val);
    float get() const;
    void set(float new_val);
};

那实际上没有理由改变!比SRP好。是ZRP!

除非我认为这是公然违反《使坏蛋的原则》。这也是绝对一文不值的。做得很少的事情不能指望是坏蛋。它的信息(TLI)太少。而且自然而然地,当您拥有TLI之类的东西时,即使使用它封装的信息,它也无法做任何真正有意义的事情,因此它必须将其泄漏给外界,以希望其他人会真正做出有意义的事情和坏蛋。对于只希望聚合数据而已的内容,泄漏是可以的,但正如我所见,该阈值是“数据”与“对象”之间的差异。

当然,TMI也很不好。我们可能会将整个软件归为一类。它甚至可以只有一种run方法。甚至有人可能会争辩说,现在有一个非常广泛的更改理由:“仅当软件需要改进时,才需要更改此类。” 我很傻,但是我们当然可以想象所有维护问题。

因此,要平衡设计对象的粒度或粗糙度。我经常根据您需要向外界泄漏多少信息以及它可以执行多少有意义的功能来进行判断。我经常发现“使坏蛋原理”有助于在与“最小悲伤原则”相结合的同时找到平衡。


1

相反,对我而言,Anemic域模型打破了一些OOP主要概念(将属性和行为联系在一起),但是基于架构选择,这是不可避免的。贫血域更容易思考,有机性降低,顺序性更高。

当多层必须使用同一数据(服务层,Web层,客户端层,代理...)播放时,许多系统倾向于这样做。

在一个地方定义数据结构和在其他服务类中定义行为比较容易。如果在多个层上使用了相同的类,则该类可能会变大,并询问以下问题:哪个层负责定义所需的行为,以及谁可以调用这些方法。

例如,这可能不是一个好主意,而是一个Agent进程,该进程可以计算您所有员工的统计信息,可以调用带薪天的计算。而且,员工列表GUI完全不需要此统计代理中使用的新的汇总ID计算(以及随之而来的技术数据)。当您以这种方式分离方法时,通常以只有数据结构的类结束。

您可以轻松地序列化/反序列化“对象”数据,或者只是其中的一些,或者将其序列化/反序列化为另一种格式(json)...而无需担心任何对象概念/职责。虽然只是数据传递。您始终可以在两个类(Employee,EmployeeVO,EmployeeStatistic…)之间进行数据映射,但是Employee在这里的真正含义是什么?

因此,是的,它完全将域类中的数据与服务类中的数据处理分开,但这是需要的。同时,这样的系统既是功能性的,可以带来业务价值,又是技术性的,可以在需要维护责任范围的同时将数据传播到需要的任何地方(而分布式对象也不能解决这个问题)。

如果不需要分开行为范围,则可以根据查看对象的方式将方法放到服务类或域类中。我倾向于将对象视为“真实”概念,这自然有助于保持SRP。因此,在您的示例中,这比授予员工的PayDayAccount的老板的加薪日更为现实。员工被公司,工厂雇用,可能生病或被征求建议,并且他有一个发薪日帐户(老板可以直接从他或从PayDayAccount注册表中检索它)。但是您可以创建一个汇总的快捷方式如果您不想为简单的软件带来太多复杂性,请点击此处。


感谢您的输入文斯。关键是,拥有丰富的域时,您不需要服务层。行为仅由一个类负责,它是您的域实体。其他层(Web层,UI等)通常处理DTO和ViewModels,这很好。域模型是为域建模,而不是执行UI工作或通过Internet发送消息。您的消息反映了这种普遍的误解,这是由于人们根本不知道如何将OOP融入其设计中。我认为这对他们来说非常可悲。
tobiak777 '16

我不认为我对富域OOP有误解,我以这种方式设计了许多软件,这对维护/演进确实非常有用。但是,很抱歉告诉您,这不是万灵药。
文斯

我不是说那是。对于编写编译器来说可能不是,但对于大多数业务/ SaaS应用程序,我认为它比您建议的艺术/科学少得多。领域模型的需求可以用数学方式证明,您的例子让我想到了有争议的设计,而不是OOP中的局限性缺陷
tobiak777

0

您在同一类中代表数据并执行逻辑,因此违反了单一责任原则(SRP)。

对我来说听起来很合理。如果公开操作,则模型可能没有公共属性。基本上,这是一个命令查询分离的想法。请注意,Command将肯定具有私有状态。


0

您不能违反“ 单一责任原则”,因为它只是一种审美标准,而不是自然法则。不要被科学名称和大写字母所迷惑。


1
这几乎不是一个真正的答案,但作为对该问题的评论,它本来很好。
杰伊·埃尔斯顿
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.