+3

Using the factory pattern to avoid shared state

Shared state là một bug phổ biến của hầu hết các app. Nó xảy ra khi bạn có nhiều thành phần của system rely state có thể thay đổi. Bug thường xuất hiện từ việc xử lý không chính xác những thay đổi đối với shared state thông qua hệ thống. Trong bài viết này, ta sẽ xem xét làm sao để tránh shared state trong nhiều trường hợp, bằng cách sử dụng factory pattern để tạo ra các trường hợp tách biệt rõ ràng cho shared state.

The problem

Giả sử ứng dụng của chúng ta chứa một Request class được sử dụng để perform các request cho backend. Nó được implement như sau:

class Request {
    enum State {
        case pending
        case ongoing
        case completed(Result)
    }

    let url: URL
    let parameters: [String : String]
    fileprivate(set) var state = State.pending

    init(url: URL, parameters: [String : String] = [:]) {
        self.url = url
        self.parameters = parameters
    }
}

Ta có một DataLoader class, nơi mà một request được truyền tới để thực hiện.

dataLoader.perform(request) { result in
    // Handle result
}

Vấn đề ở đây là gì? Vì request không chỉ chứa thông tin về nơi và cách request được thực hiện, mà còn có cả trạng thái của nó. Vì vậy, chúng ta có thể tình cờ kết thúc sharing state một cách dễ dàng. Nếu một developer không quen thuộc với việc implement detail của request có thể giả định rằng đó là một giá trị đơn giản có thể được reuse, như sau:

class TodoListViewController: UIViewController {
    private let request = Request(url: .todoList)
    private let dataLoader = DataLoader()

    func loadItems() {
        dataLoader.perform(request) { [weak self] result in
            self?.render(result)
        }
    }
}

Với code ở trên, chúng ta có thể dễ dàng kết thúc trong các tình huống không xác định khi loaditems được gọi nhiều lần trước khi một pending request được hoàn thành (ví dụ như là một search control, hoặc một pull-to-refresh mechanism, đó có thể là kết quả của nhiều request). Vì tất cả các request được thực hiện sử dụng cùng một instance, ta có thể sẽ set lại trạng thái trạng thái của nó, làm cho DataLoader của ta bị confused. Một cách để giải quyết vấn đề này là tự động hủy bỏ mỗi pending request khi một request mới được thực hiện. Mặc dù có thể giải quyết vấn đề ngay tại đây nhưng nó sẽ gây ra những vấn đề khác làm cho API khó đoán trước và khó sử dụng.

Factory methods

Thay vào đó, ta sẽ sử dụng một kỹ thuật khác để giải quyết vấn đề trên, bằng cách sử dụng factory method. Chúng ta sẽ bắt đầu bằng cách giới thiệu một kiểu StatefulRequest là một lớp con của Request, và di chuyển thông tin state đến đó, như sau:

// Our Request class remains the same, minus the statefulness
class Request {
    let url: URL
    let parameters: [String : String]

    init(url: URL, parameters: [String : String] = [:]) {
        self.url = url
        self.parameters = parameters
    }
}

// We introduce a stateful type, which is private to our networking code
private class StatefulRequest: Request {
    enum State {
        case pending
        case ongoing
        case completed(Result)
    }

    var state = State.pending
}

Sau đó ta sẽ thêm một factory method cho request để cho phép chúng ta xây dựng một phiên bản stateful cho một passed request

private extension Request {
    func makeStateful() -> StatefulRequest {
        return StatefulRequest(url: url, parameters: parameters)
    }
}

Cuối cùng, khi DataLoader bắt đầu thực hiện một request, ta chỉ đơn giản chuyển nó về một StatefulRequest.

class DataLoader {
    func perform(_ request: Request) {
        perform(request.makeStateful())
    }

    private func perform(_ request: StatefulRequest) {
        // Actually perform the request
        ...
    }
}

Bởi luôn luôn tạo một instance mới cho mỗi request được thực hiện, chúng ta đã loại bỏ tất cả các khả năng state được chia sẻ

Factories

Tiếp theo, chúng ta hãy nhìn vào một tình huống khác mà factory pattern có thể được sử dụng để tránh shared state, sử dụng các loại factory. Giả sử chúng ta xây dựng một ứng dụng về movies, nơi user có thể list các bộ phim theo danh mục hoặc theo recommend. Chúng ta sẽ có một view controller cho mỗi trường hợp sử dụng, và cả hai đều sử dụng singleton MovieLoader để thực hiện request, như sau:

class CategoryViewController: UIViewController {
    // We paginate our view using section indexes, so that we
    // don't have to load all data at once
    func loadMovies(atSectionIndex sectionIndex: Int) {
        MovieLoader.shared.loadMovies(in: category, sectionIndex: sectionIndex) {
            [weak self] result in
            self?.render(result)
        }
    }
}

Những gì chúng ta đang đối mặt ở đây là kết quả của request đang được chia sẻ. Để giải quyết vấn đề này, chúng ta sẽ sử dụng một instance mới của MovieLoader cho mỗi view controller. Bằng cách này, ta chỉ đơn giản có mỗi loader có thể cancel tất cả pending request khi nó deallocated

class MovieLoader {
    deinit {
        cancelAllRequests()
    }
}

Tuy nhiên chúng ta không thực sự muốn tự tạo một instance mới mỗi khi chúng ta tạo một view controller mới. Có thể chúng ta cần chèn những attribute như cache, URL session, và những thứ khác mà chúng ta phải tiếp tục truyền qua view controller. Điều này khá lộn xộn, thay vào đó ta sẽ sử dụng factory để giải quyết vấn đề này

class MovieLoaderFactory {
    private let cache: Cache
    private let session: URLSession

    // We can have the factory contain references to underlying dependencies,
    // so that we don't have to expose those details to each view controller
    init(cache: Cache, session: URLSession) {
        self.cache = cache
        self.session = session
    }

    func makeLoader() -> MovieLoader {
        return MovieLoader(cache: cache, session: session)
    }
}

Sau đó ta sẽ tạo khởi tạo view controller với MovieLoaderFactory

class CategoryViewController: UIViewController {
    private let loaderFactory: MovieLoaderFactory
    private lazy var loader: MovieLoader = self.loaderFactory.makeLoader()

    init(loaderFactory: MovieLoaderFactory) {
        self.loaderFactory = loaderFactory
        super.init(nibName: nil, bundle: nil)
    }

    private func openRecommendations(forMovie movie: Movie) {
        let viewController = RecommendationsViewController(
            movie: movie,
            loaderFactory: loaderFactory
        )

        navigationController?.pushViewController(viewController, animated: true)
    }
}

Như bạn thấy ở trên, một lợi thế lớn của việc sử dụng factory pattern ở đây là chúng ta chỉ đơn giản có thể truyền factory cho bất kỳ view Controller tiếp theo nào. Như vậy chúng ta có thể tránh được tình trạng shared state. Hi vọng bài viết hữu ích đối với các bạn.


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í