Architecture

Design pattern yêu thích của mình: Composite

Trong bài viết này mình sẽ cùng các bạn tìm hiểu về Composite design pattern và một case study mình từng gặp trong thực tế nha.

Là một design pattern thuộc “họ” structural, cũng như những patterns khác cùng “họ” Composite pattern giúp cho lập trình viên mở rộng tính năng của phần mềm mà không cần đụng đến một dòng code nào ở phía client, hay nói cách khác, nó giúp chúng ta thoả mãn open-closed principle trong SOLID.

Definition

Composite design pattern là một structural pattern, nó cho phép chúng ta xây dựng một cấu trúc hình cây trong đó mỗi node trên cây đều có chung một Interface. Giúp cho client có thể điều khiển cả cây mà chỉ cần thông qua root node.

Structure

Chìa khoá của Composite là chúng ta cần phải có một Interface/Protocol đủ tốt để làm cầu nối giữa client và các components phía sau. Câu này hơi trừu tượng, mình sẽ giải thích rõ hơn ở phần case study nhé.

Case Study

Phần này mình sẽ đưa ra một case study mà mình đã từng gặp ở công ty cũ nha.

Requirement

Lần đó mình phụ trách làm ứng dụng cho một công ty, đại loại là nó đã có một website chuyên viết các bài về du lịch, đặc sản,… giờ họ muốn có một ứng dụng để hiển thị các bài viết đó trên điện thoại. Các Bài viết được chia thành các thể loại khác nhau: mới, yêu thích, phổ biến. Mỗi thể loại sẽ có một màn hình riêng để hiển thị, ngoài ra sẽ có một màn hình tổng hợp hiển thị tất cả các thể loại bài viết. API thì bên khách hàng nó sẽ lo.

Prototype

OK, requirement đã rõ, giờ sao nà !? Dễ mà nhỉ 😙 mỗi màn hình thì cứ gọi API lấy bài viết về rồi hiển thị lên thôi.

Why do bad things happen to good people ?

Buổi sáng “định mệnh” hôm đó, sếp đến và nói với mình rằng do website của khách hàng sử dụng wordpress nên nó không custom được API cho màn hình tổng hợp nên màn hình này mình sẽ phải gọi một lúc 3 API để lấy các bài viết thuộc các thể loại tương ứng.

Finding Solution

Cái khó là màn hình tổng hợp thôi chứ mấy màn hình kia không có gì. Nên mình sẽ nói về màn hình đó nha.
Mình bắt đầu với chìa khoá của chúng ta: một Interface “đủ tốt”. Đứng từ phía client, client không cần và cũng không nên biết các bài viết thuộc thể loại nào, client chỉ là thằng yêu cầu “Đưa tao danh sách bài viết để tao sử dụng, tao không quan tâm mày lấy từ đâu”. Từ suy nghĩ đó mình có protocol FeedLoader:

protocol FeedLoader {
   typealias Result = Swift.Result<[FeedItem], Error>  
   func load(completion: @escaping (Result) -> Void)
}

Client trong có thể là một ViewController, một Presenter, một ViewModel hoặc một Service nằm trong một module nào đó,… Mình sẽ lấy ví dụ client là một ViewController: AllCategoriesViewController.

class AllCategoriesViewController: UIViewController {  
  var loader: FeedLoader?
   
  func viewDidLoad(){    
      super.viewDidLoad()        
      loadFeed()  
  }    
  private func loadFeed(){    
      loader?.load { items in      
         // Do your work here    
      }  
  }  
}

Như mình đã nói, client yêu cầu lấy list bài viết và chỉ quan tâm đến kết quả, không quan tâm đến cách làm hay “Tell Don’t Ask” . OK, Giờ đến các implements của FeedLoader:

final class NewArticleLoader: FeedLoader {
    private let url: URL
    private let client: HTTPClient
    init(url: URL, client: HTTPClient) {
       self.url = url
    }
    func load(completion: @escaping (FeedLoader.Result) -> Void){
       client.get(from: url) {
          // map data to [FeedItem]
          // completion(.success(items))
       }
    }
}
final class FavoriteArticleLoader: FeedLoader {
    private let url: URL
    private let client: HTTPClient
    init(url: URL, client: HTTPClient) {
       self.url = url
    }
    func load(completion: @escaping (FeedLoader.Result) -> Void){
       client.get(from: url) {
          // map data to [FeedItem]
          // completion(.success(items))
       }
    }
}
final class TrendingArticleLoader: FeedLoader {
    private let url: URL
    private let client: HTTPClient
    init(url: URL, client: HTTPClient) {
       self.url = url
    }
    func load(completion: @escaping (FeedLoader.Result) -> Void){
       client.get(from: url) {
          // map data to [FeedItem]
          // completion(.success(items))
       }
    }
}

Nếu đọc đến đây và bạn bắt đầu nghĩ: “ủa rồi Composite pattern của tao đâu ? Bài này viết về nó mà phải hông ta ?!” thì các bạn hãy bình tĩnh, Composite của các bạn tới rồi đây 😇.

final class AllCategoriesArticleLoaderComposition: FeedLoader {
    private let newArticleLoader: NewArticleLoader
    private let favoriteArticleLoader: FavoriteArticleLoader
    private let trendingArticleLoader: TrendingArticleLoader
    private let group = DispatchGroup()
    init(newArticleLoader: NewArticleLoader,
         favoriteArticleLoader: FavoriteArticleLoader,
         trendingArticleLoader: TrendingArticleLoader) {
         self.newArticleLoader = newArticleLoader
         self.favoriteArticleLoader = favoriteArticleLoader
         self.trendingArticleLoader = trendingArticleLoader
    }
func load(completion: @escaping (FeedLoader.Result) -> Void){
       var items: [FeedItem] = []()
       [newArticleLoader, favoriteArticleLoader,         trendingArticleLoader].forEach {
           group.enter()
           $0.load { items in
              items.append(items)
              group.leave()
           }
           group.wait(timeout: .distantFuture)
       }
     ......
}

Implementation

let client = HTTPClient(session: URLSession.shared)

let newArticleURL = "https://an-url/new/"
let newArticleLoader = NewArticleLoader(url: newArticleURL, client: client)

let favoriteArticleURL = "https://an-url/favorite/"
let favoriteArticleLoader = FavoriteArticleLoader(url: favoriteArticleURL, client: client)

let trendingArticleURL = "https://an-url/trending/"
let trendingArticleLoader = TrendingArticleLoader(url:   trendingArticleURL, client: client)

let compositionLoader = AllTypesArticleLoaderComposition(newArticleLoader: newArticleLoader,
                                 favoriteArticleLoader: favoriteArticleLoader, 
                                 trendingArticleLoader: trendingArticleLoader)

let viewController = AllCategoriesViewController()
viewController.loader = compositionLoader

Conclusion

Để tóm cái váy lại thì các bạn cùng nhìn vào diagram này để xem chúng ta đã làm những gì nhé:

Bằng việc áp dụng Composite design pattern, compose các components khác nhau, bạn đã làm cho phần mềm flexible hơn rất nhiều, open-closed hơn rất nhiều. Bạn có thể thêm, sửa hoặc xoá chức năng mà không cần thay đổi một dòng code nào ở phía client 🥰.
Vậy là bài viết xin phép được dừng tại đây. Hy vọng các bạn đã học được chút gì đó từ bài viết.


Tài liệu tham khảo:
– 
Design Patterns: Elements of Reusable Object-Oriented Software (Gang of four)
– Tell don’t ask principle: https://martinfowler.com/bliki/TellDontAsk.html

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *