【译】C#中的SOLID之D:依赖反转
原文链接:传送门。
这篇文章是关于SOLID设计原则的系列文章的一部分(关于D的部分)。你可以从这里开始进行学习,也可以使用下面的链接跳转到相应的页面:
S – Single Responsibility
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversion
好吧,让我们先从维基百科的定义开始:
- 高级别的模块不能依赖于低级别的模块。两者都应该依赖于抽象(比如说,依赖于接口)。
- 抽象不应该依赖于具象。具象(实际的实现)应该依赖于抽象。
这听起来似乎有些复杂,然而当你与这些代码一起工作的时候,你会发现它实际上简单很多。事实上很可能它正是某个你正在做的(特别是在.NET Core里)但并没有仔细考虑的事。
简单点来说,想象我们有一个服务,其对某个“Repository”进行了调用。一般来说,那个“Repository"会有一个接口(特别是在.NET Core中),我们会使用依赖注入来将那个“Repository”注入到我们的服务中,然而,服务仍旧是与接口一起工作,而不是与具体的实现一起工作。在这些基础之上,我们的服务本身并不知道任何关于Repository如何获取到数据的细节。这个Repository可能调用了SQL SERVER,Azure表存储,或者是硬盘上的一个文件。但是对于我们的服务来说,它真的不关心这个。它只知道其可以依赖这个抽象来获取到它所需要的数据,并且现在要担心这个Repository可能具有的一些依赖。
依赖反转 vs 依赖注入
通常来说,人们常常混淆依赖注入和依赖反转。我知道很多年前在一次采访中我被要求举出SOLID,我当时说 依赖注入是 这个D。
宽泛的说,依赖注入是实现依赖反转的一种方式。就如同是实现原则的一个工具。依赖反转简单的表名你应该依赖于抽象,并且高级别模块不应该担心低级别模块的依赖,而依赖注入就是通过注入依赖来实现它的一种方式。
另一种考虑它的方式便是你可以通过简单使用工厂模式来创建低级别的模块,并让工厂返回接口实现依赖反转(因此高级别的模块便会依赖于抽象并且不需要知道藏在接口之后的具体的类是什么)。
或者使用一个服务定位模式(Service Locator pattern ),有些人或许也会说其就是依赖注入。它之所以也被归类为依赖反转是因为同样你也不需要担心低级别的模块是如何创建的。你只需要调用这个服务定位器然后你便魔法般的得到了一个完美的抽象。
实践中的依赖反转
让我们来看一个例子,其没有真正的演示好依赖反转行为:
class PersonRepository { private readonly string _connectionString; private readonly int _connectionTimeout; public PersonRepository(string connectionString, int connectionTimeout) { _connectionString = connectionString; _connectionTimeout = connectionTimeout; } public void ConnectToDatabase() { //Go away and make a connection to the database. } public void GetAllPeople() { //Use the database connection and then return people. } } class MyService { private readonly PersonRepository _personRepository; public MyService() { _personRepository = new PersonRepository("myConnectionString", 123); } }
这段代码的问题在于MyService类严重依赖于PersonRepository类的具体实现细节。举个例子,我们需要传递给它一个连接字符串(所以我们其实泄露了其是一个SQL SERVER),以及一个连接超时时间。PersonRepository类本身允许一个“ConnectToDatabase”方法本身其并不算一件坏的设计。但是如果在我们能够调用“GetAllPeople”方法之前,我们需要调用“ConnectToDatabase”方法,那么很显然我们并没有将抽象从这个SQL SERVER的具体实现中分离出来。
让我们做一些简单的事情来清理下这段逻辑:
interface IPersonRepository { void GetAllPeople(); } class PersonRepository : IPersonRepository { private readonly string _connectionString; private readonly int _connectionTimeout; public PersonRepository(string connectionString, int connectionTimeout) { _connectionString = connectionString; _connectionTimeout = connectionTimeout; } private void connectToDatabase() { //Go away and make a connection to the database. } public void GetAllPeople() { connectToDatabase(); //Use the database connection and then return people. } } class MyService { private readonly IPersonRepository _personRepository; public MyService(IPersonRepository personRepository) { _personRepository = personRepository; } }
很简单。我创建了一个接口来剥离了实现细节。我已经移除了那个Repository并且使用依赖注入将它注入到我的服务中。在这种方式下我的服务不需要关注连接字符串及其他构造器需求。我也从Public方法中移除了“ConnectToDatabase”方法,原因在于我们的服务不应该担心获取数据的预置条件。所有它需要知道的只是调用“GetAllPeople”方法然后得到数据。
切换到工厂模式
当写这篇文章的时候,我意识到说“是的我正在使用魔法般的依赖注入”并它可以正常工作并不是很有帮助。所以让我们快速的写出一个工厂模式来代替它:
class PersonRepositoryFactory { private string connectionString = "";//Get from app settings or similar. private int connectionTimeout = 123; public IPersonRepository Create() { return new PersonRepository(connectionString, connectionTimeout); } } class MyService { private readonly IPersonRepository _personRepository; public MyService() { _personRepository = new PersonRepositoryFactory().Create(); } }
很显然并不像使用依赖注入那般完美,并且还有一些不同的方式来对其进行修正,但是控制反转的主要点依然保留在那里。请注意仍然的,我们的服务不需要担心像是连接字符串那样的实现细节,并且其依然依赖于接口抽象。
下一步
至此,你已经到了C#/.NET 关于SOLID设计原则的结尾。现在你可以出去去你理想的公司应聘吧(希望真的这么简单就好了哈哈,译者注)!