一个班级如何在不违反单一责任原则的情况下拥有多种方法


64

单一责任原则在维基百科上定义为

单一责任原则是一种计算机编程原则,它指出每个模块,类或功能都应对软件提供的功能的一部分负责,并且责任应完全由类封装

如果一个班级仅应承担一项职责,那么它怎么可能有不止一种方法?每种方法都不会承担不同的责任,这将意味着该类将承担多个责任。

我看到的每个展示单一责任原则的示例都使用一个只有一个方法的示例类。查看示例或使用多种方法解释类可能会有所帮助,但仍然可以认为这是一种责任。


11
为什么要投票?似乎是SE.SE的理想问题;该人研究了该主题,并努力使问题很清楚。它应该得到支持。
Arseni Mourzenko

19
否决票可能是由于这个问题已经被问过几次了,例如,请参阅softwareengineering.stackexchange.com/questions/345018/…。我认为,它并没有增加实质性的新方面。
汉斯·马丁·莫斯纳


9
这简直是​​荒谬的还原。如果每个类在字面上只允许一个方法,那么从字面上看,任何程序都不可能做一件以上的事情。
Darrel Hoffman

6
@DarrelHoffman事实并非如此。如果每个类都是一个仅带有“ call()”方法的函子,那么您基本上就已经用面向对象的程序模拟了简单的过程式程序。您仍然可以执行其他操作,因为类的“ call()”方法可以调用许多其他类的“ call()”方法。
Vaelus

Answers:


29

单一责任可能不是单一功能可以实现的。

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

该类可能会违反单一责任原则。不是因为它具有两个功能,而是如果代码必须满足不同利益相关者的需求,getX()并且getY()必须满足它们可能需要进行更改。如果X副总裁发送备忘录,所有数字均应表示为浮点数,而会计总监Y女士则坚持认为,无论X先生认为如何,她部门审查的所有数字均应为整数,则此类最好具有因为事情将变得混乱,所以只对谁负责。

如果遵循了SRP,则很明显,位置类是否有助于X先生及其团队接触到的东西。弄清楚该类负责什么,并且您知道哪个指令会影响该类。如果它们都影响这一类,那么它的设计就很差,无法最大程度地减少变更的影响。“一个班级只有一个改变的理由”并不意味着整个班级只能做一件小事。这意味着我不应该看这堂课,并且说X先生和Y太太都对这堂课感兴趣。

除了这样的事情。不,多种方法都可以。只要给它起一个清晰的名称即可,该类属于哪些方法,哪些不属于。

鲍伯叔叔的SRP 与其说是柯利定律,不如说是康威定律。鲍勃叔叔提倡将柯里定律(做一件事)应用于职能而非阶级。SRP警告不要将混合原因一起更改。康韦定律说,该系统将遵循组织的信息流向。这导致遵循SRP,因为您不在乎从未听说过的内容。

“一个模块应该对一个演员负责,而只有一个演员负责”

Robert C Martin-清洁建筑

人们一直希望SRP成为限制范围的所有理由。限制范围的原因比SRP多。我通过限制类为抽象来进一步限制范围,该抽象可以采用可以确保内部外观不会令您感到惊讶的名称。

您可以将Curly定律应用于课程。您不在Bob叔叔谈论的话题范围内,但是您可以做到。当您开始认为这意味着一种功能时,您就会出错。这就像在想一个家庭应该只有一个孩子。生一个以上的孩子并不能阻止它成为一个家庭。

如果将Curly定律应用于班级,则班级中的所有内容都应与一个统一的观念有关。这个想法可能很广泛。这个想法可能是持久性。如果其中有一些日志记录实用程序功能,那么它们显然就不合适了。X先生是否是唯一关心此代码的人并不重要。

这里应用的经典原理称为关注点分离。如果您将所有关注点分开,则可能会争辩说,任何一个地方剩下的就是一个关注点。在1991年的电影《城市滑头》将我们介绍给角色Curly之前,我们就是这个想法。

这可以。鲍伯叔叔所称的责任并不重要。对他的责任不是您关注的重点。这可能会迫使您进行更改。您可以专注于一个问题,仍然可以创建对具有不同议程的不同人群负责的代码。

也许你不在乎。精细。认为坚持“做一件事”将解决您的所有设计难题,这表明人们对“一件事”最终会变成什么样子缺乏想象力。限制范围的另一个原因是组织。您可以将许多“一件事”嵌套在其他“一件事”中,直到您拥有装满所有物品的垃圾抽屉为止。我谈到这之前

当然,限制范围的经典OOP原因是该类中包含私有字段,而不是使用getter共享数据,我们将需要该数据的每个方法放在类中,以便他们可以私有使用数据。许多人发现此限制过于局限,无法用作范围限制器,因为并非所有属于同一方法的方法都使用完全相同的字段。我希望确保将数据组合在一起的任何想法与将方法组合在一起的想法相同。

观察这个问题的功能性方法就是a.f(x)并且a.g(x)简单地是f a(x)和g a(x)。不是两个函数,而是一起变化的函数对的连续体。该a甚至没有拥有它的数据。可能只是您知道将要使用的实现f和方式g。一起改变的功能属于一起。那是好的旧的多态性。

SRP只是限制范围的众多原因之一。这是一个很好的。但不是唯一的。


25
我认为这个答案使试图弄清楚SRP的人感到困惑。总统先生和董事夫人之间的斗争不是通过技术手段解决的,并且用它来证明工程决策是合理的。康韦定律在行动。
whatsisname

8
@whatsisname相反。SRP明确适用于利益相关者。它与技术设计无关。您可能不同意这种方法,但这就是Bob叔叔最初定义SRP的方式,他不得不一遍又一遍地重申它,因为出于某种原因,人们似乎无法理解这个简单的概念(注意,是否实际上是一个完全正交的问题)。
a安

蒂姆·奥丁格(Tim Ottinger)所描述的柯里定律强调,变量应始终代表一件事。对我来说,SRP比这要强一些。一个类可以在概念上表示“一件事”,但是如果两个外部变更驱动程序以不同的方式对待“一件事”的某个方面,或者关注两个不同的方面,则违反SRP。问题是建模之一;您已经选择将某个事物建模为一个单独的类,但是域中的某些事物使选择成为问题(随着代码库的发展,事物开始以您的方式出现)。
FilipMilovanović

2
@FilipMilovanović我看到Conway的法律与SRP之间的相似之处,是Bob叔叔在他的《清洁架构》书中解释SRP的方式,来自于该组织具有干净的非周期性组织结构图的假设。这是一个老想法。甚至圣经也有这样的话:“没有人可以服务两个主人”。
candied_orange

1
@TKK将其(而不是等同于)与康威斯法则(而非柯利法则)联系起来。我否认SRP是Curly的定律的想法,主要是因为Bob叔叔在他的Clean Architecture书中这样说。
candied_orange

48

这里的关键是作用域,或者,如果您愿意的话,是粒度。由类表示的功能的一部分可以进一步分为功能的各个部分,每个部分都是一个方法。

这是一个例子。假设您需要根据序列创建CSV。如果要符合RFC 4180,将需要花费一些时间来实现算法并处理所有边缘情况。

用单个方法执行此操作将导致代码不易读,尤其是该方法将同时执行多项操作。因此,将其分为几种方法。例如,其中一个可能负责生成标头,即CSV的第一行,而另一种方法会将任何类型的值转换为适合CSV格式的字符串表示形式,而另一种方法将确定是否值需要用双引号引起来。

这些方法都有自己的责任。检查是否需要添加双引号的方法有自己的方法,而生成标头的方法只有一个。这是SRP应用于方法

现在,所有这些方法都有一个共同的目标,即采取一个序列并生成CSV。这是全班的唯一责任。


Pablo H评论:

很好的例子,但是我觉得它仍然不能回答为什么SRP允许一个类拥有多个公共方法的原因。

确实。我给出的CSV示例最好有一个公共方法,所有其他方法都是私有的。一个更好的例子是由Queue类实现的队列。此类基本上包含两个方法:(push也称为enqueue)和pop(也称为dequeue)。

  • 的职责Queue.push是将一个对象添加到队列的尾部。

  • 的职责Queue.pop是从队列的头部移除一个对象,并处理队列为空的情况。

  • Queue类的职责是提供队列逻辑。


1
很好的例子,但是我觉得它仍然不能回答为什么SRP允许一个类拥有多个公共方法的原因。
Pablo H

1
@PabloH:公平。我添加了另一个示例,其中一个类具有两个方法。
Arseni Mourzenko

30

功能就是功能。

责任就是责任。

机械师负责修理汽车,这将涉及诊断,一些简单的维护任务,一些实际的维修工作,一些将任务委派给其他人等。

容器类(列表,数组,字典,地图等)负责存储对象,这涉及存储对象,允许插入,提供访问权限,某种排序等。

单一职责并不意味着代码/功能很少,它意味着在相同职责下“属于”的任何功能。


2
同意。@Aulis Ronkainen-给出两个答案。对于嵌套的责任,使用您的机械师类比,车库负责车辆的维护。车库中的不同机械师对汽车的不同部分负责,但是这些机械师中的每一个在凝聚力中共同工作
wolfsshield

2
@wolfsshield,同意。只做一件事的机械师是没有用的,但是只负责一件事情的机械师不是(至少是必须的)。尽管现实生活中的类比不一定总是描述抽象OOP概念的最佳方法,但区分这些差异很重要。我认为,不了解差异是造成混乱的根本原因。
Aulis Ronkainen

3
@AulisRonkainen虽然看起来,闻起来和感觉都很类比,但我确实打算使用这种机制来强调SRP中“ 责任 ”一词的具体含义。我完全同意你的回答。
彼得

20

单一责任并不一定意味着它只做一件事。

以一个用户服务类为例:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

此类有多种方法,但职责很明确。它提供对数据存储中用户记录的访问。它唯一的依赖关系是用户模型和数据存储。它是松散耦合的,并且具有高度的凝聚力,这正是SRP试图让您考虑的东西。

SRP不应与“接口隔离原则”混淆(请参阅SOLID)。接口隔离原理(ISP)表示,较小的轻量级接口优于较大的更通用的接口。Go在其标准库中大量使用了ISP:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP和ISP当然是相关的,但并不意味着彼此。ISP在接口级别,而SRP在类级别。如果一个类实现了几个简单的接口,则它可能不再仅具有一种职责。

感谢Luaan指出ISP和SRP之间的区别。


3
实际上,您是在描述接口隔离原理(SOLID中的“ I”)。SRP是完全不同的野兽。
a安

顺便说一句,您在这里使用哪种编码约定?我希望的对象 UserServiceUser被UpperCamelCase,但方法 CreateExistsUpdate我会作出lowerCamelCase。
KlaymenDK

1
@KlaymenDK是的,大写只是使用Go的一种习惯(大写=导出/公共,小写=私有)
Jesse

@Luaan感谢您指出,我将澄清我的答案
Jesse

1
@KlaymenDK许多语言都将PascalCase用于方法和类。以C#为例。
Omegastick

15

餐厅有位厨师。他唯一的责任是做饭。但是他可以煮牛排,土豆,西兰花和其他一百种东西。您会为菜单上的每道菜聘请一位厨师吗?还是每道菜的每个成分都由一名厨师负责?或一个可以满足他的单一职责的厨师:做饭?

如果您还要求那位厨师也进行工资核算,那就是您违反了SRP。


4

反例:存储可变状态。

假设您有史以来最简单的类,其唯一的工作就是存储int

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

如果仅限于1种方法,则可以使用setState()getState(),除非破坏封装并i公开。

  • 没有吸气剂的二传手是无用的(您永远无法读取信息)
  • 没有setter的getter毫无用处(您永远都不能使信息突变)。

很明显,此单一职责需要在此类上至少具有2种方法。QED。


4

您误解了单一责任原则。

单一责任不等于单一方法。他们的意思不同。在软件开发中,我们谈论凝聚力。具有高内聚力的功能(方法)“一起”属于“一类”,可以被视为执行一项职责。

由开发人员来设计系统,以便实现单一责任原则。可以将其视为一种抽象技术,因此有时只是一种见解。实施单一职责原则使代码主要易于测试,也易于理解其体系结构和设计。


2

数据而不是功能的角度来看事物并组织它们通常是很有帮助的(使用任何语言,尤其是OO语言)。

因此,认为类的责任是维护类的完整性并提供帮助以正确使用其拥有的数据。显然,如果所有代码都在一个类中,而不是分散在多个类中,则这样做更容易。与其他位置相比,Point add(Point p)使用Point类中的方法可以更可靠地完成两点的加法操作,并且更易于维护代码。

特别是,该类不应暴露任何可能导致数据不一致或不正确的内容。例如,如果Point必须位于(0,0)到(127,127)平面内,则构造函数和任何修改或产生新Point值的方法都有责任检查给定的值,并拒绝任何可能违反此规定的更改需求。(通常,诸如a之类的东西Point是不可变的,并且确保Point在构造a 之后没有任何方法可以修改它,这也是该类的责任)

注意这里的分层是完全可以接受的。您可能有一个Point处理单个点的Polygon类和一个处理一组Points的类;这些仍然有各自的职责,因为Polygon将处理与某事物完全相关的所有责任Point(例如确保一个点既有an 值x又有yvalue值)委派给了Point班级。

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.