0

Avoiding singletons in Swift

Có vẻ như có sự đồng thuận của cộng đồng lập trình IOS cho rằng singletons là không tốt, tuy nhiên cả Apple và các nhà phát triển Swift bên thứ ba tiếp tục sử dụng chúng cả trong nội bộ ứng dụng và trong các framework.

Why are singletons so popular?

Trước tiên, hãy bắt đầu bằng cách hỏi tại sao singletons lại phổ biến như vậy? Nếu hầu hết các nhà phát triển đồng ý rằng nên tránh sử dụng nó, vậy tại sao nó lại phổ biến?

Tôi nghĩ rằng câu trả lời có hai phần. Thứ nhất, tôi nghĩ rằng một lý do chính mà mô hình Singleton đã được sử dụng rất nhiều khi viết ứng dụng cho nền tảng của Apple, là Apple tự sử dụng nó rất nhiều. Là các nhà phát triển bên thứ ba, chúng ta thường hướng đến Apple để xác định "thực tiễn tốt nhất" cho nền tảng của họ, và bất kỳ mô hình nào mà họ thường sử dụng thường trở nên phổ biến rộng rãi trong cộng đồng.

Phần thứ hai của hỏi, tôi nghĩ, là tiện lợi. Singletons thường có thể hoạt động như một phím tắt để truy cập các giá trị cốt lõi hoặc các đối tượng nhất định, vì chúng chủ yếu có thể truy cập từ bất cứ đâu. Chỉ cần nhìn vào ví dụ này, nơi chúng tôi muốn hiển thị tên người dùng hiện đang đăng nhập trong một ProfileViewController, cũng như để đăng nhập người dùng khi một nút được gõ:

class ProfileViewController: UIViewController {
    private lazy var nameLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = UserManager.shared.currentUser?.name
    }

    private func handleLogOutButtonTap() {
        UserManager.shared.logOut()
    }
}

Làm một cái gì đó giống như các tính năng xử lý tài khoản người dùng & tài khoản ở trên trong một UserManager singleton , thực sự là rất tiện lợi (và rất phổ biến). Vì vậy, những gì chính xác là như vậy xấu về việc sử dụng mô hình này?

What's so bad about singletons?

Khi thảo luận về những thứ như mô hình và kiến trúc, thật dễ dàng để rơi vào ma trận của một chút lý thuyết. Trong khi thật lý tưởng để có mã của chúng tôi về lý thuyết là "chính xác" và tuân theo tất cả các nguyên tắc và thực tiễn tốt nhất - thực tế thường xảy ra và chúng ta cần tìm một số cơ sở trung gian.

Vì vậy, những vấn đề cụ thể là singletons thường gây ra, và tại sao họ nên tránh? Ba lý do chính tại sao tôi có khuynh hướng tránh singleton là:

  • Chúng là trạng thái chia sẻ global. Trạng thái của chúng được tự động chia sẻ trên toàn bộ ứng dụng và lỗi thường có thể bắt đầu xảy ra khi trạng thái đó thay đổi bất ngờ.
  • Mối quan hệ giữa các singleton và mã phụ thuộc vào chúng thường không được định nghĩa rất rõ ràng. Vì singletons rất tiện lợi và dễ truy cập - sử dụng chúng rộng rãi thường dẫn đến rất khó để duy trì "mã spaghetti" mà không có sự phân cách rõ ràng giữa các đối tượng.
  • Quản lý vòng đời của họ có thể rất phức tạp. Vì singletons sống trong suốt tuổi thọ của một ứng dụng, việc quản lý chúng có thể thực sự khó khăn, và thường phải dựa vào các lựa chọn để theo dõi các giá trị. Điều này cũng làm cho mã mà dựa vào singletons thực sự khó kiểm tra, vì bạn không thể dễ dàng bắt đầu từ một "slate sạch" trong mỗi trường hợp thử nghiệm. Trong ví dụ ProfileViewController của chúng ta từ trước, chúng ta đã có thể thấy dấu hiệu của 3 vấn đề này. Không rõ ràng là nó phụ thuộc vào UserManager và nó phải truy cập currentUser như là một tùy chọn bởi vì chúng ta không có cách nào để nhận được một bảo đảm thời gian biên dịch rằng dữ liệu thực sự có ở thời điểm trình điều khiển chế độ xem được trình bày. Âm thanh như lỗi chỉ chờ đợi để xảy ra !

Dependency injection

Thay vì sử dụng ProfileViewController để truy cập vào các tài sản của nó như là các singleton, chúng tôi sẽ thay thế chúng trong initializer. Ở đây chúng tôi đang chỉ đến người dùng như non-optional, cũng như một LogOutService có thể được sử dụng để thực hiện hành động đăng xuất:

class ProfileViewController: UIViewController {
    private let user: User
    private let logOutService: LogOutService
    private lazy var nameLabel = UILabel()

    init(user: User, logOutService: LogOutService) {
        self.user = user
        self.logOutService = logOutService
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = user.name
    }

    private func handleLogOutButtonTap() {
        logOutService.logOut()
    }
}

Services

Ví dụ, chúng ta hãy xem xét kỹ hơn cách LogOutService có thể được thực hiện như thế nào. Nó cũng sử để chỉ đến các dịch vụ cơ bản của nó, và cung cấp một API tốt đẹp và được xác định rõ ràng để chỉ làm một việc duy nhất - đăng xuất.

class LogOutService {
    private let user: User
    private let networkService: NetworkService
    private let navigationService: NavigationService

    init(user: User,
         networkService: NetworkService,
         navigationService: NavigationService) {
        self.user = user
        self.networkService = networkService
        self.navigationService = navigationService
    }

    func logOut() {
        networkService.request(.logout(user)) { [weak self] in
            self?.navigationService.showLoginScreen()
        }
    }
}

Retrofitting

Đi từ thiết lập sử dụng nhiều đơn vị thành một trong những sử dụng đầy đủ các dịch vụ, trạng thái cục bộ có thể thực sự phức tạp và tốn thời gian. Cũng có thể thật sự khó khăn để biện minh cho việc dành thời gian, và đôi khi có thể đòi hỏi một refactor lớn thậm chí có thể được.

Thay vì sắp xếp tất cả các singlelet của chúng tôi ngay lập tức và tạo ra các lớp dịch vụ mới, chúng ta có thể chỉ định các dịch vụ của chúng ta như các protocol, như sau:

protocol LogOutService {
    func logOut()
}

protocol NetworkService {
    func request(_ endpoint: Endpoint, completionHandler: @escaping () -> Void)
}

protocol NavigationService {
    func showLoginScreen()
    func showProfile(for user: User)
    ...
}

Chúng ta có thể dễ dàng "retroft" các singletons của chúng tôi như là các dịch vụ bằng cách làm cho chúng phù hợp với các giao thức dịch vụ mới của chúng tôi. Trong nhiều trường hợp, chúng ta thậm chí không cần phải thực hiện bất kỳ thay đổi nào về triển khai thực hiện và chỉ đơn giản là có thể truyền dụ như là một dịch vụ.

Kỹ thuật tương tự cũng có thể được sử dụng để trang bị thêm các đối tượng cốt lõi khác trong ứng dụng của chúng tôi mà chúng tôi có thể đã sử dụng theo cách "singleton-like", chẳng hạn như sử dụng AppDelegate để điều hướng.

extension UserManager: LoginService, LogOutService {}

extension AppDelegate: NavigationService {
    func showLoginScreen() {
        navigationController.viewControllers = [
            LoginViewController(
                loginService: UserManager.shared,
                navigationService: self
            )
        ]
    }

    func showProfile(for user: User) {
        let viewController = ProfileViewController(
            user: user,
            logOutService: UserManager.shared
        )

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

Bây giờ chúng ta có thể bắt đầu làm cho tất cả bộ điều khiển chế độ xem của chúng tôi "singleton free" bằng cách sử dùng dependency injection & services mà không cần phải thực hiện các điều khoản chính xác và viết lại lên phía trước ! Sau đó, chúng tôi có thể bắt đầu thay thế đơn vị riêng lẻ của chúng tôi bằng các dịch vụ và các loại API khác một cách lần lượt.

Conclusion

Singletons không xấu, nhưng trong nhiều trường hợp, chúng đi kèm với một loạt các vấn đề có thể tránh được bằng cách tạo ra các mối quan hệ tốt hơn được xác định giữa các đối tượng của bạn và bằng cách sử dụng dependency injection.

Nếu bạn đang làm việc trên một ứng dụng hiện đang sử dụng nhiều singleton và bạn đã trải qua một số lỗi mà họ thường gây ra, hy vọng bài đăng này đã cho bạn một số cảm hứng như thế nào bạn có thể bắt đầu di chuyển ra khỏi chúng trong một non- cách gây rối.

Source

https://www.swiftbysundell.com/posts/avoiding-singletons-in-swift


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.