Misuse of single cases

Source: Internet
Author: User

Original: http://objccn.io/issue-13-2/

Singleton is one of the most widely used core design patterns in the whole Cocoa. In fact, the Apple Developer Library takes the singleton as one of the "Cocoa core competencies". As an iOS developer, we often deal with a single case, such as UIApplication and NSFileManager so on. We've seen countless examples of using single examples in open source projects, Apple sample code, and StackOverflow. Xcode even has a default "Dispatch Once" code snippet that allows us to add a single example to the code very simply:

+ (instancetype)sharedInstance{    static dispatch_once_t once;    static id sharedInstance;    dispatch_once(&once, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance;}

For these reasons, the singleton is ubiquitous in iOS development. The problem is that they can easily be abused.

Although some people think that the singleton is ' anti-pattern ', ' devil ' and ' pathological liar ', I will not completely deny the benefits of the singleton, but will show some of the problems of using the singleton, so that the next time you can use the auto-completion function of the snippet, dispatch_once you will be able to evaluate its impact, think twice.

Global state

Most developers agree that using a globally mutable state is bad behavior. Too many states make the program difficult to understand and difficult to debug. Our object-oriented programmers have a lot to learn from functional programming in minimizing the state complexity of code.

@implementation SPMath {    NSUInteger _a;    NSUInteger _b;}- (NSUInteger)computeSum{    return _a + _b;}

In the implementation of this simple math library above, the programmer needs to computeSum set the instance variables correctly before the _a call _b . This has the following issues:

    1. computeSumIt does not explicitly declare the state it depends on in the form of a parameter _a _b . And just by looking at the function declaration, you know that the output of this function depends on which variables are different, and another developer must look at the specific implementation of the function in order to understand that the function relies on those variables. It is not good to hide dependencies.

    2. When the values are modified for the invocation to be computeSum prepared _a , the _b programmer needs to ensure that the modifications do not affect any other code that relies on the two variables for correctness. This is especially difficult in multi-threaded environments.

Compare the following code with the example above:

+ (NSUInteger)computeSumOf:(NSUInteger)a plus:(NSUInteger)b{ return a + b;}

Here, the dependency on the variable a and b is explicitly declared. We do not need to change the state of the instance variable in order to invoke this method. And we don't have to worry about calling this function to leave a lasting side effect. We can even declare this method as a class method, which tells the reader of the code that the method does not modify the state of any instance.

So what does this example have to do with a singleton? In the words of Miško hevery, "The single case is the global state of the sheepskin." A singleton can be used anywhere without the need to explicitly declare dependencies. Like variable   _a   and   _b   in   computesum   internal is used, Without being explicitly declared, any module of the program can call   [Spmysingleton sharedinstance]   and access this singleton. This means that any side effects associated with this single interaction can affect arbitrary code elsewhere in the program.

 @interface spsingleton: NSObject+ ( Instancetype) sharedinstance;-(Nsuinteger) badmutablestate;-(void) setBadMutableState :(Nsuinteger) badmutablestate;  @end  @implementation spconsumera-(void) somemethod{if ([[ Spsingleton Sharedinstance] badmutablestate]) {//...}}  @end  @implementation spconsumerb-(void) someothermethod{[[Spsingleton sharedInstance] Setbadmutablestate:0];}  @end             

In the example above, SPConsumerA and SPConsumerB is two completely independent modules. However SPConsumerB , you can affect the behavior by using the shared state provided by the Singleton SPConsumerA . This should only occur when consumer B explicitly references a and indicates the relationship between the two. The singleton is used here because of its global and multi-state characteristics, which leads to the implicit coupling between two seemingly completely unrelated modules.

Let's look at a more specific example and expose an additional problem that uses a globally mutable state. For example, we want to build a Web viewer in our app. To support this viewer, we built a simple URL cache:

@interface SPURLCache+ (SPCache *)sharedURLCache;- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;@end 

The developer began writing unit tests to make sure that the code would be expected in a number of different scenarios. First, he wrote a test case to ensure that the page Viewer can display error messages when the device is not connected. Then he wrote a test case to ensure that the Web Viewer was able to handle server errors correctly. In the end, he wrote a test case for the success of the case to ensure that the returned network content could be displayed correctly. The developer runs all of the test cases, and they all work as expected. Praise!

After a few months, these test cases begin to fail, even though the page viewer's code has never been changed since it was written! What the hell is going on?

It turns out that someone changed the order of the tests. The test case that was successfully processed was first run, and then the other two were run. The two test cases that handled the error are now successful, not the same as expected, because the URL cache is a single example that caches the response between different test cases.

The persistence state is the enemy of unit testing, because unit tests are effective when individual test cases are independent of each other. If the state is passed from one test case to another, it is related to the order in which the test cases are executed. The test cases with bugs, especially those that should not have been passed, are very bad things.

The life cycle of an object

Another key issue is the life cycle of a single case. When you add a singleton to a program, it is easy to assume that "there will always be only one instance". But in many of the IOS code I've seen, this assumption can be broken.

For example, let's say we're building an app where users can see their friends list. Each of their friends has a picture of personal information, and we want our app to be able to download and cache these images on the device. Using dispatch_once code Snippets, we can write a SPThumbnailCache single example:

@interface SPThumbnailCache : NSObject+ (instancetype)sharedThumbnailCache;- (void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userId;- (NSData *)cachedProfileImageForUserId:(NSString *)userId;@end

We continue to build our application and everything looks normal until one day we decide to implement the ' logout ' feature so that users can switch accounts in the app. Suddenly we find ourselves facing a nasty problem: the user-related state is stored in a global singleton. When the user logs off, we want to be able to clean up all the persistent state on the hard disk. Otherwise, we will leave these abandoned data on the user's device, wasting valuable hard disk space. We also want to be able to use a completely new instance of the new user when the user logs out and a new account is logged in SPThumbnailCache .

The problem is by defining an instance of a singleton that is considered "created once, permanently valid." You can think of some solutions to the above problems. Perhaps we can remove this singleton when the user is logged out:

static SPThumbnailCache *sharedThumbnailCache;+ (instancetype)sharedThumbnailCache{    if (!sharedThumbnailCache) {        sharedThumbnailCache = [[self alloc] init]; } return sharedThumbnailCache;}+ (void)tearDown{ // The SPThumbnailCache will clean up persistent states when deallocated sharedThumbnailCache = nil;}

This is an obvious abuse of the singleton pattern, but it can work, right?

We can certainly use this approach to solve it, but the price is too great. We cannot use simple dispatch_once schemes, and this scheme guarantees thread safety and [SPThumbnailCache sharedThumbnailCache] access to the same instance in all places where the call is made. Now we need to be very careful with the order in which the code that uses the thumbnail cache is executed. Let's say that when a user is performing a logout operation, some background tasks are performing an operation to save the picture to the cache:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{    [[SPThumbnailCache sharedThumbnailCache] cacheProfileImage:newImage forUserId:userId];});

We need to ensure that all background tasks tearDown must not be executed until they are completed. This ensures that the newImage data can be properly cleaned out. Or, we need to make sure that when the thumbnail cache is removed, the background cache task must be canceled. Otherwise, an instance of a new thumbnail cache will be created lazily, and the previous user's data ( newImage objects) will be stored in it.

Because it does not have a clear owner for a singleton instance (because the Singleton manages its own life cycle), it becomes very difficult to "close" a single case.

Analysis here, I hope you can realize that "this thumbnail cache never should be a singleton!" ”。 The problem is that the life cycle of an object may not be well thought out in the early stages of the project. For a specific example, Dropbox's IOS client once supported only one account login. It's been in this state for years, until one day we want to be able to support multiple user accounts at the same time (log in to both private and work accounts). All of a sudden, our previous hypothesis that "only one user can be logged in at the same time" is not true. If you assume that the life cycle of an object is consistent with the life cycle of your application, the flexible extension of your code is constrained, and sooner or later, when the demand for the product changes, you will pay the price for that assumption.

The lesson here is that the singleton should only be used to hold the global state and not be bound to any scope. If the scope of these states is shorter than the life cycle of a complete application, then this state should not be managed using a singleton. Using a single example to manage the state of user bindings is a bad taste of the code, and you should seriously reassess the design of your object graph.

Avoid using a single case

Since the singleton has so many disadvantages to the state of the local scope, how should we avoid using them?

Let's revisit the example above. Since the cache state of our thumbnail cache is tied to a specific user, let's define a user object:

  @interface spuser: nsobject @property ( nonatomic, readonly) Spthumbnailcache *thumbnailCache; @end  @implementation spuser-(instancetype) init{if (( Self = [super init]) {_thumbnailcache = [[Spthumbnailcache alloc] init]; //Initialize other user-specific state ...} return self;}  @end             

We now use an object as a model class for a authenticated user session, and we can store all the user-related state in this object. Now suppose we have a view controller to show the list of friends:

@interface SPFriendListViewController : UIViewController- (instancetype)initWithUser:(SPUser *)user;@end

We can explicitly pass the authenticated user object as a parameter to the view controller. This technique of passing dependencies to dependent objects is formally called Dependency injection, and it has many advantages:

    1. For SPFriendListViewController readers who read this header, it is clear that it will be displayed only if there are logged-in users.
    2. This SPFriendListViewController can be a strong reference to the user object as long as it is still in use. For example, for the previous example, we can save an image to the thumbnail cache in a background task like this:

      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{    [_user.thumbnailCache cacheProfileImage:newImage forUserId:userId];});

      Even if the background task is not completed, code elsewhere in the app can create and use a completely new SPUser object without blocking user interaction when the first instance is cleaned up.

For a more detailed explanation of the 2nd, let's draw an object graph before and after using dependency injection.

Suppose we SPFriendListViewController are the root view controller of the current window. When using a singleton, our object graph looks like this:

The view controller itself, as well as the list of custom image view, will interact with the sharedThumbnailCache resulting. When the user logs out, we want to clear the root view controller and exit to the login page:

The problem here is that the view controller of the buddy list may still be executing the code (because of the background operation) and may therefore still have some calls that are not executed sharedThumbnailCache .

Versus using a Dependency injection solution:

For simplicity, suppose the SPApplicationDelegate managed SPUser instances (in practice, you might hand over the management of these user states to another object to make your application delegate simplified). When a Friend List view controller is displayed, a user reference is passed in. This reference is also passed down to the profile image. Now, when the user is logged out, our object graph looks like this:

This object graph looks much like when you use a single example. So what's the difference?

The key issue is scope. In the case of a single case, it sharedThumbnailCache can still be accessed by any module of the program. If the user quickly login to a new account. The user also wants to see his buddy list, which means that you need to interact with the thumbnail cache again:

When a user logs in to a new account, we should be able to build and interact with the new, without having SPThumbnailCache to expend effort on destroying the old thumbnail cache. Based on the typical rules of object management, the old view controllers and the old thumbnail cache should be able to be cleaned up by themselves in the background delay. In short, we should isolate the state associated with user A and the state associated with user B:

Conclusion

I hope the content in this article is not as incomprehensible as fantasy fiction. People have complained about the abuse of a single case for years, and we all know that the global state is a bad thing. But in the world of IOS Development, the use of Singleton is so pervasive that we sometimes forget the lessons we learned over the years in other object-oriented programming.

The key point of all this is that we want to minimize the scope of the mutable state in object-oriented programming. But the singleton is standing on the opposite side because it allows the mutable state to be accessed from anywhere in the program. The next time you want to use a singleton, I hope you can think about using dependency injection as an alternative.

More articles under the topic #13

Original avoiding Singleton abuse

About the translator's breaking ground June

Strive to be a product-aware ape, slow down, to feel the beauty of life.

Http://blog.codingcoder.com

Misuse of single cases

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.