SOLID - Part 5: Nguyên lý D - Dependency Inversion Principle (DIP)

1. Đặt vấn đề: 

Đây là bài cuối cùng của mình về SOLID. Nếu bạn vô tình đọc part 5 này thì nên đọc từ đầu, hoặc quay lại để ôn cũng được. Nhắc 1 chút về Dependency (coupling), khi một module giả sử tên là Big (class/ function) sử dụng 1 module khác tên là Small. Thì Small chính là 1 dependency của Big. Ví dụ: 

Trong MapViewController, nó dùng MapKit để hiển thị map -> MapKit là dependency của MapViewController. Tóm lại Dependency là 1 thành phần riêng biệt, được "inject" vào 1 module khác để hỗ trợ, support 1 công việc nào đó.

2. Dependency Inversion Principle (DIP)

a. Nguyên tắc và nguyên nhân sử dụng

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

Các high-level modules không nên phụ thuộc vào các low-level modules, cả 2 nên phụ thuộc vào abstraction.
Abstraction không nên phụ thuộc vào details, mà detail nên phụ thuộc vào abstraction.

High-level modules là các module sử dụng các low-level modules để làm 1 việc gì đó mà nó cần. Không có định nghĩa cố định, nếu nó dùng người khác nó là high-level, nếu người khác dùng nó, nó là low-level. Và low-level modules chính là 1 dependency của high-level modules.

b. Áp dụng

Các bạn xem đoạn code sau: 

class NetworkService {
  func fetchData() {
  // TODO: - calling API here
  }
}

class Usage {
  func getData(){
    let networkService = NetworkService()
    networkService.fetchData()
  }
}

Ở đây, Usage (high-level) bị phụ thuộc vào NetworkService (low-level). Nó có nhược điểm là getData() phụ thuộc vào class NetworkService -> không reuse được -> Usage không reuse được. Nếu NetworkService class thay đổi thì bắt buộc ta phải vào Usage để chỉnh sửa lại. Nếu lớp Usage được dùng ở nhiều nơi thì toi. Ví dụ như sau: 

class View01 {
    let usage: Usage = Usage()
   
    func getData() {
        usage.getData()
    }
}
 
class View02 {
    let usage: Usage = Usage()
   
    func getData() {
        usage.getData()
    }
}

Theo như nguyên lý, Usage và NetworkService nên phụ thuộc chung 1 protocol như sau: 

protocol Service {
  func fetchData()
}

class NetworkService: Service {
  func fetchData() {
  // TODO: - calling API here
  }
}

class Usage {
  let service: Service
  init(service: Service) {
    self.service = service
  }
  func getData() {
    service.fetchData()
  }
}

class View01 {
  let usage: Usage = Usage(service: NetworkService)
  func getDataForUI() {
    usage.getData()
  }
}
 
class View02 {
  let usage: Usage = Usage(service: NetworkService)
  func getDataForUI() {
    usage.getData()
  }
}

Các bước thực hiện như sau: 

+ Bước 1: Tạo 1 protocol và định nghĩa các func

+ Bước 2: Các low-level module sẽ implement protocol đó

+ Bước 3: Các high-level module dùng protocol cho nghiệp vụ của mình

+ Bước 4: Sử dụng khi sử dụng high-level module ở đâu đó, ta truy xuất low-level module ở những chỗ định nghĩa protocol. 

Nó giải quyết được các vấn đề sau:

+ High-level và low-level không còn phụ thuộc trực tiếp vào nhau nữa

+ Các low-level module linh hoạt và dễ thay đổi hơn

+ Các func ở high-level không lo việc thay đổi bên trong func. Vì nó truy xuất thông qua protocol.

Bạn sẽ thấy cái hay nhất ở đây, đó là khi mà có những yêu cầu thay đổi toàn bộ low-level, tức là gỉa sử bây giờ muốn dùng 1 networkservice khác (URLSession thay cho Alamofire chẳng hạn) thì 

protocol Service {
  func fetchData()
}

class NetworkService02: Service {
  func fetchData() {
  // TODO: - use URLSession to call API here
  }
}

class View01 {
  let usage: Usage = Usage(service: NetworkService02)
  func getDataForUI() {
    usage.getData()
  }
}

 Bạn có thấy thiếu thiếu cái gì đó không ? Đó là ta chẳng có đụng chạm gì đến thằng Usage (là lớp useCase đó cả). Tóm lại, ta thấy đoạn code ta hiểu nôm na như sau: Khách hàng sử dụng Usage của mình, mình cung cấp cho Khách hàng các gói Service cơ bản: Service01, Service02, Service03 mà mình đã thuê bên thứ 3. Cả 3 gói đều thoả mãn các chức năng cơ bản của Service. Khách hàng ưng dùng gói nào thì dùng, còn bên trong chúng sẽ dụng công nghệ 01, 02, 03 như thế nào Khách hàng không cần quan tâm. Lỡ có thay đổi chỉnh sửa gì trong các gói đó, cả mình với Khách hàng đều không quan tâm. Bên thứ 3 là Service sẽ lo vụ đó.

Giả sử không dùng protocol thì câu chuyện là như sau: Khách hàng sử dụng Usage của mình, và mình tự làm luôn phần Service. Lỡ có hư hỏng chi phần service thì mình phải tự chỉnh sửa từ Service của mình, đến Usage của mình và nơi mà khách hàng dùng Usage của mình. 

3. Tổng kết

Ai cũng có xu hướng code theo kiểu top-down, nghĩa là một module to, chia thành các module nhỏ, rồi nhỏ hơn. Nhìn vào đó ta có cái nhìn tổng quát về module to quản lý và thực thi các module nhỏ nhưng lại làm cho module to phụ thuộc nhiều vào nhỏ, rồi nhỏ hơn. Chữ ngược (inversion) trong nguyên lý nói lên ý chính của vấn đề này tức là ngược tư duy từ thấp lên trên. Tức là đẳng cấp cao, có kinh nghiệm, suy ngẫm trước rồi mới code đó.

DIP vận dụng các protocol. Giúp cho việc phân tách, tư duy để code dễ hơn một chút. Đến đây mình xin kết thúc loạt bài về SOLID của mình. Cám ơn các bạn đã đồng hành với mình đến tận quãng đường này. 

Bình luận
* Các email sẽ không được công bố trên trang web.
I BUILT MY SITE FOR FREE USING