单一责任原则在维基百科上定义为
单一责任原则是一种计算机编程原则,它指出每个模块,类或功能都应对软件提供的功能的一部分负责,并且责任应完全由类封装
如果一个班级仅应承担一项职责,那么它怎么可能有不止一种方法?每种方法都不会承担不同的责任,这将意味着该类将承担多个责任。
我看到的每个展示单一责任原则的示例都使用一个只有一个方法的示例类。查看示例或使用多种方法解释类可能会有所帮助,但仍然可以认为这是一种责任。
单一责任原则在维基百科上定义为
单一责任原则是一种计算机编程原则,它指出每个模块,类或功能都应对软件提供的功能的一部分负责,并且责任应完全由类封装
如果一个班级仅应承担一项职责,那么它怎么可能有不止一种方法?每种方法都不会承担不同的责任,这将意味着该类将承担多个责任。
我看到的每个展示单一责任原则的示例都使用一个只有一个方法的示例类。查看示例或使用多种方法解释类可能会有所帮助,但仍然可以认为这是一种责任。
Answers:
单一责任可能不是单一功能可以实现的。
class Location {
public int getX() {
return x;
}
public int getY() {
return y;
}
}
该类可能会违反单一责任原则。不是因为它具有两个功能,而是如果代码必须满足不同利益相关者的需求,getX()
并且getY()
必须满足它们可能需要进行更改。如果X副总裁发送备忘录,所有数字均应表示为浮点数,而会计总监Y女士则坚持认为,无论X先生认为如何,她部门审查的所有数字均应为整数,则此类最好具有因为事情将变得混乱,所以只对谁负责。
如果遵循了SRP,则很明显,位置类是否有助于X先生及其团队接触到的东西。弄清楚该类负责什么,并且您知道哪个指令会影响该类。如果它们都影响这一类,那么它的设计就很差,无法最大程度地减少变更的影响。“一个班级只有一个改变的理由”并不意味着整个班级只能做一件小事。这意味着我不应该看这堂课,并且说X先生和Y太太都对这堂课感兴趣。
除了这样的事情。不,多种方法都可以。只要给它起一个清晰的名称即可,该类属于哪些方法,哪些不属于。
鲍伯叔叔的SRP 与其说是柯利定律,不如说是康威定律。鲍勃叔叔提倡将柯里定律(做一件事)应用于职能而非阶级。SRP警告不要将混合原因一起更改。康韦定律说,该系统将遵循组织的信息流向。这导致遵循SRP,因为您不在乎从未听说过的内容。
“一个模块应该对一个演员负责,而只有一个演员负责”
人们一直希望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只是限制范围的众多原因之一。这是一个很好的。但不是唯一的。
这里的关键是作用域,或者,如果您愿意的话,是粒度。由类表示的功能的一部分可以进一步分为功能的各个部分,每个部分都是一个方法。
这是一个例子。假设您需要根据序列创建CSV。如果要符合RFC 4180,将需要花费一些时间来实现算法并处理所有边缘情况。
用单个方法执行此操作将导致代码不易读,尤其是该方法将同时执行多项操作。因此,将其分为几种方法。例如,其中一个可能负责生成标头,即CSV的第一行,而另一种方法会将任何类型的值转换为适合CSV格式的字符串表示形式,而另一种方法将确定是否值需要用双引号引起来。
这些方法都有自己的责任。检查是否需要添加双引号的方法有自己的方法,而生成标头的方法只有一个。这是SRP应用于方法。
现在,所有这些方法都有一个共同的目标,即采取一个序列并生成CSV。这是全班的唯一责任。
Pablo H评论:
很好的例子,但是我觉得它仍然不能回答为什么SRP允许一个类拥有多个公共方法的原因。
确实。我给出的CSV示例最好有一个公共方法,所有其他方法都是私有的。一个更好的例子是由Queue
类实现的队列。此类基本上包含两个方法:(push
也称为enqueue
)和pop
(也称为dequeue
)。
的职责Queue.push
是将一个对象添加到队列的尾部。
的职责Queue.pop
是从队列的头部移除一个对象,并处理队列为空的情况。
Queue
类的职责是提供队列逻辑。
功能就是功能。
责任就是责任。
机械师负责修理汽车,这将涉及诊断,一些简单的维护任务,一些实际的维修工作,一些将任务委派给其他人等。
容器类(列表,数组,字典,地图等)负责存储对象,这涉及存储对象,允许插入,提供访问权限,某种排序等。
单一职责并不意味着代码/功能很少,它意味着在相同职责下“属于”的任何功能。
单一责任并不一定意味着它只做一件事。
以一个用户服务类为例:
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之间的区别。
UserService
和User
被UpperCamelCase,但方法 Create
,Exists
和Update
我会作出lowerCamelCase。
餐厅有位厨师。他唯一的责任是做饭。但是他可以煮牛排,土豆,西兰花和其他一百种东西。您会为菜单上的每道菜聘请一位厨师吗?还是每道菜的每个成分都由一名厨师负责?或一个可以满足他的单一职责的厨师:做饭?
如果您还要求那位厨师也进行工资核算,那就是您违反了SRP。
从数据而不是功能的角度来看事物并组织它们通常是很有帮助的(使用任何语言,尤其是OO语言)。
因此,认为类的责任是维护类的完整性并提供帮助以正确使用其拥有的数据。显然,如果所有代码都在一个类中,而不是分散在多个类中,则这样做更容易。与其他位置相比,Point add(Point p)
使用Point
类中的方法可以更可靠地完成两点的加法操作,并且更易于维护代码。
特别是,该类不应暴露任何可能导致数据不一致或不正确的内容。例如,如果Point
必须位于(0,0)到(127,127)平面内,则构造函数和任何修改或产生新Point
值的方法都有责任检查给定的值,并拒绝任何可能违反此规定的更改需求。(通常,诸如a之类的东西Point
是不可变的,并且确保Point
在构造a 之后没有任何方法可以修改它,这也是该类的责任)
注意这里的分层是完全可以接受的。您可能有一个Point
处理单个点的Polygon
类和一个处理一组Point
s的类;这些仍然有各自的职责,因为Polygon
将处理与某事物完全相关的所有责任Point
(例如确保一个点既有an 值x
又有y
value值)委派给了Point
班级。