Skip to content

Latest commit

 

History

History
231 lines (172 loc) · 9.13 KB

依赖反转原则.md

File metadata and controls

231 lines (172 loc) · 9.13 KB

这是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原则还不了解,可以在单一职责中查看其相关介绍。

1. 依赖反转原则

前几篇文章已经介绍过开闭原则里氏替换原则,这两个原则有一定关联性,依赖倒置原则是严格使用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.

高级模块不应依赖底层模块,两者均应依赖抽象。

抽象不应依赖具体实现,实现应依赖抽象。

1.1 糟糕的设计

糟糕的设计指软件或工程的现状。有时,它的设计方式良好,但随着时间推移,变成了糟糕的设计。不能仅因为你有不同的设计方案,就认为当前设计方案糟糕。

对于哪种方案更好可能会有争议,但有以下特征的设计,可以归为糟糕的设计:

  • 死板(rigidity):很难改变,一处修改可能影响其它部分。违背OCP会出现该问题,如扩展一处功能需要同步修改其它部分;模块依赖其它模块,而非依赖抽象,也会出现该问题。
  • 脆弱(fragility):很难修改,一处修改可能破坏软件其它部分。违背开闭原则会导致该问题。修改一处功能需要同步修改其它部分,甚至编译时期不能发现需要同步修改的模块;模块依赖其它模块,没有依赖抽象,也会出现该问题。
  • 耦合(Coupling):代码耦合在一起,很难重用、提取代码。模块依赖其它模块,没有依赖抽象会导致耦合。

可以看到,模块依赖其它模块,没有依赖抽象会导致上述问题。

你可能会想,为什么讨论这些内容?其与DIP有什么关系?因为DIP与良好设计成正比。即遵守DIP后,软件不会死板、脆弱、耦合在一起。

1.2 DIP 与 SOLID 中其它原则的关系

正如前面说到的,依赖倒置原则是由开闭原则和里氏替换原则组合使用产生的,因为抽象可以解决违背OCP、LSP而出现的问题。OCP、LSP两篇文章中的四个示例都是通过抽象(protocol)来解决的。因此,遵守DIP的过程,也是遵守OCP、LSP的过程。

查看下面示例前,建议先查看开闭原则里氏替换原则,了解如何通过protocol进行抽象。

2. 依赖倒置原则示例

有一个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。单一职责、开闭原则、里氏替换原则和接口隔离原则都有同样问题。

3. 遵守依赖倒置原则

为了遵守依赖倒置原则,不同层级之间依赖抽象,具体实现也依赖抽象的接口。

因此,创建了ProductProtocolNetworkProtocol

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的属性,从依赖具体实体NetworkProduct改为依赖抽象的实体NetworkProtocolProductProtocol

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)均依赖抽象,NetworkViewController的具体实现部分也依赖协议,没有依赖具体类。

总结

依赖抽象除有助于良好的设计,还有以下优点:

  • 可以将依赖从旧实现替换为新API实现。例如,由于Network依赖了协议,我们可以很方便的更换底层实现,只需遵守NetworkProtocol即可。
  • 依赖抽象更容易进行单元测试。

参考资料:

  1. Dependency Inversion Principle in Swift