We have reached our final destination in our overview of the five SOLID design principles! The final principle is the Dependency Inversion principle, and it states that high-level code should not depend on low-level code. Instead, high-level code should depend on an abstraction layer that serves as a “middle-man” between the high and low-level code. A second aspect to the principle is that abstractions should not depend upon details, but rather details should depend upon abstractions. If this all sounds extremely confused, don't worry. We'll cover both aspects of this principle below.
在整个“坚实”原则概述的旅途中,我们到达最后一站了!最后的原则是依赖反转原则,它规定高等级的代码不应该依赖低等级的代码。高等级的代码应该依赖着抽象层,抽象层就像是“中间人”一样,负责连接着高等级和低等级的代码。该原则第二个方面是抽象定义不应该依赖着具体实现,但具体实现应该依赖着抽象定义。如果这些东西让你极端困惑,别担心。接下来我们会将这两方面统统介绍给你。
Dependency Inversion Principle 依赖反转原则
This principle states that high-level code should not depend on low-level code, and that abstractions should not depend upon details.
该原则要求高等级代码不应该依赖低等级代码,抽象定义不应该依赖具体实现。
If you have already read prior chapters of this book, you already have a good grasp of the Dependency Inversion principle! To illustrate the principle, let's consider the following class:
如果你已经读过了本书前面几个章节,你就已经很好掌握了依赖反转原则!为了说明本原则,让我们考虑下面这个类:
class Authenticator {
public function __construct(DatabaseConnection $db)
{
$this->db = $db;
}
public function findUser($id)
{
return $this->db->exec('select * from users where id = ?', array($id));
}
public function authenticate($credentials)
{
// Authenticate the user...
}
}
As you might have guessed, the Authenticator
class is responsible for finding and authenticating users. Let's examine the constructor of this class. You will see that we are type-hinting a DatabaseConnection
instance. So, we're tightly coupling our authenticator to the database, and essentially saying that users will always only be provided out of a relational SQL database. Furthermore, our high-level code (the Authenticator
) is directly depending on low-level code (the DatabaseConnection
).
你可能猜到了,Authenticator
就是用来查找和验证用户的。继续研究它的构造函数。我们发现它使用了类型提示,要求传入一个DatabaseConnection
对象,所以该验证类和数据库被紧密的联系在一起。而且基本上讲,这个数据库还只能是关系数据库。从而可知,我们的高级代码(Authenticator
)直接的依赖着低级代码(DatabaseConnection
)。
First, let's discuss “high-level” and “low-level” code. Low-level code implements basic operations like reading files from a disk, or interaction with a database. High-level code encapsulates complex logic and relies on the low-level code to function, but should not be directly coupled to it. Instead, the high-level code should depend on an abstraction that sits on top of the low-level code, such as an interface. Not only that, but the low-level code should also depend upon an abstraction. So, let's write an interface that we can use within our Authenticator
:
首先我们来谈谈“高级代码”和“低级代码”。低级代码用于实现基本的操作,比如从磁盘读文件,操作数据库等。高级代码用于封装复杂的逻辑,它们依靠低级代码来达到功能目的,但不能直接和低级代码耦合在一起。取而代之的是高级代码应该依赖着低级代码的顶层抽象,比如接口。不仅如此,低级代码也应当依赖着抽象。 所以我们来写个Authenticator
可以用的接口:
interface UserProviderInterface {
public function find($id);
public function findByUsername($username);
}
Next let's inject an implementation of this interface into our Authenticator
:
接下来我们将该接口注入到Authenticator
里面:
class Authenticator {
public function __construct(UserProviderInterface $users, HasherInterface $hash)
{
$this->hash = $hash;
$this->users = $users;
}
public function findUser($id)
{
return $this->users->find($id);
}
public function authenticate($credentials)
{
$user = $this->users->findByUsername($credentials['username']);
return $this->hash->make($credentials['password']) == $user->password;
}
}
After making these changes, our Authenticator
now relies on two high-level abstractions: UserProviderInterface
and HasherInterface
. We are free to inject any implementation of these interfaces into the Authenticator
. For example, if our users are now stored in Redis, we can write a RedisUserProvider
which implements the UserProviderInterface
contract. The Authenticator
is no longer depending directly on low-level storage operations.
做了这些小改动后,Authenticator
现在依赖于两个高级抽象:UserProviderInterface
和HasherInterface
。我们可以向Authenticator
自由的注入这俩接口的任何实现类。比如,如果我们的用户存储在Redis里面,我们只需写一个RedisUserProvider
来实现UserProviderInterface
接口即可。Authenticator
不再依赖着具体的低级别的存储操作了。
Furthermore, our low-level code now depends on the high-level UserProviderInterface
abstraction, since it implements the interface itself:
此外,由于我们的低级别代码实现了UserProviderInterface
接口,则我们说该低级代码依赖着这个接口。
class RedisUserProvider implements UserProviderInterface {
public function __construct(RedisConnection $redis)
{
$this->redis = $redis;
}
public function find($id)
{
$this->redis->get('users:'.$id);
}
public function findByUsername($username)
{
$id = $this->redis->get('user:id:'.$username);
return $this->find($id);
}
}
Inverted Thinking 反转的思维
Applying this principle inverts the way many developers design applications. Instead of coupling high-level code directly to low-level code in a “top-down” fashion, this principle states that both high and low-level code depend upon a high-level abstraction.
贯彻这一原则会反转好多开发者设计应用的方式。不再将高级代码直接和低级代码以“自上而下”的方式耦合在一起,这个原则提出无论高级还是低级代码都要依赖于一个高层次的抽象。
Before we “inverted” the dependencies of our Authenticator
, it could not be used with anything other than a database storage system. If we changed storage system, our Authenticator
would also have to be modified, in violation of the Open Closed principle. Again, as we have seen before, multiple principles usually stand or fall together.
在我们没有反转Authenticator
的依赖之前,它除了使用数据库存储系统别无选择。如果我们改变了存储系统,Authenticator
也需要被修改,这就违背了开放封闭原则。我们又一次看到,这些设计原则通常一荣俱荣一损俱损。
After forcing the Authenticator
to depend upon an abstraction over the storage layer, we can use it with any storage system that implements our UserProviderInterface
contract without any modifications to the Authenticator
itself. The conventional dependency chain has been inverted and our code is much more flexible and welcoming of change!
通过强制让Authenticator
依赖着一个存储抽象层,我们就可以使用任何实现了UserProviderInterface
接口的存储系统,且不用对Authenticator
本身做任何修改。传统的依赖关系链已经被反转了,代码变得更灵活,更加无惧变化!
完