依赖反转原则

简介

在整个「SOLID」原则概述的旅途中,我们到达最后一站了!最后一个原则是依赖反转原则,它规定高层次的代码不应该依赖低层级的代码。换句话说,高层次的代码应该依赖抽象接口,抽象接口就像是「中间人」一样,负责连接着高层次和低层次代码。这个原则的另一层意思是,抽象接口不应该依赖具体实现,但具体实现应该依赖抽象接口。如果这些理论听起来让你极端困惑,别担心,接下来我们会将围绕这两个方面将这个原则详细介绍给你。

实践

如果你已经读过了本书前面几个章节,你其实就已经很好地掌握了依赖反转原则!为了说明本原则,我们来看看下面这个类:

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...
    }
}

你可能猜到了,Authenticator 是用来查找和认证用户的。我们来看一下它的构造函数,你会发现它使用了类型提示,要求传入一个DatabaseConnection 对象,所以该认证器和数据库被紧密地耦合在一起,并且用户数据只能通过支持 SQL 的关系型数据库提供。此外,我们的高层次代码(Authenticator)直接依赖低层次代码(DatabaseConnection)。

首先,我们需要来谈谈「高层次代码」和「低层次代码」。低层次代码用于实现一些底层的基本操作,比如从磁盘读文件、操作数据库等。高层次代码用于封装复杂的业务逻辑并且依靠低层次代码来实现功能,但不能直接和低层次代码耦合在一起。换句话说,高层次代码需要依赖低层次代码的顶层抽象,比如接口。不仅如此,低层次代码也应当依赖抽象接口。所以,我们来写个可以在 Authenticator 中使用的接口:

interface UserProviderInterface 
{
    public function find($id);
    public function findByUsername($username);
}

接下来我们将该接口注入到 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;
    }
}

做了以上改动后,Authenticator 现在依赖于两个高层级的抽象:UserProviderInterfaceHasherInterface。我们可以向Authenticator 注入这两个接口的任何实现类。例如,如果我们的用户存储在 Redis 里面,我们只需写一个 RedisUserProvider 来实现UserProviderInterface 接口即可。Authenticator 不再直接依赖低层次的存储操作了。

此外,我们的低层次代码现在依赖高层次的 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);
    }

}

反转的思想:使用这一原则会反转很多开发者设计应用的方式。不再将高层次代码直接和低层次代码以「自上而下」的方式耦合在一起,这个原则规定不论高层级还是低层次代码都要依赖于一个高层次的抽象,从而使得低层次代码依赖于高层次代码的需求抽象。

在我们没有反转 Authenticator 的依赖之前,它除了使用数据库存储系统别无选择。如果我们改变了存储系统,Authenticator 也需要被修改,这就违背了开放封闭原则。我们又一次看到,这些设计原则通常一荣俱荣一损俱损。

强制让 Authenticator 依赖一个存储层的抽象接口之后,我们就可以通过任何实现了 UserProviderInterface 接口的存储系统来使用它,而且不用对 Authenticator 本身做任何修改。传统的依赖关系链已经被反转了,代码变得更灵活,可以更好的拥抱变化!

上一篇: 接口隔离原则

下一篇: 没有下一篇了