SoFunction
Updated on 2025-04-12

Detailed explanation of different poses of iOS creating objects

Preface

When writing iOS code, there are some considerations in how to get a new object out. Using different postures to create objects will have subtle effects on later maintenance.

init Create

In a previous article analyzing iOS code coupling, it was mentioned that when we assign values ​​to an object's property, initializing property through the init method will make our code more reliable.

Some people define class with property as follows:

@interface User : NSObject
@property (nonatomic, strong) NSNumber*     userID;
@end

When using it, the following is:

User* user = [[User alloc] init];
 = @1000;

Especially when defining a model, it is easy to write such codes, first init, and then assign values ​​to the property one by one. The problem with this kind of code is that property is writable to the outside and property is in a state that may change at any time. Many previous articles have emphasized the importance of immutable. Similarly, for a class, we should also give priority to designing immutable.

initWith Created

If you set the property to readonly, or the property is not exposed, and the property assignment is initialized through initWith, you can get a class definition with immutable. The specific example code above is as follows:

//
@interface User : NSObject
@property (nonatomic, strong, readonly) NSNumber* userID;
- (instancetype)initWithUserID:(NSNumber*)uid;
@end
 
//
@implementation User
- (instancetype)initWithUserID:(NSNumber*)uid {
 self = [super init];
 if (!self) {
  return nil;
 }
 _userID = uid;
 return self;
}
@end

The userID is readonly in the .h file, and the userID has only one chance of being assigned, that is, in the initWith method of the User. The advantage of this method is that once the User object is created, it is in the immutable state. The property is not modified and safe and reliable.

Designated initializer

In order to facilitate developers to use the init method, Apple introduced a pattern called designed initializer. It is mainly used to manage scenarios where a class has multiple properties that need to be assigned. For example, our User class above:

@interface User : NSObject
@property (nonatomic, strong, readonly) NSNumber*     userID;
@property (nonatomic, strong, readonly) NSString*     userName;
@property (nonatomic, strong, readonly) NSString*     signature;
@end

Some scenarios require initialization of userID and userName, while some scenarios require initialization of userID and signature, so we need to provide multiple initWith methods for use in different scenarios. In order to manage the initWith method, Apple divides the init method into two types: designed initializer and convenience initializer (also called secondary initializer).

There is only one designed initializer, which will provide an initial value for each property in the class, and is the most complete initWith method. There can be many convenience initializers, which can choose only the initialization part of the property. The convenience initializer will call the designed initializer, so the designed initializer can also be called the final initializer.

No matter what type of class we define, it is a good habit to assign an initial value to each property in the class, which can avoid some unexpected bugs, which is also an important responsibility of the designed initializer.

In actual projects, the number of properties of a class may increase with the growth of the business, and the final result is that more and more convenience initializers will be generated. If the above User class is 3 properties, in extreme cases, there can be up to 7 init methods. When Peak was reading the code, he did see some classes that define a series of neatly arranged init methods. Although the code looks standardized, it seems long-lasting, and every time you need to search for the appropriate init method with your naked eyes.

In fact, we can use another posture to init our object.

Builder pattern

Initially, when I was learning Android, I found that this builder pattern can also be used to build objects, and it can solve the problem of too many init methods that are difficult to manage. Let’s first look at how to implement it. As the name suggests, builder pattern uses another class named builder to create our target object, or the above example, the code is as follows:

//
@interface UserBuilder : NSObject
@property (nonatomic, strong, readonly) NSNumber*     userID;
@property (nonatomic, strong, readonly) NSString*     userName;
@property (nonatomic, strong, readonly) NSString*     signature;

- (UserBuilder*)userID:(NSNumber*)userID;
- (UserBuilder*)userName:(NSString*)userName;
- (UserBuilder*)signature:(NSString*)signature;
@end

//
@implementation UserBuilder
- (UserBuilder*)userID:(NSNumber*)userID {
 _userID = userID;
 return self;
}
- (UserBuilder*)userName:(NSString*)userName {
 _userName = userName;
 return self;
}
- (UserBuilder*)signature:(NSString*)signature {
 _signature = signature;
 return self;
}
@end

Next, the User's init method gets the initial value of the property from the Builder:

//
@interface User : NSObject
@property (nonatomic, strong, readonly) NSNumber*     userID;
@property (nonatomic, strong, readonly) NSString*     userName;
@property (nonatomic, strong, readonly) NSString*     signature;

- (instancetype)initWithUserBuilder:(UserBuilder*)builder;
@end
 
//
@implementation User
- (instancetype)initWithUserBuilder:(UserBuilder*)builder {
 self = [super init];
 if (!self) {
  return nil;
 }
 
 _userID = ;
 _userName = ;
 _signature = ;
 
 return self;
}
@end

If you want to create a User object, then this way:

UserBuilder* builder = [[[[UserBuilder new] userName:@"peak"] userID:@1000] signature:@"roll"];
User* user = [[User alloc] initWithUserBuilder:builder];

In this way, we avoid writing multiple init methods. Similarly, the User object is also immutable, and we can only do an assignment operation in the init method. Each scenario can initialize part of the property according to its own needs. Of course, in the end we need to assign values ​​to each property in the initWithUserBuilder. The role played by initWithUserBuilder is similar to the designed initializer.

Students who pursue the beauty of code may have discovered that the creation syntax of UserBuilder is ugly and multiple [ ] nests are used. To make the code look better, we can also use block to create:

User* user = [User userWithBlock:^(UserBuilder* builder) {
  = @"peak";
  = @1000;
  = YES;
}];

builder pattern is used more often on the Android platform, and I rarely see usage scenarios on the iOS platform. The shortcomings of builder pattern are also quite obvious. You need to define another builder class and write more code (property is basically repeated). Personally, I think that when there are many properties and many initialization scenarios, using builder pattern on iOS will also be a good solution.

Designated initializer vs builder pattern. The difference between the two actually reflects the differences in the language itself well. Students who have studied java will understand that in the world of java, everything can be encapsulated as objects. When using java, various auxiliary classes are often defined to complete a certain task. The advantage is that they have high encapsulation degree and small granularity in class responsibilities. The disadvantage is that there are too many classes, and sometimes they are encapsulated for the sake of encapsulation. The code in some scenarios is not intuitive enough.

According to readers' feedback, it turns out that the topic of this article has been written. After reading it, I found that it is more comprehensive than what I wrote. I recommend you to read it.Portal

Summarize

The above is the entire content of this article. This article briefly sorts out the different postures of creating objects, and hope it will be helpful to everyone. If you have any questions, please leave a message to communicate.