The road to APP reconstruction (II) Model design

07 Aug 2017

Reading time ~19 minutes

The road of APP reconstruction (1) Network request framework

The road to APP reconstruction (II) Model design

The road to APP refactoring (3) Introducing unit testing


preface

Many apps use the MVC design pattern to separate “user interaction” from “data and logic”, and one of the important functions of model is persistence. The Model designed below may not be a perfect, scalable Model example, but in the case of the app I need to refactor, such a design will meet my needs.

About the Model

The Model layer contains the data and logic of the app, and the classes in the Model layer need to care about the representation, storage, and operation of the data. The Model layer is a relatively independent part of the app ecosystem because it does not communicate directly with the Controller layer or View layer, but indirectly when other layers need to request its information.

What’s the use of a Model?

To write a good model, first of all, it is necessary to clarify the role of the model.

  • Attribute access: Storing attributes and data in a file as attributes
  • Variability:Properties can bereadwrite, so it can be changed and saved locally
  • KVO: You can control the UI or elsewhere by observing the value of an attribute and being notified when it changes
  • Data processing: Process network data and local storage data according to service logic

How do YOU define a Model class

We can create a series of Model classes that can inherit from each other, and each Model class corresponds to the entity in the current app. For example, in the app I need to reconstruct now, user data corresponds to UserInformationModel, and class information corresponds to StudentClassModel.

On the other hand, there are a number of issues that need to be addressed in the implementation of the Model class, so I’ll explain them in combination with the app I’m currently refactoring.

Information storage format processing

Data can be stored in various formats. In the APP I reconstructed, data and other information are stored in common data structures, such as data or dictionary to store Model information. There were no major problems when the Model was first built, but problems began to emerge when the demand grew and the information for a Model class began to grow. For example, I want to output the user’s name, age, class, homeroom teacher, grade, etc.

// never do this !!!
- (void)printInformation
{
	NSLog(@"name: %@", [user objectForKey:@"name"]);
	NSLog(@"age: %@"m [user objectForKey:@"age"]);
	NSLog(@"class: %@"m [user objectForKey:@"clazz"]);
	NSLog(@"teacher: %@"m [user objectForKey:@"teacher"]);
	NSLog(@"grade: %@"m [user objectForKey:@"grade"]);
}Copy the code

This may seem like a fine way to pull out the data, but when you get a lot of data the code gets messy, and no! The United States! View! , and the most important problem is that in the process of retrieving data from the dictionary, the key will be typed wrong, resulting in data retrieval failure.

Therefore, in the process of Model design, the Model should be designed as a class rather than a structure as far as possible. Property can be used to access information in the class, which can provide the developer with basic spelling check and completely eliminate the failure of data acquisition caused by wrong key typing. It also makes it easier for other developers to see the data types stored in the Model, and for future expansion and maintenance.

// YES! do this!
- (void)printInformation
{
	NSLog(@"name: %@", user.name);
	NSLog(@"age: %@", user.age);
	NSLog(@"class: %@", user.clazz);
	NSLog(@"teacher: %@", user.teacher);
	NSLog(@"grade: %@", user.grade);
}Copy the code

Network data processing

Since most of the functions of the APP I reconstructed are based on the network, the processing of network data in the Model layer will be the focus. Because network data is acquired asynchronously, and the acquisition process is prone to failure. Therefore, it is more difficult to obtain network data than local data. On the other hand, the network request framework of many apps provides the caching function, which can obtain data from the cache faster in the next request, without the need for another network request. This involves local data acquisition and other issues, making the processing of network data seem particularly tedious.

In a synchronous network environment, we can put error handling elsewhere, simply cache it, and even update, delete, and add new network data as if it were local. Unfortunately, networks are asynchronous, so we need to deal with this important issue.

First of all, the cache and failure handling mentioned above should be handed over to the network request framework for handling, and finally get a responseObject, and then perform model transformation and other operations on responseObject.

See this blog post about the Network request framework

For example, in my current project, I use MJExtension to convert dictionaries to models, which I put into each individual API, such as requests for inventory, I’m converting responseObject to InventoryModel in the InventoryAPI, rather than putting it in VC, so that vc is just making a network request, and when it’s done, it’s going to return the model that I want.

InventoryAPI.m

- (id)modelingFormJSONResponseObject:(id)JSONResponseObject
{
    NSUInteger count = ((NSArray *)JSONResponseObject).count;
    NSMutableArray *modelsArray = [NSMutableArray array];
    for(int i = 0; i < count; i ++)
    {
        InventoryModel *model = [InventoryModel mj_objectWithKeyValues:JSONResponseObject[i]];
        [modelsArray addObject:model];
    }
    return modelsArray;
}Copy the code

InventoryViewController

- (void)requestInformation { [api startWithBlockSuccess:^(__kindof YXYBaseRequest *request) { // request.responseObject ModelsArray} Failure :^(__kindof YXYBaseRequest * Request, NSError *error) {}]; }Copy the code

Local data processing

There are several ways to store local data, and a common practice is to use.plist files to store very simple data, such as Settings, and SQLite databases to store other complex data. On the other hand, you can try using Core Data to store Data models. Although Core Data can cause more problems and even affect performance, its NSFetchResultsController, lazy loading, Data processing tools are also very useful, so… Look at yourself.

In local data processing, the most important thing is how to obtain and modify data. There is not much local content in the project I need to reconstruct now. Most of the Data is obtained through the network, so I am not prepared to elaborate on the reconstruction and standardized processing of Data Model. But the basic principle is to have an accessor for each model that provides operations like fetchALl, fetchAllUsingPredicate, createInstance, save, and so on. Each model can use these operations to retrieve data, but the logic of these operations is hidden. I don’t need to know if my model is in a database, or in a.plist file, or in a cache when I call it. I just need to know that when I call these methods, I can get the model I want, and I can get the data I want.

This blog post covers SQLite, Core Data, and FMDB in detail. If you want to know more about database storage, you can take a look.

Business logic processing

In the Model, not only stores that can handle data, but also business logic. In the project I need to reconstruct, both strong and weak business logic are scattered in VC, resulting in vc oversize. In the following code, it is necessary to extract a time from DateModel, convert the time in NSDate format to NSString, and display it in View:

DateModel.h

@interface DateModel : BaseModel

@property (copy, nonatomic) NSDate *currentDate;

@endCopy the code

DateModel.m

- (instantcetype)init
{
	if (self = [super init])
		currentDate = [NSDate date];
	return self;
}Copy the code

aViewController.m

- (void)showTheDate { DateModel *dateModel = [DateModel new]; NSDate *date = dateModel.currentDate; NSDateFormatter *format = [[NSDateFormatter alloc] init]; DateFormat = @"yyyy MM dd HH: MM :ss"; NSString *string = [format stringFromDate:date]; self.dateLabel.text = string; }Copy the code

In fact, this code can be placed separately in the Model layer to make the whole VC code clearer, such as:

NSDate+dateTransform.h

@interface NSDate (dateTransform)

+ (NSString *)transformStringFromDate:(NSDate *)date;

@endCopy the code

NSDate+dateTransform.m

+ (NSString *)transformStringFromDate:(NSDate *)date { NSDateFormatter *format = [[NSDateFormatter alloc] init]; DateFormat = @"yyyy MM dd HH: MM :ss"; NSString *string = [format stringFromDate:date]; return string; }Copy the code

Create a classification of NSDate and place the transformation code in it, which is called by DateModel to complete the transformation

DateModel.h

@interface DateModel : BaseModel

@property (copy, nonatomic) NSString *currentDate;

@endCopy the code

DateModel.m

- (instantcetype)init
{
	if (self = [super init])
	{
		currentDate = [NSDate transformStringFromDate:[NSDate date]];
	}
	return self;
}Copy the code

Then in VC, we simply call:

aViewController.m

- (void)showTheDate
{
	DateModel *dateModel = [DateModel new];
	self.dateLabel.text = dateModel.currentDate;
}Copy the code

So the vc code structure is a lot clearer, the model layer out of the attribute can be delivered directly to the view layer to display, and date conversion this kind of code can easily be reused. In summary, for this type of model, the calling method can be as abstract as possible, and the IOC pattern can be applied when decoupling this part of the code is needed, otherwise it becomes very difficult to change the logic later.

Read more:

  • IOC (Inversion of Control)
  • DI(Dependency injection)

conclusion

IOS apps often don’t give much thought to the design capabilities of the Model layer, but for various reasons, such as scattered code or error-prone network connections, the problem is complicated. To avoid these problems, the design of the Model layer must strictly follow these principles, such as separating the code that processes data, designing an accessor for local data, designing an accessor for network data, and so on. But no matter how we design the Model layer, it is important that the implementation of these features be completely hidden from callers, and that the implementation of these features be simple enough to change and update in the future.

In short, network libraries, business logic, database tools, etc., are likely to change after a period of development. A good Model layer design allows you to upgrade the underlying libraries without changing a single line of controller and View layer code.