This paper analyzes the five principles of object-oriented design in detail: S(single responsibility principle “SRP”), O(open-closed principle “OCP”), L(Liskov replacement principle “LSP”), I(interface isolation principle “ISP”) and D(dependency inversion principle “DIP”), supplemented by examples.

This post is also published on my personal blog

Overview


The five “SOLID” principles of software design and the 23 classic design patterns have been around for a while now, and there are two extreme attitudes to them in actual development: stay away from them and turn your nose up at them. Obviously, I use the word “extreme” to indicate that I do not agree with this view. SOLID and classical design patterns are valuable practical experience summed up by predecessors in the long-term software development, which is worth our learning and reference. Of course, this does not mean that we should always talk about them in order to show our “internal forces”, nor does it mean that they should be used as a “supreme order” and “uncrossed red line” early in the software development process (which will undoubtedly increase the complexity of development).

It should be considered as a solution to the problem.

At this point, it is necessary to talk about “agile development” : in the era of mobile Internet, everyone is racing against time to seize traffic with the rhythm of “small steps, rapid iteration, rapid trial and error”. “Agile development” is thus often mentioned, but unfortunately mostly on the tip of the tongue. In the era of mobile Internet, I think there are two core points of agile development:

  • Don’t over-design, always try to keep your code simple, easy to understand, and easy to maintain (don’t add complexity by applying principles and design patterns from the start);
  • Embrace change, be it due to requirements or other reasons that the existing code structure can not meet the needs, actively refactor the code, always maintain a good code structure, zero tolerance for code decay (problems can be solved by referring to SOLID, design patterns, etc.).

From the above two points, agile development is an ongoing process, not a whim event.

Ps: Refactoring doesn’t have to be a sea change. Renaming variables, decomposing complex methods, and so on are refactorings.

This article will take the five SOLID design principles as the main line, supplemented by design mode as the solution, talk about QQ reading, iOS SYSTEM API in the design of the gain and loss (mainly on QQ reading individual code reflection).

Single Responsibility Principle “SRP”


SRP is easy to understand and expresses the same concerns as “cohesion.” SRP is arguably the simplest and most fundamental principle among the Five SOLID Principles. However, in actual development, SRP is the most difficult to grasp. Single responsibility. What exactly is a responsibility? How about a single granularity? In short, it is not easy to grasp, just like the right amount in life, “add water when cooking, salt when cooking” (often frustrating). In agile Software Development, Uncle Bob defines responsibility as “the cause of change.” A single responsibility is when there is only one cause that causes an entity (module, class, method, etc.) to change. When grasping a single responsibility, this is a good starting point, by observing and thinking about whether the design entity has more than one reason for change to judge whether its responsibility is single.

For the sake of convenience, entities refer to functional code blocks such as modules, classes and methods unless otherwise specified.

The author believes that SRP, as the most basic design principle, has two main benefits:

  • Reduce entity complexity and improve maintainability;
  • Improve entity reusability, which inevitably suffers when multiple responsibilities are coupled to an entity. Even if multiple places are reused, a change in one responsibility can have an unintended effect on the entity that is reusing the other responsibility, which is not what we want to see. That’s why Uncle Bob defines responsibility as “change.”

Example 1 UIView and CALayer

In the UIView hierarchy, we know that behind each View there is a CALayer corresponding to it. UIView is primarily responsible for user interaction, while CALayer is responsible for layout, rendering, and animation. The reason why Apple designs UIView and CALayer systems is to make their responsibilities more simple and reusable. On iOS and Mac OS, there are fundamental differences in the way user interaction is handled, but the layout, rendering, animation, and so on are the same. Thus, by separating the above responsibilities, CALayer can be well reused between iOS and Mac OS, while user interaction is handled independently, hence UIKit, AppKit.

Example 2 View and ViewModel

View and Layer in Example 1 belong to the system implementation level. At the application level, THE responsibility of UIView is clear and single: UI layout. However, in the actual development of a large number of display related business logic written in the View, seriously affecting the View reusability. The reason is that in non-MVVM mode, the presentation logic can only be placed in the Controller, which is bound to make the Controller too bloated. Therefore, in QQ reading, we propose to build UI components in view-ViewModel mode, put display logic into ViewModel, and View only deals with layout logic. At present, the effect is good, the logic of View is clearer, and the reusability is greatly improved. See the article “Customizing UI Component Libraries” for more information.

Open-closure Principle “OCP”


“Nothing is permanent except change”, especially in software development. It is almost impossible for a module, class, method and other entities to remain unchanged after the first release. As a result, change is something developers have to deal with (and love to hate). OCP is designed to teach us how to respond to change. OCP stands for “open to extension, closed to modification”. Specifically, the functionality of the entity can be extended (changed), but the source code of the entity is not allowed to be modified. Seems very contradictory! It’s like “You can buy anything you want, but you can’t spend money.” After careful analysis, the focus of OCP is to extend new functionality, that is, to extend new functionality can add new code, but not modify existing code. Because the impact of changes to existing code is unpredictable, it can be disastrous if the changes lead to chain reactions. How?

The key is abstraction.

“Interface programming, not implementation programming” is a common refrain. Interface oriented programming, that is, relying on an abstract interface in order to flexibly replace the implementation behind the interface. That’s what OCP needs!

Implementation scheme

Among the 23 classical design patterns, “Template Method pattern” and “Strategy pattern” can achieve OCP well. The implementation of Template Method pattern depends on inheritance, and the delegation (interface) used by Strategy pattern.

The Template Method pattern

TemplateMethod
PrimitiveOperation...
TemplateMethod
PrimitiveOperation...

The Strategy pattern

Example 1: QQ reading login module

At first, QQ was the only login method, but one day Apple’s father said that users should not be forced to log in to use the App. In desperation, we added a visitor login mode.

Numerous business modules

What’s the problem? The author thinks that it is ok for the business layer to interact with QQ login directly at the beginning. The key is to be aware of the problems when adding visitors’ login, and immediately reconstruct the problem, rather than paste and stack the code on the existing code base.

Sure enough, it wasn’t long before the product required wechat login. So I took the opportunity to do a complete refactoring of the login.

QRAuthenticatonCenter
QRAuthenticatonCenter
QRAuthenticatonCenter
QRYWAuthenticator
QROpenQQAuthenticator
QRAuthenticatorDelegate
QRYWAuthenticator
QROpenQQAuthenticator

Abstract Factory mode can make it unnecessary to modify QRAuthenticatonCenter when a new login method is added. However, the author thinks that in this scenario, the benefits brought by it are not enough to compensate for its complexity, that is, the disadvantages outweigh the advantages, so it is abandoned.

As can be seen above, OCP has been realized through the logon module reconstructed by Strategy mode, and the benefits brought by it have been fully enjoyed in the process of subsequent iterations.

Example 2: QQ reading engine module

QQ reading TXT engine is the most core of the whole project, is also the oldest module. Initially, there were two types of paragraphs in the engine: text and empty paragraphs, represented by an int variable called type. As we iterate, more and more interactive elements are added to the reading page that are not the content itself: author’s words, god’s words, and so on. Currently, the value of Type has been expanded to as many as 15 or 6 categories, and each new type needs to be modified in one or 20 places in the core engine, which can be described as covered with thin ice. This is an example of a serious violation of OCP, with serious consequences. Once the problem was identified, the refactoring scheme became clear: the logic of each type of paragraph was extracted into a class through the Strategy pattern and followed the same interface. The TXT engine relied on the abstract interface to make it comply with OCP.

Liskov substitution principle “LSP”


LSP: Subtype must be able to replace its base type. In plain English, anything that uses a base class type (such as an input parameter to a method call) can be replaced with its subclass type without unexpected errors. After reading the definition of LSP, I cannot help asking: What is the use of LSP? To answer this question, consider the opposite: What about disobeying LSP? Take method parameters as an example: If method M has a parameter of type B, if a subclass of class B does not comply with LSP and a subclass of class B is passed in when method M is called, M will fail. In this case, method M must do something special for subclasses of B in order not to make an error (if… else…) . A familiar smell! Is this a violation of OCP!

From Agile Software Development: Breaches of LSPS often result in the use of run-time type identification “RTTI” in a way that clearly violates OCP.

Example 1 square and rectangle

Uncle Bob has an example of squares and rectangles in Agile Software Development. It is common knowledge that a square is a special kind of rectangle. So it makes sense to have the Square class inherit from the Rectangle class. The Square class had to rewrite the setWidth and setHeight methods of its Rectangle base class to make sure that the Square’s Rectangle length and width remained the same after each call. This may not seem out of place, but in the following method there is a problem:

void g(Rectangle &r) {
    r.setWidth(5);
    r.setHeight(4);
    assert(r.area() == 20);
}
Copy the code

Function G knows a rectangle exactly right, but if it calls g with a reference to Square, it’s wrong! Clearly, the inheritance relationship between Square and Rectangle violates LSP.

From Agile Software Development: LSP leads us to a very important conclusion: a model, taken in isolation, is not really valid. The validity of a model can only be expressed through its clients. When considering the appropriateness of a particular design, the solution cannot be viewed entirely in isolation. The design must be viewed in the light of reasonable assumptions made by its users.

Therefore, whether an LSP is violated depends largely on the client.

The inheritance between Square and Rectangle violates LSP because there IS no “IS-A” relationship between them in terms of length and width.

From Agile Software Development: Square is not Rectangle, but how objects behave is what software really cares about. LSP clearly indicates that the IS-A relationship in OOD IS based on the behavior mode, which can be reasonably assumed and IS dependent on by the client.

LSP and polymorphism

The premise of discussing LSP is polymorphism, otherwise it is impossible to talk about. However, polymorphism is essentially a subclass whose methods override the virtual functions of the base class. Does this contradict the LSP requirement that subclasses can replace base classes? Because you end up calling the subclass’s methods through the base class pointer. The answer is no, but LSP is a better guide to how inheritance is used. To satisfy LSP requirements, subclasses can only extend the functions of the base class, but cannot “tamper” with them. Isn’t that what inheritance is all about?

Therefore, LSP does at least three things:

  • One of the important guarantees to achieve OCP;
  • Reduce the complexity of inheritance, which extends the functionality of the base class rather than “tamper” with it (treating the base class and all its subclasses equally);
  • Before deciding to use inheritance, you can better determine whether the two really have an “IS-A” relationship.

Heuristic judgment rules and improvement schemes

LSPS can be subtle and often difficult to detect during development. Uncle Bob proposes two heuristic rules for your reference:

  • Derived classes have degenerate functions, such as the following code base classBaseThe methods infIs functional, but to its subclassesDerivedIn thefDegenerate to an empty method, which is often indicative of an LSP violation, and should be warned:
public class Base {
    public void f(a) { /*some code*/}}public calss Derived : Base {
    public void f(a) {}}Copy the code
  • Throwing an exception from a derived class, that is, a method of the derived class that does not throw an exception from the base class, is often unexpected to the caller.

LSP violation indicates that inheritance is no longer suitable. In this case, you can extract the common codes of the parent and child. Then either make them “brothers,” both derived from the extracted code, or integrate the extracted code in a composite manner.

Interface Isolation rule ISP


Isps: Clients should not be forced to rely on interfaces they do not need. That is, classes that clients depend on should not contain methods that they don’t need to reduce the complexity of the system and reduce coupling between classes. On the contrary, if a client program depends on a class that contains a large number of methods it does not need, and these methods are required by other clients, when these methods need to change due to requirements or new methods need to be added, it is bound to hurt the client program that does not need these methods, thus increasing the coupling degree of the system. How to solve it? Isolate and split interfaces, of course! In an interface/protocol-supporting language (such as Objective-C), it is easy to split a class’s public methods into multiple interfaces. In languages that do not support interfaces, such as C++, interfaces can be decomposed through multiple inheritance, delegation, and so on.

Example 1 UITableView DataSource, Delegate

IOS developers are probably all too familiar with UITableView, which provides two sets of interfaces: UITableViewDataSource and UITableViewDelegate. In a scenario, both sets of interfaces serve UITableView. The reason for separating them is so that the responsibility for providing data to UITableView and handling user interactions can be split into separate classes.

Example 2: QQ reading login interface

In the section of “OCP”, the login module of QQ reading is briefly introduced. We know that the login details are handled by QRQQAuthenticator, QRWechatAuthenticator and QRGuestAuthenticator. These authenticators all implement the QRAuthenticatorDelegate interface:

@protocol QRAuthenticatorDelegate <NSObject>

// Active login
- (void)authenticateWithCompletion:(QRAuthenticateCompletion)completion;

/ / renewal
- (void)refreshTokenWithCompletion:(QRAuthenticateCompletion)completion; .@end
Copy the code

However, for QQ login, QRQQAuthenticator is required for special treatment when QQ is not installed. As this special treatment is only required for QQ login, it is not appropriate to put the corresponding interface into the QRAuthenticatorDelegate. In the end, we define it as an independent interface QRQQManuallyAuthenticationDelegate:

@protocol QRQQManuallyAuthenticationDelegate <NSObject>

- (void)manuallyAuthenticateWithAccount:(Account *)account;
- (void)checkVerifyCode:(NSString *)verifyCode account:(Account *)account;

@end
Copy the code

And let QRQQAuthenticator implement these two interfaces:

@interface QRQQAuthenticator : NSObject<QRAuthenticatorDelegate.QRQQManuallyAuthenticationDelegate>
@end
Copy the code

However, QRWechatAuthenticator and QRGuestAuthenticator only need to implement QRAuthenticatorDelegate.

You might wonder about isPs: according to SRP, the responsibilities of a class should be single, so why implement multiple interfaces? In reality, there are classes that are less cohesive at the interface level. For example, the QRQQAuthenticator class in Example 2 needs to be processed for normal login and renewal, as well as manual login. Therefore, the QRQQAuthenticator class does not have high cohesion on the interface. The ISP is used to guide how to split interfaces in this case.

Dependency inversion principle “DIP”


In the development, large modules are generally developed by several students in collaboration, and the division of labor is generally carried out in a hierarchical way. At this point, it is common to hear students in charge of low-level modules say to students in charge of high-level modules: “I have provided you with these methods, the code has been submitted, you have a look.” From the DIP perspective, two mistakes were made! First, low-level modules play a leading role in formulating interfaces between the two sides. Second, there is a lack of abstraction. DIP:

  • High-level modules should not depend on low-level modules; both should depend on abstractions;
  • Abstraction should not depend on details, details should depend on abstractions.

The dependency inversion principle emphasizes the relationship between high-level modules and low-level modules: high-level modules put forward requirements (interface) as the demand side, and low-level modules realize the requirements (interface) put forward by high-level modules.

Why?

  • High-level modules should not know the details of low-level modules;
  • When interfaces are written by low-level modules, implementation details can be involuntarily exposed in the interface, which is undesirable.

Example 1: Paging load

In the article on tabular application templates, we mentioned that “most application scenarios of most apps are tabular”, and paging load is standard for tabular application scenarios. If the low-level module (Model) is responsible for specifying the interface, it is likely that paging details will be exposed in the interface:

- (void)requestMoreDataWithPageStamp:(NSInteger)pageStamp completion:(void(^) (NSError *, id))completion;
Copy the code

Clearly, pageStamp is a detail of the Model’s interaction with the server, something that high-level modules don’t and shouldn’t care about. If a high-level module (Controller) makes the requirement (interface), the interface might look like this:

- (void)requestMoreDataWithCompletion:(void(^) (NSError *, id))completion;
Copy the code

Of course, this example is relatively simple, and a more experienced developer will not expose pageStamp information in the interface. However, there is a concern about the details exposed by low-level module specification of interfaces.

Example 2: Decouple high-level and low-level modules through abstraction

At the same time, DIP proposes that high-level modules and low-level modules cannot have a direct dependency, and that they should both rely on abstractions (interfaces). In this way, the high-level module can be decoupled from the low-level module, which makes the high-level module have better reusability.

Table Application Scenario Templating

DIP is the least costly principle to implement in SOLID, but the benefits are substantial, so DIP should be the principle to follow from beginning to end.

summary


The five principles of S, O, L, I, and D essentially help us reduce the complexity of software systems. They just focus on different dimensions:

  • SRP: Software entities (modules, classes, methods) are required to have a single responsibility to reduce entity complexity and improve entity cohesion;
  • OCP: software entities are required to be open to extension and closed to modification, so that the software system can reduce the impact on the existing parts of the system when expanding functions;
  • LSP: Requires inheritance relationships. Subclasses must replace base classes to reduce complexity caused by inheritance and reduce the possibility of inheritance misuse.
  • ISP: Breaking up complex interfaces to avoid forcing higher-level modules to rely on interfaces they don’t need and reduce unnecessary coupling;
  • DIP: To avoid unintentional exposure of low-level details when interfaces are formulated by low-level modules, decouple high-level and low-level modules through abstraction.

For SOLID and other design principles and patterns, you don’t have to talk about them every day, but you can use them to solve problems when you encounter them.

References:

Agile Software Development — Principles, Patterns, and Practices

Design Patterns: Elements of Reusable Object-oriented Software

Refactoring — Improving the Design of Existing Code