+2

Seperation of concern với protocol trong Swift

Separation of concerns (SOC) là một nguyên lý quan trọng và cơ bản trong việc thiết kế và kiến trúc phần mềm. Ý tưởng của nguyên lý này rất đơn giản, đó là mỗi một object chỉ nên biết và thực thi công việc của chính nó. Tuy nhiên thì mặc dù nguyên lý là đơn giản như vậy nhưng việc áp dụng nó thì lại không dễ dàng cho lắm. Vì vậy bài viết này sẽ giúp chúng ta có cái nhìn dễ dàng hơn trong việc áp dụng nguyên lý này, đặc biệt là trong Swift.

Giả dụ chúng ta đang code một ViewController (VC) có tên là ContactSearchViewController cho phép người dùng tìm kiếm contact được lưu local trong máy. Ứng dụng này sử dụng Core Data để lưu trữ dữ liệu, do đó phương pháp đầu tiên được dùng đó là nhét Core Data context vào trong VC như là một dependency:

class ContactSearchViewController: UIViewController {
    private let coreDataContext: NSManagedObjectContext

    init(coreDataContext: NSManagedObjectContext) {
    self.coreDataContext = coreDataContext
    super.init(nibName: nil, bundle: nil)
    }
}

Cách thực thi như ở trên là rất phổ biến tuy nhiên nó vẫn còn tồn tại một số vấn đề sau:

Ở đây VC được thông báo rằng app sử dụng Core Data. Tuy nhiên thì code của chúng ta sẽ trở nên kém linh hoạt hơn ( ví dụ tôi muốn sử dụng một database khác như Realm chẳng hạn), đồng thời cũng khiến việc test trở nên khó khăn hơn nếu như chúng ta sử dụng dependency injection) vì những class có liên quan tới hệ thống như NSManagedObjectContext rất khó để test

Vì VC sẽ hoàn toàn truy cập vào database của chúng ta, thế nên nó có thể làm bất cứ thứ gì, read hoặc write, và điều này là không cần thiết. VC sẽ chỉ search trong Contact, do đó chỉ cần cấp quyền read là đủ. Do đó code sẽ trở nên đẹp hơn nếu chúng ta tách hai quyền read và write ra và cấp cho từng đối tượng phù hợp. Khi gặp những vấn đề trên thì cách giải quyết phổ biến đó là sử dụng protocol. Thay vì để VC trực tiếp truy cập vào implement của database thì chúng ta có thể tạo các protocol chứa các API mà app cần để load, save đối tượng như sau:

protocol Database {
    func loadObjects<T: Model>(matching query: Query) -> [T]
    func loadObject<T: Model>(withID id: String) -> T?
    func save<T: Model>(_ object: T)
}

Bây giờ chúng ta có thẻ sử dụng protocol Database khi khởi tạo VC ContactSearchViewController, thay vì inject Core Data context:

class ContactSearchViewController: UIViewController {
    private let database: Database

    init(database: Database) {
        self.database = database
        super.init(nibName: nil, bundle: nil)
    }
}

Phương pháp ở trên giúp chúng ta giải quyết được vấn đề số 1, code đã trở nên linh hoạt và dễ test hơn. Bạn có thể tạo một mock bằng cách implement protocol Database, và trong trường hợp muốn chuyển sang loại database khác, chúng ta chỉ cần thêm các implementation mới vào trong protocol Database:

extension NSManagedObjectContext: Database {
    ...
}

extension Realm: Database {
    ...
}

extension MockedDatabase: Database {
    ...
}

extension UITestingDatabase: Database {
    ...
}

Vậy còn vấn đề số 2: tách biệt quyền read & write thì sao?

Thay vì sử dụng một protocol duy nhất cho tất cả các tính năng của database, chúng ta sẽ chia nó ra thành từng cụm nhỏ. Ví dụ như 2 protocol cho reading và writng :

protocol ReadableDatabase {
    func loadObjects<T: Model>(matching query: Query) -> [T]
    func loadObject<T: Model>(withID id: String) -> T?
}

protocol WritableDatabase {
    func save<T: Model>(_ object: T)
}

Phương pháp này được gọi là protocol composition, và cái hay của nó là rất dễ tùy biến, pha trộn các tính năng dựa trên yêu cầu của bạn, đồng thời việc test cũng trở nên dễ dàng hơn khi chúng ta tạo mock cho từng method nhỏ thay vì cả một protocol.

Với những trường hợp như database thì phương pháp này cho phép chúng ta có thêm quyền điều khiển luồng dữ liệu trong app. Ví dụ như giới hạn quyền truy cập database với những VC chỉ cần read bằng cách cho những VC đó adopt protocol read only:

class ContactSearchViewController: UIViewController {
    private let database: ReadableDatabase

    init(database: ReadableDatabase) {
        self.database = database
        super.init(nibName: nil, bundle: nil)
    }
}

Một điều hay nữa đó là chúng ta vẫn có thể dùng kiểu Database với typealias:

typealias Database = ReadableDatabase & WritableDatabase

Việc sử dụng protocol như trên rất hữu dụng khi chúng ta thiết kế API cho framework. Thay vì sử dụng một protocol lớn chứa rất nhiều function thì giờ đây chúng ta có thể cắt ra thành từng phần nhỏ và tùy ý sử dụng mà không sợ bị lộ chi tiết implementation. Đồng thời cũng làm cho hệ thống trở nên trong sáng và gọn gàng hơn.

Nguồn bài viết : https://www.swiftbysundell.com/posts/separation-of-concerns-using-protocols-in-swift


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí