Using the factory pattern to avoid shared state
Bài đăng này đã không được cập nhật trong 6 năm
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