Preface
In addition to MVC and MVVM, the singleton mode can be said to be another common design mode in iOS development. Whether it is UIKit or some popular three-party libraries, we can see singletons. And we developers themselves will subconsciously regard the code in these library as best practices and bring them into daily work, even if many people know that singletons have some obvious flaws.
In view of singleton defects, this article will introduce some ways to replace or modify singleton patterns to improve code quality.
Advantages of singleton
In addition to the imitation best practices mentioned above, there are certainly intrinsic reasons and reasons for the popularity of singletons. For example: a singleton object ensures that there is only one instance, which is conducive to our coordination of the overall behavior of the system. For example, in a server program, the configuration information of the server is stored in a file, and these configuration data are read uniformly by a singleton object, and then other objects in the service process obtain these configuration information through this singleton object. This method simplifies configuration management in complex environments. On the other hand, global single object also reduces unnecessary object creation and destruction actions and improves efficiency.
Here is a typical singleton pattern code:
class UserManager { static let shared = UserManager() private init() { // Singleton mode to prevent multiple instances } .... } extension UserManager { func logOut( ) { ... } func logIn( ) { ... } } class ProfileViewController: UIViewController { private lazy var nameLabel = UILabel() override func viewDidLoad() { () = ?.name } private func handleLogOutButtonTap() { () } }
Singleton defects
Although some advantages of singleton are mentioned above, this cannot mask some obvious shortcomings of singleton pattern:
- Globally Sharing Modified State: One of the side effects of singleton mode is that the amount of shared states may be modified during the life of the app, and these modifications may cause some location errors. What's worse is that these problems are very difficult to locate because of the scope and life cycle characteristics.
- Dependencies are unclear: Because singletons are very easy to access globally, this will be the code that turns our code into so-called spaghetti-style code. The relationship between singleton and user is unclear, and later maintenance is also very troublesome.
- Difficult to track tests: Because singleton pattern has the same life cycle as app and any modifications made during the life cycle, it is impossible to ensure that a clean instance is used for testing.
- Since there is no abstraction layer in the single interest pattern, it is very difficult to expand the singleton class.
- The singleton class has too heavy responsibilities, which to a certain extent violates the "single responsibility principle".
Dependency injection
Different from using singleton objects between, we can perform dependency injection in the initialization.
class ProfileViewController: UIViewController { private let user: User private let logOutService: LogOutService private lazy var nameLabel = UILabel() init(user: User, logOutService: LogOutService) { = user = logOutService (nibName: nil, bundle: nil) } override func viewDidLoad() { () = } private func handleLogOutButtonTap() { () } } class LogOutService { private let user: User private let networkService: NetworkService private let navigationService: NavigationService init(user: User, networkService: NetworkService, navigationService: NavigationService) { = user = networkService = navigationService } func logOut() { (.logout(user)) { [weak self] in self?.() } } }
The dependencies in the above code are obviously clearer than before, and are more convenient for later maintenance and writing test instances. In addition, through the LogOutService object, we extract certain specific services, avoiding the common bloat state in singletons.
Protocol transformation
It is obviously a very time-consuming and unreasonable thing to rewrite an application that is abused by a single case into the above dependency injection and service. Therefore, the following will introduce the method of gradually transforming singletons through protocols. The main method here is to rewrite the services provided by LogOutService above into a protocol:
protocol LogOutService { func logOut() } protocol NetworkService { func request(_ endpoint: Endpoint, completionHandler: @escaping () -> Void) } protocol NavigationService { func showLoginScreen() func showProfile(for user: User) ... }
After defining the protocol service, we let the original singleton follow the protocol. At this time, we can use singleton objects as services to inject dependency without modifying the original code implementation.
extension UserManager: LoginService, LogOutService {} extension AppDelegate: NavigationService { func showLoginScreen() { = [ LoginViewController( loginService: , navigationService: self ) ] } func showProfile(for user: User) { let viewController = ProfileViewController( user: user, logOutService: ) (viewController, animated: true) } }
Several methods to implement singleton mode in Swift3.0 - Dispatch_Once
It is very common to use the singleton mode in development. Normally, our idea is to use the dispatch_once API of GCD to write. However, in swift 3.0, Apple has abandoned this method, but don't worry, we can implement it in other ways.
Combining the characteristics of the swift language, the following writing methods are summarized:
- Ordinary creation method
- Static creation method
- struct creation method
- Implementation by adding extensions to DIspatchQueue
Notice:Here I hope that in addition to using it, you will also be able to call the corresponding method
1. Ordinary creation method
//MARK - : Singleton: Method 1 static let shareSingleOne = Single()
2. Static creation method
let single = Single() class Single: NSObject { //-MARK: Singleton: Method 2 class var sharedInstance2 : Single { return single } }
Creation method
//-MARK: Singleton: Method 3 static var shareInstance3:Single{ struct MyStatic{ static var instance :Single = Single() } return ; }
4. Implementation by adding extensions to DispatchQueue
public extension DispatchQueue { private static var _onceTracker = [String]() /** Executes a block of code, associated with a unique token, only once. The code is thread safe and will only execute the code once even in the presence of multithreaded calls. - parameter token: A unique reverse DNS style name such as .<name> or a GUID - parameter block: Block to execute once */ public class func once(token: String, block:()->Void) { objc_sync_enter(self) defer { objc_sync_exit(self) } if _onceTracker.contains(token) { return } _onceTracker.append(token) block() } }
Use the string token as the ID of once, and add a lock when executing once to avoid the problem of inaccurate judgment of tokens under multi-threads.
You can pass tokens when using
(token: "") { print( "Do This Once!" ) }
Or use UUID:
private let _onceToken = NSUUID().uuidString (token: _onceToken) { print( "Do This Once!" ) }
Conclusion
Singleton mode is not undesirable, for example, it is very suitable for log service, peripheral management and other scenarios. But most of the time, singleton pattern may increase the complexity of the system due to unclear dependencies and globally shared mutable states. If you use a lot of singleton patterns in your current code, I hope this article can help you to free yourself from it and build a more robust system.
Okay, the above is the entire content of this article. I hope that the content of this article has a certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.