单一责任原则

tech2023-11-13  89

Robust software systems should be built up from a web of interrelated objects whose responsibilities are tight, neatly cohesive, boiled down to just performing a few narrowed and well-defined tasks. However, it’s admittedly pretty difficult to design such systems, at least in the first take. Most of the time we tend to group tasks in question by following an ad-hoc semantic sense in consonance with the nature of our own human mind.

健壮的软件系统应该由相互关联的对象组成的网络构建,这些对象的职责紧密,紧密结合,归结为仅执行一些狭窄且定义明确的任务。 但是,至少在第一时间,设计这样的系统确实很困难。 在大多数情况下,我们倾向于通过遵循特定的语义来将有问题的任务分组,以符合我们人脑的本质。

One of the most notorious consequences of this rational associative process is that, at some point, we effectively end up creating classes that do too much. The so-called “God class” is quite possibly the most extreme and coarse example of a structure that packages literally piles of unrelated operations behind the fence of the same API, but there are other subtle, more furtive situations where assigning of multiple roles to the same class are harder to track down. A good example of this is the Singleton.

这种合理的关联过程最臭名昭著的后果之一是,在某些时候,我们实际上最终会创建过多的类。 所谓的“上帝类”很可能是该结构的最极端和最粗糙的示例,该结构实际​​上将大量无关的操作打包在同一API的篱笆后面,但是在其他一些细微,更虚假的情况下,将多个角色分配给同一个班级很难追查。 一个很好的例子就是Singleton。

We all know that Singletons have been condemned for years because they suffer all sort of nasty implementation issues, with the classic mutable global state by far the most infamous one. They can be also be blamed for doing two semantically-unrelated things at the same time: asides from playing their main role, whatever this might be, they’re responsible for controlling how the originating classes should be instantiated as well. Singletons are entities unavoidably cursed with the obligation of performing at least two different tasks which aren’t even remotely related to each other.

我们都知道,单身汉已经受到了多年的谴责,因为他们遭受各种令人讨厌的实施问题,而经典的易变的全球国家迄今为止是最臭名昭著的国家。 他们也可能因同时做两项在语义上不相关的事情而受到指责:尽管发挥了主要作用,但无论如何,他们都负责控制应如何实例化原始类。 单身人士是不可避免地被迫执行至少两个彼此之间没有远程关联的不同任务的实体。

It’s easy to stay away from Singletons without feeling a pinch of guilt. But how can one be pragmatic in more generic, day-to-day situations, and design classes whose concerns are decently cohesive? Even when there’s no straight answer to the question, it’s possible to adhere in general to the rules of the Single Responsibility Principle, whose formal definition states the following:

远离辛格尔顿一家很容易而不会感到内。 但是,在更为通用,日常的情况下,以及关注程度很高的设计类中,如何能做到务实呢? 即使没有直接的问题答案,也有可能总体上遵守“ 单一责任原则”的规则,其正式定义规定如下:

There should never be more than one reason for a class to change.

改变班级的理由不应该只有一个以上。

What the principle attempts to promote is that classes must always be designed to expose only one area of concern and the set of operations they define and implement must be aimed at fulfilling that concern in particular and nothing else. In other words, a class should only change in response to the execution of those semantically-related operations. If it ever needs to change in response to another, totally unrelated operation, then it’s clear the class has more than one responsibility.

该原则试图促进的是,必须始终将类设计为仅暴露一个关注的领域,并且它们定义和实现的一组操作必须旨在特别地实现该关注,而没有其他目的。 换句话说,仅应响应于那些与语义相关的操作的执行来更改类。 如果需要更改以响应另一个完全不相关的操作,那么很明显,班级承担了多个责任。

Let’s move along and code a few digestible examples so that we can see how to take advantage of the principle’s benefits.

让我们继续前进并编写一些易于理解的示例,以便我们了解如何利用该原理的好处。

典型违反单一责任原则 (A Typical Violation of the Single Responsibility Principle)

For obvious reasons, there’s plenty of situations where a seemingly-cohesive set of operations assigned to a class actually scope different unrelated responsibilities, hence violating the principle. One that I find particularly instructive is the Active Record Pattern because of the momentum it has gained in the last few years.

出于明显的原因,在很多情况下,分配给一个类的看似有凝聚力的一组操作实际上限定了不同的不相关职责,从而违反了该原则。 我发现特别有启发性的一种是活动记录模式,因为它在过去几年中获得了发展。

Let’s pretend we’re a blind worshiper of the pattern and of the so-called database model and want to appeal to its niceties for creating a basic entity class which should model the data and behavior of generic users. Such a class, along with the contract that it implements, could be defined as follows:

让我们假设我们是该模式和所谓的数据库模型的盲目崇拜者,并希望利用它的优点来创建基本实体类,该实体类应为通用用户的数据和行为建模。 这样的类及其实现的契约可以定义如下:

<?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function getGravatar(); public function findById($id); public function insert(); public function update(); public function delete(); } <?php namespace Model; use LibraryDatabaseDatabaseAdapterInterface; class User implements UserInterface { private $id; private $name; private $email; private $db; private $table = "users"; public function __construct(DatabaseAdapterInterface $db) { $this->db = $db; } public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The user ID has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException( "The user ID is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) < 2 || strlen($name) > 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = $name; return $this; } public function getName() { if ($this->name === null) { throw new UnexpectedValueException( "The user name has not been set."); } return $this->name; } public function setEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( "The user email is invalid."); } $this->email = $email; return $this; } public function getEmail() { if ($this->email === null) { throw new UnexpectedValueException( "The user email has not been set."); } return $this->email; } public function getGravatar($size = 70, $default = "monsterid") { return "http://www.gravatar.com/avatar/" . md5(strtolower($this->getEmail())) . "?s=" . (integer) $size . "&d=" . urlencode($default) . "&r=G"; } public function findById($id) { $this->db->select($this->table, ["id" => $id]); if (!$row = $this->db->fetch()) { return null; } $user = new User($this->db); $user->setId($row["id"]) ->setName($row["name"]) ->setEmail($row["email"]); return $user; } public function insert() { $this->db->insert($this->table, [ "name" => $this->getName(), "email" => $this->getEmail() ]); } public function update() { $this->db->update($this->table, [ "name" => $this->getName(), "email" => $this->getEmail()], "id = {$this->id}"); } public function delete() { $this->db->delete($this->table, "id = {$this->id}"); } }

As one might expect from a typical implementation of Active Record, the User class is pretty much a messy structure which mingles chunks of business logic such as setting/getting usernames and email addresses, and even generating some nifty Gravatars on the fly, with data access. Is this a violation of the Single Responsibility Principle? Well, unquestionably it is, as the class exposes to the outside world two different responsibilities which by no means have a true semantic relationship with each other.

就像人们对Active Record的典型实现所期望的那样, User类几乎是一个凌乱的结构,将诸如设置/获取用户名和电子邮件地址之类的业务逻辑混合在一起,甚至可以通过数据访问即时生成一些漂亮的Gravatar 。 。 这是否违反了单一责任原则? 好吧,毫无疑问,当班级向外界展示两种不同的责任时,它们之间绝没有真正的语义关系。

Even without going through the class’ implementation and just scanning its interface, it’s clear to see that the CRUD methods should be placed in the data access layer completely insulated from where the mutators/accessors live and breath. In this case, the result of executing the findById() method for instance will change the state of the class while any call to the setters impact the CRUD operations as well. This implies there are two overlapped responsibilities coexisting here which makes the class change in response to different requirements.

即使不经历该类的实现,而只是扫描其接口,也很清楚地看到,CRUD方法应放置在数据访问层中,与更改器/访问器的生活和呼吸完全隔离。 在这种情况下,执行findById()方法的结果将更改类的状态,而对设置器的任何调用也会影响CRUD操作。 这意味着这里存在两个重叠的职责,这使得班级根据不同的要求而变化。

Of course, if you’re anything like me you’ll be wondering how to turn User into an Single Responsibility Principle-compliant structure without too much hassle during the refactoring process. The first modification that should be introduced is to keep all the domain logic within the class’ boundaries while moving away the one that deals with data access… yes, to the data access layer. There are a few nifty ways to accomplish this, but considering that the responsibilities should be sprinkled across multiple layers, the use of a data mapper is an efficient approach that permits to do this in a fairly painless fashion.

当然,如果您像我一样,您会想知道如何在重构过程中将User转变为符合“单一责任原则”的结构,而不会造成太多麻烦。 应该引入的第一个修改是将所有域逻辑保持在类的边界之内,同时移走处理数据访问的逻辑……是的,进入数据访问层。 有几种精妙的方法可以完成此任务,但是考虑到责任应该分散在多个层次上,因此使用数据映射器是一种有效的方法,可以以相当轻松的方式完成此任务。

将数据访问逻辑放入数据映射器 (Putting Data Access Logic in a Data Mapper)

The best way to keep the class’ responsibilities (the domain-related ones, of course) isolated from the ones that deal with data access is via a basic data mapper. The one below does a decent job when it comes to dividing up the responsibilities in question:

使班级职责(当然是与领域相关的职责)与负责数据访问的职责保持隔离的最佳方法是通过基本的数据映射器。 下面的一个在划分相关责任方面做得不错:

<?php namespace Mapper; use ModelUserInterface; interface UserMapperInterface { public function findById($id); public function insert(UserInterface $user); public function update(UserInterface $user); public function delete($id); } <?php namespace Mapper; use LibraryDatabaseDatabaseAdapterInterface, ModelUserInterface, ModelUser; class UserMapper implements UserMapperInterface { private $db; private $table = "users"; public function __construct(DatabaseAdapterInterface $db) { $this->db = $db; } public function findById($id) { $this->db->select($this->table, ["id" => $id]); if (!$row = $this->db->fetch()) { return null; } return $this->loadUser($row); } public function insert(UserInterface $user) { return $this->db->insert($this->table, [ "name" => $user->getName(), "email" => $user->getEmail() ]); } public function update(UserInterface $user) { return $this->db->update($this->table, [ "name" => $user->getName(), "email" => $user->getEmail() ], "id = {$user->getId()}"); } public function delete($id) { if ($id instanceof UserInterface) { $id = $id->getId(); } return $this->db->delete($this->table, "id = $id"); } private function loadUser(array $row) { $user = new User($row["name"], $row["email"]); $user->setId($row["id"]); return $user; } }

Looking at the mapper’s contract, it’s easy to see how nice the CRUD operations that polluted the User class’ ecosystem before have been placed inside a cohesive set which is now part of the raw infrastructure instead of the domain layer. This single modification should let us refactor the domain class and turn it into a cleaner, more distilled structure that conforms to the principle:

通过查看映射器的合同,可以很容易地看到污染User类生态系统的CRUD操作之前被放置在一个紧密结合的集合中有多好,该集合现在已成为原始基础结构的一部分,而不是域层。 只需进行一次修改,就可以重构域类,并将其转换为更干净,更精简的结构,以符合以下原则:

<?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function getGravatar(); } <?php namespace Model; class User implements UserInterface { private $id; private $name; private $email; public function __construct($name, $email) { $this->setName($name); $this->setEmail($email); } public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The user ID has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException( "The user ID is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) < 2 || strlen($name) > 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = $name; return $this; } public function getName() { return $this->name; } public function setEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( "The user email is invalid."); } $this->email = $email; return $this; } public function getEmail() { return $this->email; } public function getGravatar($size = 70, $default = "monsterid") { return "http://www.gravatar.com/avatar/" . md5(strtolower($this->email)) . "?s=" . (integer) $size . "&d=" . urlencode($default) . "&r=G"; } }

It could be said that User now has a better designed implementation as the batch of operations it performs not only are pure domain logic, but they’re semantically bound to each other. In Single Responsibility Principle parlance, the class has only one well-defined responsibility which is exclusively and intimately related to handling user data. No more, no less.

可以说User现在有一个设计更好的实现,因为它执行的这批操作不仅是纯域逻辑,而且在语义上是相互绑定的。 用“单一职责原则”来说,该类仅具有一个明确定义的职责,该职责与处理用户数据密切相关。 不多不少。

Of course, the example would look half-backed if I don’t show you how to get the mapper doing all the data access stuff while keeping the User class persistence agnostic:

当然,如果我不向您展示如何在保持User类持久性不可知的情况下如何使映射器执行所有数据访问工作,则该示例将显得有些支持:

<?php $db = new PdoAdapter("mysql:dbname=test", "myusername", "mypassword"); $userMapper = new UserMapper($db); // Display user data $user = $userMapper->findById(1); echo $user->getName() . ' ' . $user->getEmail() . '<img src="' . $user->getGravatar() . '">'; // Insert a new user $user = new User("John Doe", "john@example.com"); $userMapper->insert($user); // Update a user $user = $userMapper->findById(2); $user->setName("Jack"); $userMapper->update($user); // Delete a user $userMapper->delete(3);

While the example is unquestionably trivial, it does show pretty clearly how the fact of having delegated the responsibility for executing the CRUD operations to the data mapper permits us to deal with user objects whose sole area of concern is to handle exclusively domain logic. At this point, the objects’ tasks are principle-compliant, nicely distilled, and narrowed to setting/retrieving user data and rendering the associated gravatars instead of being focused additionally on persisting that data in the storage.

尽管该示例无疑是微不足道的,但它确实清楚地表明了将执行CRUD操作的责任委托给数据映射器的事实如何使我们能够处理仅关注领域逻辑的用户对象。 在这一点上,对象的任务是符合原则的,经过精炼的,并且缩小到设置/检索用户数据并渲染相关的引力图,而不是额外地专注于将数据持久存储在存储器中。

结束语 (Closing Remarks)

Perhaps just a biased opinion based on my own experience as developer (so take it at face value), I’d dare to say the Single Responsibility Principle’s worst curse and certainly the reason why it’s so blatantly ignored in practice is the pragmatism of reality. Obviously it’s a lot easier “to get the job done” and struggle with tight deadlines by blindly assigning a bunch of roles to a class, without thinking if they’re semantically related to each other.

也许只是基于我自己作为开发人员的经验得出的有偏见的想法(因此,将其视为实物),我敢说“单一责任原则”的最坏诅咒,而且在实践中如此被公然忽略的原因肯定是现实的实用主义。 显然,“盲目地分配”一类角色给班级,而不考虑它们在语义上是否相互关联,“完成工作”和在紧迫的期限内挣扎要容易得多。

Even in enterprise environments, where the use of contracts for outlining explicitly the behavior of application components isn’t just a luxury but a must, it’s pretty difficult to figure out how to group together cohesively a set of operations. Even though, it’s doesn’t hurt to take some time and design cleaner classes that don’t mix up unnecessarily heaps of unrelated responsibilities. In that sense, the principle is just a guideline that will assist you in the process, but certainly a very valuable one.

即使在企业环境中,使用合同明确概述应用程序组件的行为不仅是奢侈,而且是必须的,但要弄清楚如何将一组操作紧密地组合在一起,还是很困难的。 即使如此,花一些时间设计干净的类也不会造成不必要的无关责任的混淆,这并没有什么坏处。 从这个意义上讲,该原则仅是可以为您提供帮助的准则,但无疑是非常有价值的准则。

Image via Fotolia

图片来自Fotolia

翻译自: https://www.sitepoint.com/the-single-responsibility-principle/

相关资源:jdk-8u281-windows-x64.exe
最新回复(0)