这是SOLID五大原则的第五篇学习笔记: 依赖反转原则 Dependency Inversion Principle(简写为DIP)。
SOLID原则每个字母对应一种原则。
- S是Single Responsibility Principle的缩写,即单一职责。
- O是Open-Closed Principle的缩写,即开闭原则。
- L是Liskov Substitution Principle的缩写,即里氏替换原则。
- I是Interface Segregation Principle的缩写,即接口隔离原则。
- D是Dependency Inversion Principle的缩写,即依赖反转原则。
如果你对SOLID原则还不了解,可以在单一职责中查看其相关介绍。
前几篇文章已经介绍过开闭原则、里氏替换原则,这两个原则有一定关联性,依赖倒置原则是严格使用OCP、LSP的结果。
Uncle Bob 对依赖反转原则的定义如下:
High level modules should not depend upon low level modules. Both should depend upon abstraction。
Abstractions should not depend upon details. Details should depend upon abstractions.
高级模块不应依赖底层模块,两者均应依赖抽象。
抽象不应依赖具体实现,实现应依赖抽象。
糟糕的设计指软件或工程的现状。有时,它的设计方式良好,但随着时间推移,变成了糟糕的设计。不能仅因为你有不同的设计方案,就认为当前设计方案糟糕。
对于哪种方案更好可能会有争议,但有以下特征的设计,可以归为糟糕的设计:
- 死板(rigidity):很难改变,一处修改可能影响其它部分。违背OCP会出现该问题,如扩展一处功能需要同步修改其它部分;模块依赖其它模块,而非依赖抽象,也会出现该问题。
- 脆弱(fragility):很难修改,一处修改可能破坏软件其它部分。违背开闭原则会导致该问题。修改一处功能需要同步修改其它部分,甚至编译时期不能发现需要同步修改的模块;模块依赖其它模块,没有依赖抽象,也会出现该问题。
- 耦合(Coupling):代码耦合在一起,很难重用、提取代码。模块依赖其它模块,没有依赖抽象会导致耦合。
可以看到,模块依赖其它模块,没有依赖抽象会导致上述问题。
你可能会想,为什么讨论这些内容?其与DIP有什么关系?因为DIP与良好设计成正比。即遵守DIP后,软件不会死板、脆弱、耦合在一起。
正如前面说到的,依赖倒置原则是由开闭原则和里氏替换原则组合使用产生的,因为抽象可以解决违背OCP、LSP而出现的问题。OCP、LSP两篇文章中的四个示例都是通过抽象(protocol)来解决的。因此,遵守DIP的过程,也是遵守OCP、LSP的过程。
有一个Product
的结构体,用来表示商品。
struct Product {
let name: String
let cost: Int
let image: UIImage
}
下面是从服务端获取商品列表的API:
final class Network {
private let urlSession = URLSession(configuration: .default)
func getProducts(for userId: String, completion: @escaping ([Product]) -> Void) {
guard
let url = URL(string: "baseUrl/products/user/\(userId)")
else {
completion([])
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
urlSession.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
completion([Product(name: "Just an example", cost: 1000, image: UIImage())])
}
}
}
}
因为这里的重点不在网络请求,我们只简单实现了请求功能。
最后,在ViewController
中通过网络拉取数据,展示商品:
class ViewController: UIViewController {
private let network: Network
private var products: [Product]
private let userId: String = "user-id"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getProducts()
}
required init?(coder: NSCoder) {
fatalError()
}
override func loadView() {
view = UIView()
}
init(network: Network, products: [Product]) {
self.network = network
self.products = products
super.init(nibName: nil, bundle: nil)
}
private func getProducts() {
network.getProducts(for: userId) { [weak self] products in
self?.products = products
}
}
}
getProducts()
通过Network
拉取数据。因为这里只是用来演示如何会违背DIP,并没有使用view展示拉取到的数据。
上述实现方案有以下问题,如果修改了Network
实体,其会影响ViewController
。因为ViewController
依赖了Network
。单一职责、开闭原则、里氏替换原则和接口隔离原则都有同样问题。
为了遵守依赖倒置原则,不同层级之间依赖抽象,具体实现也依赖抽象的接口。
因此,创建了ProductProtocol
和NetworkProtocol
:
protocol ProductProtocol {
var name: String { get }
var cost: Int { get }
var image: UIImage { get }
}
// Abstraction
protocol NetworkProtocol {
func getProducts(for userId: String, completion: @escaping ([ProductProtocol]) -> Void)
}
NetworkProtocol
协议中的getProducts(for:completion:)
方法依赖抽象的ProductProtocol
,而非依赖具体实现类。Product
实体也遵守了ProductProtocol
:
struct Product: ProductProtocol {
let name: String
let cost: Int
let image: UIImage
}
Network
实体也遵守了NetworkProtocol
协议。如下所示:
final class Network: NetworkProtocol {
private let urlSession = URLSession(configuration: .default)
func getProducts(for userId: String, completion: @escaping ([ProductProtocol]) -> Void) {
guard
let url = URL(string: "baseURL/products/user/\(userId)")
else {
completion([])
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
urlSession.dataTask(with: request) { (data, response, error) in
completion([Product(name: "Just an example", cost: 1000, image: UIImage())])
}
}
}
最后,修改ViewController
的属性,从依赖具体实体Network
、Product
改为依赖抽象的实体NetworkProtocol
、ProductProtocol
。
class ViewController: UIViewController {
private let network: NetworkProtocol // Abstraction dependency
private var products: [ProductProtocol] // Abstraction dependency
private let userId: String = "user-id"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getProducts()
}
required init?(coder: NSCoder) {
fatalError()
}
override func loadView() {
view = UIView()
}
init(network: NetworkProtocol, products: [ProductProtocol]) { // Abstraction dependency
self.network = network
self.products = products
super.init(nibName: nil, bundle: nil)
}
private func getProducts() {
network.getProducts(for: userId) { [weak self] products in
self?.products = products
}
}
}
通过上述修改,该示例已经遵守依赖反转原则。高级实现(ViewController)和底层实现(Network)均依赖抽象,Network
和ViewController
的具体实现部分也依赖协议,没有依赖具体类。
依赖抽象除有助于良好的设计,还有以下优点:
- 可以将依赖从旧实现替换为新API实现。例如,由于
Network
依赖了协议,我们可以很方便的更换底层实现,只需遵守NetworkProtocol
即可。 - 依赖抽象更容易进行单元测试。
参考资料: