Recently, I have completed the reconstruction of our company’s iOS project, sorting out the overall code architecture, mainly in accordance with the MVP architecture mode, and comprehensively considering the difficulty and effect of reconstruction. In the process, I also accumulated some experience in code refactoring, which is summarized here.
Project overview and MVP mode refactoring
Project introduction
First, a brief introduction to the project. The architecture of our original project is the relatively standard MVC mode, which is also the official architecture mode recommended by Apple. The Model layer is used to represent entity classes, the View layer is responsible for displaying the interface and delivering UI events, and the Controller layer is responsible for most of the business logic. In addition, for some common reusable logic, we abstract out the Service layer and provide it to the Controller, and the network layer is also independent. The diagram below shows the overall architecture more clearly
Problems with the MVC pattern
The MVC architecture, as apple’s official recommended architectural pattern, separates the data Model and presentation View through the Controller layer, which is a good choice for small projects. As the complexity of the project increases, we gradually realize the drawbacks of the MVC pattern, which are mainly reflected in the following aspects
- The Controller layer has too much responsibility, and the Model and View layers are too simple
The Controller handles the business logic, handles UI updates, handles UI events, synchronizes the Model layer, and almost all of our code is written in the Controller layer. There is a single pattern principle in design mode, and you can see that the Controller layer here already has at least four responsibilities.
- Business logic and UI are mixed together, making it difficult to write unit tests
This is partly because the Controller layer in the Cocoa framework, the UI that we’re most familiar with, the ViewController and View are naturally coupled, and a lot of View lifecycle methods like viewWillAppear are in VC, On the other hand, we are often used to putting UI operations and even initialization operations in VC, causing UI and business logic to get mixed up. When you’re trying to write unit tests on business logic, look at the UI operations mixed up in your business logic code and you know what’s hard — data can be mocked, but UI can’t be mocked.
- A lot of business logic code exists in the Controller layer, which is difficult to maintain
When an interface function is complex, all of our logic code is stacked in the Controller, for example, our original WebViewController has 5000 lines of code, so maintaining code in this situation is like walking on thin ice.
Refactoring the MVP model
The MVP Model solves the problem that the Controller layer is too bloated. Since UIViewController and UIView are coupled, both are grouped into the View layer, the business logic is isolated in the Presenter layer, and the Model layer remains unchanged. The following comparison shows the structure of the MVP mode
- The Controller layer has too much responsibility, and the Model and View layers are too simple
In MVP mode, the Controller layer and the View layer have been merged into the View layer, which handles UI updates and event delivery, while the Model layer remains an entity class. Business logic originally written in the ViewController layer has been migrated to Presenter. MVP mode solves the problem of excessive Controller layer responsibilities.
- Business logic and UI are mixed together, making it difficult to write unit tests
The Presenter layer deals with the business logic, while the ViewController layer implements the interface provided by the Presenter to update the View, thus decoupling the business logic from the UI. If we were to write unit tests, we would Mock an object that implements the interface provided by Presenter. The MVP pattern addresses the decoupling of UI and logic well.
- A lot of business logic code exists in the Controller layer, which is difficult to maintain
By migrating business logic to Presenter layer, the dilemma of Controller layer seems to be solved. However, if a requirement logic is complicated, simply migrating business logic cannot solve the fundamental problem, there will also be a lot of business logic code in Presenter layer, which is difficult to maintain. We will discuss how to solve this problem next.
MVC pattern improvement – Router pattern
I have already mentioned this in my previous article. Here is a link to protocol oriented programming practices for iOS refactoring
The example analysis
As mentioned above, although MVP mode can solve many problems existing in MVC mode, for more complex requirements, logic is too complex, and Presenter layer is difficult to maintain. Let’s take a look at how to design and implement complex business logic through a practical example.
Many complex requirements start with a simple scenario and work their way up. Without constant optimization and refactoring, you end up with code that Only God can understand. So with that said, let’s get down to business. Let’s look at this requirement.
V1.0 Single file upload
Implement a simple single file upload, the index of the file is stored in the database, the file is stored in the App sandbox. This should be a piece of cake for experienced client developers, relatively simple and easy to implement. We can roughly break this requirement down into the following sub-requirements
- Initialize the upload View
- Update upload View
- Click the upload button event
- Get the upload model from the database
- Initiate an HTTP request to upload a file
- Checking network Status
The above are implemented using the traditional MVC pattern as shown in the following figure
UploadViewController
UploadPresenter now handles the upload logic, while UploadViewController focuses on UI updates and event delivery, making the overall structure clearer and easier to maintain in the future.
V2.0 multi-file upload
Demand is coming! The need to support multiple file uploads on top of the original means we have an additional sub-requirement
- Maintain the upload model queue
It was obvious that UploadPresenter needed a function to maintain an upload queue, and I did initially, but since uploaders need to listen for a lot of events and call back frequently, writing logic like this in Presenter multiplied the complexity of the code.
Therefore, after some consideration, I consider to separate the logic of file uploading into a layer of FileUploader, while UploadPresenter is only responsible for maintaining the Queue of FileUploader and checking the network status. The specific implementation is shown below.
V3.0 Multi-source upload
Originally, the source of our uploaded file exists in the App sandbox. We can find the index and path of this file through database query, and then obtain this file for uploading. Now comes the evil requirement to support uploading photos/videos from system albums.
- Support system album and App sandbox to get files
By this point some readers may be nodding their heads, but if you don’t think about it carefully, you could be on the road to code quality breakdown.
At this point, we need to think, they’re multiple sources, but for FileUploader, it doesn’t really care about the source of the model, it just needs to get the binary stream of the model. Thus, we can abstract out a BaseModel and provide a stream read-only property. Both sources inherit BaseModel and override the stream read-only property to implement their own methods for constructing the stream file. For FileUploader, it just holds BaseModel, which is a typical usage scenario for inheritance and polymorphism.
If there are additional files from other sources, such as web files (download before upload? Just continue to inherit BaseModel, reload stream, and everything is transparent to FileUploader and all of its upper layers. With this design, our code becomes maintainable and extensible again. Below is the architecture diagram.
V4.0 Multimode upload
In HTTP file uploads, we can directly upload the binary stream of the file, which requires specific support from the server. But the more common and widely supported approach is to use HTTP form file transfer, which is the standard assembly of multipart/form-data to transfer data while assembling the BODY of an HTTP request. So, we have one more requirement:
- Supports form transfer and stream transfer
The FSBaseM and ABaseM models are abstractly superclassed. The superclass contains the abstraction of the binary data of each file. The subclass implements the binary direct assembly stream, and assembles the stream according to the multipart/form-data format, as shown below.
V5.0 supports FTP/Socket uploading
Just now our file upload, the underlying protocol is based on Http, at this time we need to support FTP/Socket protocol transmission, how to do?
- Support HTTP/FTP/Socket
After thinking above, I believe you must know how to do. Here’s a thought, the answer is here in the MVP_V5 schema
contrast
Finally, let’s take stock of all the current requirements
- Initialize the upload View
- Update upload View
- Click the upload button event
- Get the upload model from the database
- Initiate an HTTP request to upload a file
- Checking network Status
- Maintain the upload model queue
- Support system album and App sandbox to get files
- Supports form transfer and stream transfer
- Support HTTP/FTP/Socket
Let’s see, if MVC, MVP_V1, MVP_V2, MVP_V3, MVP_V4, MVP_V5 are used respectively to implement the current ten requirements, our code will be roughly distributed in which layers.
The pros and cons are clear. Using the original MVC pattern, conservatively estimate ViewController code at least 3K lines.
conclusion
- The MVP design pattern decouples logic from UI operations
- In layered mode, the upper layer owns the lower layer, and the lower layer communicates with the upper layer through interfaces to achieve decoupling.
- Inheritance and polymorphism are used to mask the details of the underlying implementation to achieve separation of responsibilities and high scalability
Code optimization and refactoring techniques
In this refactoring project, I also summarized some refactoring tips and tricks to help students who want to start refactoring code
Things but three
-
Extracting large chunks of code that have been repeated three or more times into a common method is the most common and easiest thing to do, as long as you get into the habit of coding, extracting large chunks of code that have been repeated three or more times into a common method.
-
There are three or more responsibilities for a class – reduced by a reasonable layering of responsibilities, which was made clear in the example above, with the upper layer holding the lower layer and the lower layer communicating with the upper layer through interfaces. That’s the essence of the MVP model.
-
If the same if/else statements occur three or more times — consider using abstract classes and polymorphisms instead of if/else statements. If the same if/else statements occur many times in your code, consider designing an abstract class to replace them. This might be a little hard to understand, but it’s a lot easier to understand for example, let’s say we have a fruit category, we have three kinds of fruit, and the fruit has color, price, and variety
class Fruit {
var name:String = ""
func getColor() -> UIColor? {
if name == "apple" {
return UIColor.red
} else if name == "banana" {
return UIColor.yellow
} else if name == "orange" {
return UIColor.orange
}
return nil
}
func getPrice() -> Float? {
if name == "apple" {
return10}else if name == "banana" {
return20}else if name == "orange" {
return30}return nil
}
func getType() -> String? {
if name == "apple" {
return "Red Fuji"
} else if name == "banana" {
return "Banana"
} else if name == "orange" {
return "The emperor"
}
return nil
}
}
Copy the code
The same if/else judgment for the name name appears three times, and if we had a fruit pear, we would have to change all of the above if/else judgments, which would be very difficult to maintain.
In this scenario, we could abstract out a Fruit abstract class/interface/protocol by implementing the Fruit class/interface/protocol. In this scenario, if there is a new Fruit, let the Fruit continue to implement the Fruit protocol. In this way, we can replace the modification with a new method and improve the maintainability of the code.
protocol Fruit {
func getPrice() -> Float?
func getType() -> String?
func getColor() -> UIColor?
var name:String { get }
}
class Apple:Fruit {
var name:String = "apple"
func getColor() -> UIColor? {
return UIColor.red
}
func getPrice() -> Float? {
return 10
}
func getType() -> String? {
return "Red Fuji"
}
}
class Banana:Fruit {
var name:String = "banana"
func getColor() -> UIColor? {
return UIColor.yellow
}
func getPrice() -> Float? {
return 20
}
func getType() -> String? {
return "Banana"
}
}
class Orange:Fruit {
var name:String = "orange"
func getColor() -> UIColor? {
return UIColor.orange
}
func getPrice() -> Float? {
return 30
}
func getType() -> String? {
return "Emperor tangerine"}}Copy the code
Reasonable layered
- Vertical layering – The layers are related to each other and the upper layers hold the lower layers, which communicate with the upper layers through interfaces. Why don’t you let the lower ones own the upper ones? Again, in order to decouple, the lower layer is designed to serve the upper layer, it should not depend on the upper layer. This design pattern is common in computer science, such as network layering in computer networks.
- Horizontal layering – No relationship between layers applies to modules with relatively independent functions. Simple division is enough. The front page of our iOS project is composed of several parts. There is not much correlation between these parts. We simply divide them into several modules. If there are a few scenarios that require communication, use
Notification
Can.
Avoid over-designing
- Complex architectural designs often make little sense in high-speed iterative development on the client side (as opposed to the server side)
- No silver bullet!Software development is engineering, there is no perfect architecture mode, most of the time need to specific problem specific analysis, flexible use of design mode, get the local optimal solution. For example, the MVP model mentioned earlier does not solve the Presenter complexity problem if it is mechanically copied.
- How to judge overdesign? The number of lines in a large number of files is less than 100. After thinking about it for a day, I did not write the code or the architecture scheme
When and to whom to refactor
- Code Review is a great tool for refactoring when the number of lines in a file starts to exceed 500
- For functions whose requirements often change or increase, attention must be paid to the design to avoid functions whose quality is uncontrollable, stable and unchanged, and no reconstruction
conclusion
Finally, I want to talk about design patterns. In fact, the process of refactoring is the flexible use of design patterns to optimize and improve code. A lot of people read and learn a lot about design patterns, but very few of them are actually useful in their work. So the key is also in the flexible use of four words, can do this, your level will be on a step.
Therefore, in our daily work, we should have the Taste of the code, know what is good code, what is dirty code, find as soon as possible can be optimized and improved, continue to produce high-quality code, rather than achieve the function of everything, or sooner or later you will have to pay for the lazy you stole before. The above is my summary and share in the process of our company’s project reconstruction. The level is limited and I hope it will be helpful to you.