«Я в курсе, что Singleton — это антипаттерн, но…». Разработчики часто говорят это при обсуждении кода. За годы обсуждений программисты достигли согласия в том, что паттерн «одиночка» — это плохо. Тем не менее, Apple и многие разработчики на языке программирования Swift продолжают использовать Singleton, как в своих приложениях, так и в фреймворках. Давайте же рассмотрим несколько полезных для избежания «одиночек» методов!
Почему анти-паттерн Singleton так популярен?
Для начала, давайте разберемся, от чего паттерн Singleton такой популярный, если большинство разработчиков согласны с тем, что его стоит избегать?
Ответ, вероятно, следует разбить на две части.
Во-первых, популярность шаблона «Одиночка», вероятно, обоснована тем, что Apple сами не пренебрегают его использованием. Многие сторонние разработчики, как и я сам, часто обращаются к коду Apple в поисках лучших примеров; почти всегда большинство разработчиков слепо повторяют код за Apple, как эталонный и неоспоримый.
Во-вторых — удобство. Singleton часто может играть роль «тега» для быстрого доступа к основным значениям или объектам, так как, в основном, они доступны из любой точки приложения. Просто посмотрите на пример. Есть класс AccountViewController, который отображает вошедшего по имени (nameLabel) пользователя, а так же выходит из аккаунта (signOutHandle) по нажатию на кнопку:
1 2 3 4 5 6 7 8 9 10 11 12 | class AccountViewController: UIViewController { private lazy var nameLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() nameLabel.text = AccountManager.shared.currentUser?.name } private func signOutHandle() { AccountManager.shared.logOut() } } |
В этом коде AccountManager (это как раз наш Singleton) отвечает за вход и выход пользователя из аккаунта на все приложение. Это и правда очень удобно, и это очень распространенная реализация подобной задачи. Так что же в этом шаблоне тогда плохо?
Почему использовать Singleton плохо?
При обсуждении шаблонов проектирования очень легко остаться всего лишь теоретиком. Хотя все таки приятно осознавать свой код «теоретически» правильным, использовать лучшие практики и принципы проектирования — но реальность часто не такая радужная, и появляется необходимость искать золотую середину.
Так какие же у Одиночек проблемы, почему их стоит избегать? Есть три основные причины. Давайте же рассмотрим их:
- Они глобальны, изменяемы, общедоступны. Состояние Одиночки автоматически распространяется на все приложение, и ошибки очень часто происходят именно тогда, когда их, одиночек, состояние изменяется.
- Отношения между одиночками и кодом , который зависит от них, как правило , не очень хорошо определены. Поскольку к одиночкам очень легко получить доступ — широкое их использование часто приводит к созданию трудноподдерживаемого «спагетти-кода», который не имеет четких разделений между объектами.
- Управление жизненным циклом может быть затруднено. Так как одиночки «живут» в течении всего периода жизни приложения, управлять ими бывает очень трудно. Так же код, который связан с одиночками, очень трудно тестировать, так как вы не можете начать «с чистого листа» в каждом тесте.
В нашем раннем примере AccountViewController мы уже видим признаки этих 3х проблем. Очень неочевидно, что зависит от AccountManager и есть ли у него доступ к currentUser. Так же мы не знаем, будет ли хоть что то кроме nil внутри currentUser, так как он опционален. Похоже, ошибка только и ждет момента, что бы случится.
Внедрение зависимости
Вместо того, что бы получать в AccountViewController доступ к его зависимостям через Singleton, давайте введем их в инициализаторе. Здесь мы ввели User и signOutService как не-опциональные объекты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class AccountViewController: UIViewController { private let user: User private let signOutService: SignOutService private lazy var nameLabel = UILabel() init(user: User, signOutService: SignOutService) { self.user = user self.signOutService = signOutService super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() nameLabel.text = user.name } private func signOutHandle() { signOutService.signOut() } } |
Теперь результат намного яснее и проще в управлении. Теперь наш код может смело рассчитывать на то, что его модель всегда на месте, и у него есть четкое API для взаимодействия с ней.
Сервисы
Как пример, давайте внимательно посмотрим, как SignOutService можно было бы реализовать. Он также использует введение зависимостей для своих основных функций, а так же обеспечивает хороший и четкий API только для того, что бы делать единственную вещь — разлогиниваться.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class SignOutService { 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 signOut() { networkService.request(.signout(user)) { [weak self] in self?.navigationService.showLoginScreen() } } } |
Модификация
Изменение функций одиночки, его улучшения и рефакторинг могут порой отнимать значительное количество времени. Но есть более простой способ — протоколы.
Вместо рефакторинга одиночек и создания новых классов, мы просто можем определить наши функции в качестве протоколов:
1 2 3 4 5 6 7 8 9 10 11 12 13 | protocol SignOutService { func signOut() } protocol NetworkService { func request(_ endpoint: Endpoint, completionHandler: @escaping () -> Void) } protocol NavigationService { func showLoginScreen() func showProfile(for user: User) ... } |
Теперь мы можем легко модифицировать функции нашего Singleton, подогнав их под соответствие нашим протоколам с новым функционалом. Во многих случаях вам даже не придется вносить какие либо изменения в реализацию.
Этот же метод может быть использован для модификации других основных объектов в приложении, которые могли бы быть использованы как Singleton. К примеру, можно использовать AppDelegate для NavigationService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | extension UserManager: LoginService, SignOutService {} extension AppDelegate: NavigationService { func showLoginScreen() { navigationController.viewControllers = [ LoginViewController( loginService: UserManager.shared, navigationService: self ) ] } func showProfile(for user: User) { let viewController = ProfileViewController( user: user, signOutService: UserManager.shared ) navigationController.pushViewController(viewController, animated: true) } } |
Теперь мы можем начать делать все наши ViewController-ы свободными от Одиночек, используя введение, сервис зависимостей, без необходимости делать огромный рефакторинг.
Паттерн Singleton, вывод
Паттерн Одиночка — не всегда плохой паттерн, н во многих ситуациях он создает множество проблем, которых можно избежать путем создания более четких отношений между вашими объектами с помощью введения зависимостей.
Если вы работаете над приложением, которое интенсивно использует Singleton-ы и вы видите некоторые ошибки, которые они обычно вызывают, надеюсь, этот пост вдохновит вас двигаться в сторону избавления от singleton-зависимости.
Это все здорово, но несколько оторвано от реальности.
Возьмем примитивный пример. У вас есть 5 контролеров и 4 модели.
Модели отдают разные данные в зависимости от того залогинен пользователь или нет, а контроллеры отображают разные данные в зависимости от от того залогинен пользователь или нет.
вы предлагаете в каждую модель и контроллер передавать пользователя?
А что если это разнородные модели и пользователи, которые уже на входе принимают некоторый надорванностях параметров? Добавлять еще один параметр?
Добавьте, со слов не очень понятно. Я разные архитектуры юзал, и нигде не было потребности в синглтоне. А такая вещь как логин юзера успешно реализуется силами аппделегата.