We seem to have reached a consensus that “singletons are great, but don’t abuse them.” But developers are still using it extensively in Both Apple and third-party Swift frameworks.
“Singletons are global states in sheep’s clothing” — Miško Hevery
Today we’ll look at the exact problem of using a single example and explore how to avoid abuse.
Why is this only so popular
Why is the singleton pattern so popular in iOS development? If most developers agree that their abuse should be avoided, why are they still being used so heavily?
I think there are two reasons. The first and most important reason is that it is used so often within Apple that people will copy and spread Apple’s practices as “best implementations”.
The second reason is that it does provide convenience. Singletons often provide a shortcut to core values and objects, since they can be accessed from almost anywhere. In this example, we want to display the name of the currently logged in user in the ProfileViewController and log out the user when the button is clicked:
class ProfileViewController: UIViewController {
private lazy var nameLabel = UILabel(a)override func viewDidLoad(a) {
super.viewDidLoad()
nameLabel.text = UserManager.shared.currentUser? .name }private func handleLogOutButtonTap(a) {
UserManager.shared.logOut()
}
}
Copy the code
It’s really convenient (and common!) to encapsulate user information and account processing in a UserManager singleton like the one above. . So what’s so bad about using this model? 🤔
What’s wrong with singletons?
When discussing issues such as patterns and architecture, it’s easy to fall into the trap of being too theoretical. It is good practice to make your code theoretically “correct” and to follow all best practices and principles. But the reality is often harsh, we need to depend on the situation.
So what specific problems do singletons usually cause, and why avoid them? I tend to avoid singletons for three main reasons:
- They are globally mutable shared states. Their state is automatically shared throughout the application, and errors often occur when the state is changed unexpectedly. Easy to fault.
- The relationship between singletons and the code that depends on them is often poorly defined. Because singletons are so convenient and easy to access, their widespread use often results in difficult maintenance, like “spaghetti code,” with no clear separation between objects. Implicit dependence.
- Managing its lifecycle can be tricky. Because singletons are active throughout the life cycle of an application, they can be difficult to manage, and they often rely on values that may be empty (optional). This also makes singleton-dependent code really hard to test, because you can’t easily start from the “initial state” in each test case. Not easy to test.
Translator added:
- Modules that are used only once can be replaced without singletons, which will always occupy memory, by maintaining member instance variables for the corresponding period.
- Singletons save user data. When logging out, be aware that dirty data may be generated if there are asynchronous storage tasks.
We can already see signs of these three problems in the previous ProfileViewController example. We cannot clear the judgment that the page is dependent on UserManager, and the singleton contains a possible empty object currentUser, which cannot be detected at compile time, that is, the page display may be currentUser nil. It looks like a Bug waiting to trigger 😬.
Dependency injection
Instead of letting the ProfileViewController access dependent data through singletons, inject them into its init methods. In this case, we will pass the user information and the LogOutService that can be used to perform the logout operation as required parameters:
class ProfileViewController: UIViewController {
private let user: User
private let logOutService: LogOutService
private lazy var nameLabel = UILabel(a)init(user: User, logOutService: LogOutService) {
self.user = user
self.logOutService = logOutService
super.init(nibName: nil, bundle: nil)}override func viewDidLoad(a) {
super.viewDidLoad()
nameLabel.text = user.name
}
private func handleLogOutButtonTap(a) {
logOutService.logOut()
}
}
Copy the code
The results are clearer and easier to manage. Our code now safely relies on the always present Model and has a clear API to perform the logout. In general, refactoring the various singletons and managers into clear, separate Services is a good way to create a clearer relationship between your App’s core objects.
Services
As an example, let’s take a closer look at implementing LogOutService. It also uses “dependencies” for its underlying services and provides a nice, well-defined API for doing just one thing: logging out.
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(a) {
networkService.request(.logout(user)) { [weak self] in
self? .navigationService.showLoginScreen() } } }Copy the code
refactoring
Moving from heavy use of singletons to code that makes full use of Services, dependency injection and local state can be difficult and time-consuming. Taking time aside, sometimes it may even require a huge refactoring.
Don’t worry, the “use protocols to remove singletons” approach can help.
Instead of refactoring all singletons at once and creating a new Service class, we can simply define a Service as a protocol, as follows:
protocol LogOutService {
func logOut(a)
}
protocol NetworkService {
func request(_ endpoint: Endpoint, completionHandler: @escaping (a) -> Void)}protocol NavigationService {
func showLoginScreen(a)
func showProfile(for user: User). }Copy the code
We can then easily remodel the singleton as a “service” by making it follow our new service agreement. In many cases, we don’t even need to make any implementation changes, just pass the singleton as a service.
The same technique can also be used to modify other core objects in our App that might have been used in a “singleton” way, such as using an App delegate for page navigation.
extension UserManager: LoginService.LogOutService {}
extension AppDelegate: NavigationService {
func showLoginScreen(a) {
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)}}Copy the code
Now we can start removing singletons from all view Controllers by using dependency injection and Services. Without much refactoring and modification 🎉!
conclusion
Singletons are not hopeless, but in many cases they do bring a host of problems. We can avoid these problems by creating more clearly defined relationships between objects and using dependency injection.
The author johnsundell
Music Coding
Reference:
Satanwoo. Making. IO / 2016/04/11 /…
objccn.io/issue-13-2/
www.swiftbysundell.com/articles/av…