This article briefly introduces the understanding of high quality code from three dimensions: easy to understand, maintainable and extensible.
At the same time, a new GUI mode: MVVS is proposed.
This post is also published on my personal blog
Overview
Personally, high quality code should be “simple” first and foremost.
“Simplicity” can be measured from the following three dimensions:
- Easy to understand
- Can be maintained
- extensible
This paper will discuss the above three points and talk about my personal understanding.
Easy to understand
High quality code must be easy to understand and read like romance novels, not literary prose.
High-quality code can be elegant, but it’s definitely not showy.
High quality code should be simple and simple to express in a simple, unpretentious way.
Accessibility is for the whole team, not the person writing the code.
Easy to understand is the foundation of efficient and seamless team collaboration.
There are many factors that affect code understandability, such as:
“Well named”
- Good naming is half the battle and one of the most challenging things;
- The general principle is to embody the “what”, not the “how”;
- Pay attention and think when reading good open source code and system apis.
“Clear structure”
(This refers to the organization of the code)
- Related code is organized together, such as:
init
withdeinit
/dealloc
UIViewController lifecycle methodviewDidLoad
,viewWillAppear
,viewDidAppear
And so on; - Different tissues are separated by a blank line;
- Or organize by Category or Extension.
“Reasonable abstraction”
- The key to abstraction is to hide details (” details are the devil “);
- When we don’t care about the details, it’s easy to ignore the details, grasp the key points, and seize the backbone;
- Abstractions exist at every level: variables, methods, classes, modules.
“Linear logic”
- Each entity (variable, method, class, module) at a different level revolves around just one thing — “high cohesion”;
- Classes, methods and statements within modules can be connected in a linear way as far as possible.
- And if they form a web or discrete points, that smells like bad code — “low cohesion”;
- For example, there are a lot of unrelated methods inside a class, and too many methods inside a method
if
,switch
Statements. A class with thousands of lines and a method with hundreds of lines is worth being wary of.
It is obvious that linear logic is easier to understand than net-like logic.
“Minimum dependency”
(Dependence can be measured in two ways: how much and how strong it is.)
- Less dependency is definitely better — “low coupling”;
- Too many dependencies undoubtedly increase code complexity and are often a sign of code design problems;
- Improving cohesion and interface oriented programming are effective ways to reduce dependency.
- At the same time, the weaker the dependency, the better. Inheritance is undoubtedly the first way to reuse code, but inheritance is also the strongest dependency and coupling relationship.
- Code reuse, should give priority to combination.
“Lean volume”
Simply put, it is useless code timely delete;
When reading code, I often encounter some inexplicable logic. After some investigation, I find that the code is obsolete.
This adds to the cost of understanding, and as time goes on, newcomers are afraid to delete the code, which eventually accumulates.
“Plain words”
As the famous saying goes, code is written for people first and machines second.
Therefore, try to use plain, simple way to describe, to express, so that everyone can “understand”;
In short, good code is simple and clear.
Easy to understand is the most basic requirement of high-quality code, the above is only a few small points that affect the code easy to understand, more need us to constantly think and summarize in the actual coding process.
In the article “On High quality mobile development” there is a more specific discussion of class design, method design, etc. Interested students can check it out.
maintainability
The main scenario for mobile development is guI-based business development.
Therefore, the maintainability discussed in this article is also centered around GUI processes.
Design patterns for GUIs (MV*) have been one of the most common topics in recent years.
No matter how the GUI model evolves, I think its core principles remain the same:
- Unidirectional data flow
- Data integrity
- Data-driven UI
“One-way Data Flow”
Regardless of the GUI mode, there are generally two layers:
- Domain Layer: Data Layer
- Presentation Layer: UI
One-way data flow means that “business data” must flow from “data layer” to “presentation layer”.
The Presentation layer can respond to UI events and pass them on to the Data layer. How the data layer handles these events is its “internal affair” and the presentation layer has no right to interfere.
To summarize, there are two “flows” behind “one-way data flow” :
- Data flow: from the “data layer” to the “presentation layer”;
- Flow of events: Flow from the presentation layer to the data layer.
Many reactive frameworks define events as separate data structures, such as Action in Redux or Event in BLoC.
I personally see one serious problem with this design: you can’t “chain” the code through “command + click”, you need to do a global search.
This seriously affects development efficiency.
In my opinion, events can be directly the interface exposed by the “data layer” to the “presentation layer”, that is, when there is an event to be processed, the corresponding interface can be directly called.
Why is that?
I’m sure any mobile developer is familiar with the term “one-way data flow”, but the underlying reasons aren’t always clear.
First of all, data cannot flow from the “presentation layer” to the “data layer.” What does that mean?
What it really means is that the presentation layer cannot directly modify the data in the data layer. Why is that?
From a design point of view, data management is the responsibility of the “data layer”, the “performance layer” should not overstep the longterm. Otherwise, the “presentation layer” violates the principle of “single responsibility” and the design concept of “high cohesion.”
From a practical point of view, the “presentation layer” directly modifying data is a “disaster” for code maintainability.
First, there is the possibility of “out of sync” as a direct consequence:
- UI refresh is not synchronized with data modification. UI is not refreshed after data modification.
- If multiple UI scenarios are not synchronized, some data may be used by multiple service modules. If the data is directly modified by one of these modules, other modules may not be aware of the modification, causing the synchronization.
As shown below, Scenes1 directly modifs the underlying data without notifying Scenes2 and Scenes3 (Scenes1 may not know the existence of Scenes2 and Scenes3 at all), resulting in data unsynchronization:
A common consequence of “out of sync” is that arrays are out of bounds when you’re using a TableView.
Such problems are often protected from array access, and are rarely and rarely addressed at the root of the problem.
This is especially true when data is shared in multiple service scenarios. Because you never know “who” changed the data source “when.”
Secondly, it may cause multithreading problems:
- The “data layer” may have dedicated threads to manage the data, and if the “presentation layer” tampers with the data, multithreading problems may arise.
How to work out
The data layer must not directly expose modifiable properties to the presentation layer.
For reference types, things can be a little more complicated. Ideally, the data returned by the “data layer” to the “presentation layer” should be final or deep-copy.
In short, there is no possibility for the “presentation layer” to directly modify the underlying data.
At this point, you may have a question, even if the “presentation layer” through events trigger the “data layer” to modify the data, still can cause synchronization problems.
Yes, this needs to be solved by “data integrity”, “data-driven UI”.
“Data Integrity”
Data integrity refers to the fact that data should not exist in an interim state. This, as shown on the left, can lead to unexpected results.
When modification is needed, the data should be replaced as a whole, which is often referred to as “data immutability”.
In functional programming, all data is immutable, and all operations result in the generation of new data rather than modification of existing data.
Strictly following the principles of “data integrity” and “data immutability” can well avoid intermediate state problems.
“Data-driven UI”
How does the upper UI sense and refresh changes in the underlying data?
The general principle is “Data-driven UI”
To put it bluntly, the “data layer” has channels and means to actively notify all the “presentation layer” objects that care about data changes.
Seems simple, right? Many projects fail to do so.
Do you smell responsive programming? Some students feel very complicated and difficult to accept when they hear “responsive programming”.
In fact, I personally don’t think “responsive programming” is necessarily about using frameworks or technologies like Rx*, ReactiveCocoa, or iOS native KVO. (They are only a means of implementation.) The core of this is:
- “Data layer” active notification, “presentation layer” passive response;
- All users of data are aware of changes in data in a timely manner.
Thus, things like Delegate, Callback, and even Notification can be used for “responsive programming.”
“Responsive programming” is an application scenario of the “observer” design pattern.
All right, let’s review and summarize:
- “One-way data flow” : ensure that the modification of data only occurs in the “data layer”, which is the premise of “data integrity” and “data-driven UI”.
- “Data integrity” : any modification of data is an overall replacement, realizing the semantics of “immutable data” and avoiding the intermediate state of data;
- Data-driven UI: Notifies the UI of data modification to avoid status synchronization.
MVVS
Based on the principles of “one-way data flow”, “data integrity” and “data-driven UI”, we propose a new GUI mode: MVVS
- M: Manager, processing business logic, managing business data, converting data to ViewState and notifying the upper UI;
- V: View, UIViewController/UIView, programming for ViewState;
- VS: ViewState, the current UI state, splits the “data-driven UI” further: data-driven state, state-driven UI.
MVVS vs. MVVM, which is equivalent to splitting VIewModel in MVVM into Manager and ViewState in MVVS, making their respective responsibilities clearer.
ViewState in MVVS is inspired by state in Flutter responsive framework flutter_bloc.
The relationship between Manager, View and ViewState is shown in the following figure:
ViewState
ViewState represents the current state of the UI and provides display data for the UI.
In principle, View state corresponds to View one by one. There may be no corresponding ViewState for an overly simplistic UI.
As shown in the figure above, the corresponding ViewController is Root ViewState, which represents the current overall state of the module.
According to different states, ViewState can be different subtypes, such as LoadingViewState, ErrorViewState, EmptyViewState, LoadedViewState, etc. The ViewController responds differently depending on the state.
other
Above we have covered a few key points where GUI architecture can affect code maintainability. There are many other factors that can affect code maintainability, such as:
-
Minimum state: It is common to see a lot of state variables in code, such as: IS ***(isFirstLoad, isNewUser), has***(hasLoaded), ***Count(userCount), etc.
The maintenance of these states themselves, as well as their maintenance of the code as a whole, can be very problematic.
Did you correct all the changes that needed to be made?
Are changes made to where these states are used notified in time?
So, as few states as possible, as few states as possible.
For example, whether ***Count can be calculated dynamically from the dataset
-
Minimum permissions: Interfaces exposed by both modules and classes should adhere to the minimum permissions principle
In the specific development process, the principle of “dependency inversion” can be adopted to let the interface demander put forward the interface demand, so as to avoid the implementation side directly providing the interface and unintentionally exposing too many details.
For details, please refer to the application of object-oriented Design Principle “SOLID” in development.
-
Write pure functions: Pure functions have few external dependencies and have the natural advantage of being maintainable and easy to understand.
General class methods have pure function properties, so don’t write instance methods when you can write class methods.
Functional programming was briefly discussed in the article “Functional Thinking.
scalability
“The only constant is change”
In the face of change, we only have a positive attitude towards it.
Therefore, code extensibility is one of the key issues to consider.
The “O-OCP, open-closed principle” in “SOLID” is one of the principles used to guide scalability.
OCP: “Open for extension, closed for modification”.
The emphasis on “extensibility” requires us to write “living code”. In Code Review, I often joke that “your Code is too dead”.
The “policy pattern” and “template approach” among the 23 design patterns are used to guide scalability.
“Strategic Mode”
One of the most effective ways to increase code extensibility is to program to the interface.
This is described in detail in “On Interface programming” and won’t be repeated here.
How do you write code that scales well during development?
Scalability is for the future,
So the first thing to think about is “what might change?”
Isolate the “mutable part” and abstract it in the form of an interface.
Those of you who are familiar with design patterns may already recognize this as “strategic pattern, Strategy”.
As mentioned in previous articles, login modules and multi-tab pages are typical extensibility scenarios that can be improved through “strategic patterns”.
Examples of login modules are described in detail in the article “Application of object-oriented design principles” SOLID “to development
Examples of multi-tab pages are covered in detail in “On Interface Programming.
“Template method”
The foundation of the “policy pattern” is interface-oriented programming.
The foundation of “Template Method, Template Method” is inheritance.
Similarly, the “template approach” pattern was briefly described in the article “The application of object-oriented Design Principles” SOLID “to development.
In the article “iOS Development Solutions for Productivity,” I described using the “template approach” pattern to improve the extensibility of UI components.
There are more than two ways to improve code extensibility, but the ideas behind them are important and can be applied flexibly during normal development.
other
In addition to the “easy to understand”, “maintainable” and “extensible” mentioned above, there are many other points worth thinking about and paying attention to, such as:
-
Error handling: Handling errors gracefully is important but often overlooked. When designing an interface, you need to pay attention to error situations.
-
Critical path log: Errors are unavoidable. In addition to giving the user a good reminder when an error occurs, we also need to understand the cause of the error. At this time, log is particularly important. Otherwise, users feedback problems, two eyes a black, do not know how to start;
-
Refactoring in time: Refactoring doesn’t have to be a “sea change” to the code, but “minor” optimizations such as renaming variables or extracting a method can be refactoring. In short, when the current code structure can no longer adapt to the needs of the new business, it needs to be refactured in time, must not patch on the original basis, code deterioration is often the beginning.
We should also treat code with awe: “Don’t do good in a small way, don’t do bad in a small way.”
summary
Writing high quality code is good for now, good for the future
Every developer should strive to produce high-quality code
The improvement of code design ability is not done in a day. It requires long-term continuous learning, practice, thinking and summary
Excellent books, excellent open source code we want to learn
Bad code is something we have to reflect on and summarize
Do the same thing in code: “Don’t do good in a small way, don’t do evil in a small way.”
All of you!