The original intention of learning and the way of explanation

I would like to systematically learn knowledge about design patterns before the end of my third year in iOS. Before learning Design patterns, I think it is more necessary to learn several Design principles of Object Oriented Design (Object Oriented Design) to lay a foundation for the later learning of Design patterns.

This article is to share the author recently learned and summarized the six design principles of object-oriented design:

abbreviations English names Chinese name
SRP Single Responsibility Principle Single responsibility principle
OCP Open Close Principle The open closed principle
LSP Liskov Substitution Principle Richter’s substitution principle
LoD Law of Demeter (Least Knowledge Principle) Demeter’s Law (least known principle)
ISP Interface Segregation Principle Interface separation Principle
DIP Dependency Inversion Principle Principle of dependence inversion

Note that the design principle SOLID (the first letter of the abbreviation in the table above, top to bottom) does not include Demeter’s law, but only the other five. In addition, the synthesis/polymerization reuse principle (CARP) is not included in this article as it is not considered by the authors to be typical of the other six principles and is not easily breached in practice. Students who are interested can look up the information and study by themselves.

In the next section, I’ll walk through these design principles individually, combining the concepts with the code and their CORRESPONDING UML class diagrams.

The language of the code is Objective-C, the language I am most familiar with. Although it is a relatively minority language, I believe that the minority of the language will not have much resistance to understanding knowledge because of the help of UML class diagrams and the formal use of classes and interfaces (in objective-C, protocols) in mainstream object-oriented languages.

In addition, in the explanation of each design pattern, the author will first describe an application scenario (requirement point), and then use two kinds of design code to compare: first provide the code of relatively bad design, and then provide the code of relatively good design. Standard UML class diagrams are attached to both codes to help you understand them. It also helps readers who are not familiar with UML class diagrams to familiarize themselves with the syntax of UML class diagrams.

Six Design Principles

The six design principles are presented in order of ease and difficulty. The open closed principle is explained first here because it is easy to understand and is the cornerstone of all other design principles.

Note:

  1. The examples used to explain the six principles are not related to each other, so the order of reading can be determined according to the reader’s preference.
  2. Interfaces in the Java language are called protocols in Objective-C. Although the Demo is written in Objective-C, the protocol is called interface instead of protocol.

Principle 1: The Open Close Principle

define

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

That is, a software entity such as a class, module, or function should be open for extension, but closed for modification.

Interpretation of definitions

  • Build the framework with abstractions and extend the details with implementations.
  • New requirements should not be implemented by changing the original class, but by implementing the interface that was previously abstracted (or the concrete class inherits from the abstract class).

advantages

The advantage of practicing the open closed principle is that you can extend functionality to a program without changing the original code. It increases the extensibility of the program and reduces the maintenance cost of the program.

The code on

Here is a simple example of the open closed principle in action from an online course.

Demand point

Designing an online course class:

Due to limited teaching resources, there were only blog-like, text-based courses at first. However, with the increase of teaching resources, video courses, audio courses and live courses were added later.

Let’s start with bad design:

Bad design

Initial text courses:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic.copy) NSString *courseTitle;         // Name of course
@property (nonatomic.copy) NSString *courseIntroduction;  // Curriculum introduction
@property (nonatomic.copy) NSString *teacherName;         // Name of lecturer
@property (nonatomic.copy) NSString *content;             // Lesson content

@end
Copy the code

The Course class states the data that will be included in the initial online Course:

  • Course name
  • Course is an introduction to
  • The lecturer’s name
  • Written content

Then, according to the requirements mentioned above, video, audio and live courses were added:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic.copy) NSString *courseTitle;         // Name of course
@property (nonatomic.copy) NSString *courseIntroduction;  // Curriculum introduction
@property (nonatomic.copy) NSString *teacherName;         // Name of lecturer
@property (nonatomic.copy) NSString *content;             // Text content


// New requirement: video courses
@property (nonatomic.copy) NSString *videoUrl;

// New requirement: audio courses
@property (nonatomic.copy) NSString *audioUrl;

// New requirement: live courses
@property (nonatomic.copy) NSString *liveUrl;

@end
Copy the code

Each of the three new courses adds a url to the original Course class. In other words, each time a new type of Course is added, it is modified in the original Course class: the data required by the Course is added.

This leads to: the video Course object we instantiate from the Course class will contain data that is not its own: audioUrl and liveUrl: this creates redundancy. The video Course object is not a pure video Course object, it contains members such as the audio address, the live address, and so on.

Obviously, this design is not a good design, because:

  1. As the requirements grow, the previously created classes need to be modified repeatedly.
  2. This creates unnecessary redundancy for the new classes.

The above two flaws are due to the fact that instead of following the open-closed principle of closed for changes, open for extensions, the design does the opposite: open for changes, and no convenience for extensions.

How can you do it in accordance with the open closed principle? Here’s a look at a better design that follows the open closed principle:

Better design

First, only the data contained in all courses are kept in the Course category:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic.copy) NSString *courseTitle;         // Name of course
@property (nonatomic.copy) NSString *courseIntroduction;  // Curriculum introduction
@property (nonatomic.copy) NSString *teacherName;         // Name of lecturer
Copy the code

Then, for the three new courses of text Course, video Course, audio Course and live Course, the Course type is inherited. And after inheritance, add its own unique data:

Text Courses:

//================== TextCourse.h ==================

@interface TextCourse : Course

@property (nonatomic.copy) NSString *content;             // Text content

@end
Copy the code

Video courses:

//================== VideoCourse.h ==================

@interface VideoCourse : Course

@property (nonatomic.copy) NSString *videoUrl;            // Video address

@end
Copy the code

Audio Courses:

//================== AudioCourse.h ==================

@interface AudioCourse : Course

@property (nonatomic.copy) NSString *audioUrl;            // Audio address

@end
Copy the code

Live courses:

//================== LiveCourse.h ==================

@interface LiveCourse : Course

@property (nonatomic.copy) NSString *liveUrl;             // Live address

@end
Copy the code

In this way, both of the above problems are solved:

  1. As class types increase, there is no need to iterate over the original parent class (Course), just create a new subclass that inherits from it and add data (or behavior) that belongs only to that subclass.
  2. Because the data (or behavior) unique to each course is scattered into different course subclasses, there is no redundancy in the data (or behavior) of each subclass.

And for the second point: maybe future video courses will have hd addresses, video acceleration. These features only need to be included in the VideoCourse class, as they are unique to the VideoCourse. Similarly, live lessons can be followed by an online question-answer course, which can be added to LiveCourse alone.

As we can see, it is because the initial programming was well designed that the subsequent increase in requirements was handled well.

Here’s a look at the UML class diagrams for the two designs to see the differences more vividly:

UML class diagram comparison

Unpracticed open closed principle:

Practice of the open closed principle:

In the UML class diagram that implements the open closed principle, the four Course classes inherit from the Course class and add their own unique properties. (In UML class diagrams: solid lines with hollow triangular arrows represent inheritance: subclasses point to their superclasses)

How to practice

To better practice the open closed principle, consider at the beginning of the design what data (or behavior) in the scenario is fixed (or difficult to change) and what is easy to change. The latter can be abstracted into interfaces or abstract methods so that in the future concrete implementations can be created to meet different requirements.

Principle 2: Single Responsibility Principle

define

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

That is: a class is allowed to have only one responsibility, that is, only one cause of class changes.

Interpretation of definitions

  • Changes in class responsibilities are often what cause a class to change: that is, if a class has multiple responsibilities, there are multiple causes for the class to change, making it difficult to maintain.

  • Often, with increasing requirements in software development, it is possible to add responsibilities that do not belong to the original class, thus violating the single responsibility principle. If we find that the current class has more than one responsibility, we should separate out the true responsibilities that do not belong to the class.

  • Not only classes, but also functions (methods) should follow the single responsibility principle, that is, a function (method) does only one thing. If you find different tasks in a function (method), you need to separate out the different tasks as another function (method).

advantages

If the responsibilities of classes and methods are clearly divided, it not only improves the readability of the code, but also reduces the risk of errors in the program. Because the clear code will make there no place for bugs to hide, it is also good for bug tracking, which reduces the maintenance cost of the program.

The code on

Single responsibility principle of the demo is relatively simple, through the object (property) design on the explanation is enough, do not need a specific client call. Let’s look at the requirements first:

Demand point

Initial requirement: You need to create an employee class that has some basic information about the employee.

New requirements: Add two methods:

  • Determine if the employee will be promoted this year
  • Calculate employees’ salaries

Let’s start with bad design:

Bad design

//================== Employee.h ==================

@interface Employee : NSObject

//============ Initial requirement ============
@property (nonatomic.copy) NSString *name;       // Employee name
@property (nonatomic.copy) NSString *address;    // Employee address
@property (nonatomic.copy) NSString *employeeID; / / employee ID
 
 
 
//============ New demand ============
// Calculate the salary
- (double)calculateSalary;

// Whether to be promoted this year
- (BOOL)willGetPromotionThisYear;

@end
Copy the code

As you can see from the above code:

  • Under initial requirements, we createdEmployeeThis employee class declares three attributes of employee information: employee name, address, and employee ID.
  • Under the new requirement, two methods are added directly to the employee class.

The new requirements approach, which seems to be fine because it concerns the employee, violates the single responsibility principle: the two approaches are not the responsibility of the employee.

  • calculateSalaryThe responsibility for this method belongs to the accounting department: salary calculation is the responsibility of the accounting department.
  • willPromotionThisYearThe responsibility for this method belongs to the personnel department: the appraisal and promotion mechanism is the responsibility of the personnel department.

The above design imposes duties that do not belong to employees into the employee class, and the original intention of this class (original duties) is simply to retain some information of employees. So doing so introduces a new responsibility to the class, and therefore the design violates the single responsibility principle.

We can simply imagine the consequences of this: if the employee’s promotion mechanism changes, or if the factors that affect the employee’s salary, such as tax policy, change, we need to modify the current class.

So how do you do that without violating the single responsibility principle? – We need to separate the two methods (responsibilities) and let the class that is supposed to handle these tasks handle them.

Better design

We retain basic information about the employee category:

//================== Employee.h ==================

@interface Employee : NSObject

// Initial requirements
@property (nonatomic.copy) NSString *name;
@property (nonatomic.copy) NSString *address;
@property (nonatomic.copy) NSString *employeeID;
Copy the code

Next create a new accounting department class:

//================== FinancialApartment.h ==================

#import "Employee.h"

// Accounting department
@interface FinancialApartment : NSObject

// Calculate the salary
- (double)calculateSalary:(Employee *)employee;

@end
Copy the code

And Personnel Department:

//================== HRApartment.h ==================

#import "Employee.h"

// Personnel Department
@interface HRApartment : NSObject

// Whether to be promoted this year
- (BOOL)willGetPromotionThisYear:(Employee*)employee;

@end
Copy the code

The two tasks were separated out by the creation of two divisions specializing in salaries and promotions, the accounting department and the personnel department classes, the FinancialApartment and the HRApartment, and placed them in the classes that were supposed to handle them.

In this way, the single responsibility principle is not only satisfied in this new requirement, but also can be directly added in these two classes if there are additional tasks to be handled by the hr department and the accounting department.

Here’s a look at the UML class diagrams for the two designs to see the differences more vividly:

UML class diagram comparison

Not practicing the single Responsibility principle:

Practiced the principle of single responsibility:

It can be seen that in the UML class diagram that practices the single responsibility principle, the two responsibilities not belonging to Employee are classified into the FinancialApartment class and the HRApartment class. (In UML class diagrams, dashed arrows indicate dependencies, often used in method parameters, etc., pointing from the dependent to the dependent)

As mentioned above, in addition to the single-responsibility design principle for classes, the single-responsibility design principle should also be followed for functions (methods). Because the principle of single responsibility for functions (methods) is easy to understand, Demo and UML class diagrams are not provided here.

Here’s a simple example:

The default navigation bar of the APP looks like this:

  • white
  • Black title
  • It’s shaded at the bottom

The pseudocode to create the default navigation bar might look something like this:

// Default style navigation bar
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{
    
    //create white color background view
    
    //create black color title
    
    //create shadow bottom
}
Copy the code

Now we can use this method to create the default navigation bar uniformly. But soon there is a new requirement, some pages need to make the navigation bar transparent, so need a transparent style of navigation:

  • Transparent bottom
  • White title
  • No shadow at the bottom

For this requirement, we can add a method:

// Transparent navigation bar
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{
    
    //create transparent color background view
    
    //create white color title
}
Copy the code

See the problem? Within these two methods, the difference between the methods that create the background View and the title color and the title color is only the color difference, while the rest of the code is duplicated. So we should draw out these two methods:

// Set the background color of the navigation bar based on the color parameter passed in
- (void)createBackgroundViewWithColor:(UIColor)color;

// Set the title based on the passed title string and color parameter
- (void)createTitlewWithColorWithTitle:(NSString *)title color:(UIColor)color;
Copy the code

And the shadow part above can also be extracted as a method:

- (void)createShadowBottom;
Copy the code

In this way, the original two methods can be written:

// Default style navigation bar
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{
    
    // Set the background to white
    [self createBackgroundViewWithColor:[UIColor whiteColor]];
    
    // Set the black title
    [self createTitlewWithColorWithTitle:title color:[UIColor blackColor]];
    
    // Set the bottom shadow
    [self createShadowBottom];
}


// Transparent navigation bar
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{
    
    // Set the transparent background
    [self createBackgroundViewWithColor:[UIColor clearColor]];
    
    // Set the white title
    [self createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
}
Copy the code

And we can take the methods from inside and call them from outside:

Set the default style of the navigation bar:

// Set the background to white
[navigationBar createBackgroundViewWithColor:[UIColor whiteColor]];

// Set the black title
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor blackColor]];

// Set the shadow
[navigationBar createShadowBottom];
Copy the code

To set the transparent style of the navigation bar:

// Set the transparent color background
[navigationBar createBackgroundViewWithColor:[UIColor clearColor]];

// Set the white title
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
Copy the code

This way, whether called inside a large method or separately outside, it is clear how each element of the navigation bar is generated, because each responsibility is assigned to a separate method. And there is a benefit, transparent navigation bar if encounter light background, use white font to use black font is good, so in this case we can createTitlewWithColorWithTitle: color: incoming black color value method. There will probably be more navigation bar styles in the future, so we just need to change the color values passed in individually, so there will not be a lot of duplication of code and it will be easy to change.

How to practice

In the employee example above, it may be because we have a preconceived idea of what a company’s organizational structure looks like and we take it for granted. In practice, however, it’s easy to mix different responsibilities together, and developers need to be aware of this.

Principle 3: Dependency Inversion Principle

define

  • Depend upon Abstractions. Do not depend upon concretions.
  • Abstractions should not depend upon details. Details should depend upon abstractions
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

That is:

  • Rely on abstractions, not implementations.
  • Abstractions should not depend on details; Details should depend on abstractions.
  • High-level modules cannot depend on low-level modules; both should depend on abstractions.

Define the interpretation

  • Program to interfaces, not implementations.
  • Try not to derive from concrete classes, but by inheriting abstract classes or implementing interfaces.
  • The division of high level module and low level module can be divided according to the level of decision ability. Business layer is naturally in the upper module, logic layer and data layer are naturally classified as the bottom.

advantages

Abstract is used to build the framework and establish the association between classes to reduce the coupling between classes. And the system built in the abstract is more stable than the system built in the concrete realization, higher expansibility, but also easy to maintain.

The code on

The dependency inversion principle is illustrated with an example that simulates project development.

Demand point

Implement the following requirements:

Use the code to simulate a real project development scenario: front-end and back-end developers working on the same project.

Bad design

First, generate two classes, one for front-end and back-end developers:

Front-end developers:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject

- (void)writeJavaScriptCode;

@end



//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeJavaScriptCode{
    NSLog(@"Write JavaScript code");
}

@end
Copy the code

Backend developers:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject

- (void)writeJavaCode;

@end



//================== BackEndDeveloper.m ==================

@implementation BackEndDeveloper

- (void)writeJavaCode{
    NSLog(@"Write Java code");
}
@end
Copy the code

These two developers provide their own methods: writeJavaScriptCode and writeJavaCode.

Next, create a Project class:

//================== Project.h ==================

@interface Project : NSObject

// Constructor, passed in the developer's array
- (instancetype)initWithDevelopers:(NSArray *)developers;

// Start development
- (void)startDeveloping;

@end



//================== Project.m ==================

#import "Project.h"
#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray *_developers;
}


- (instancetype)initWithDevelopers:(NSArray *)developers{
    
    if (self = [super init]) {
        _developers = developers;
    }
    return self;
}



- (void)startDeveloping{
    
    [_developers enumerateObjectsUsingBlock:^(id  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        if ([developer isKindOfClass:[FrondEndDeveloper class]]) {
            
            [developer writeJavaScriptCode];
            
        }else if ([developer isKindOfClass:[BackEndDeveloper class]]){
            
            [developer writeJavaCode];
            
        }else{
            //no such developer}}]; }@end
Copy the code

In the Project class, we first pass the developer’s array into the Project instance object through a constructor method. Then, in the startDeveloping method, the way you iterate through the array and determine the element type lets developers of different types call the corresponding function.

Think about it, what’s wrong with this design?

Problem a:

If the backend development language is changed to GO, then the above code needs to be changed in two places:

  • BackEndDeveloper: One needs to be providedwriteGolangCodeMethods.
  • ProjectOf the classstartDevelopingMethod is going to beBackEndDeveloperOf the classwriteJavaCodetowriteGolangCode.

Problem two:

If the boss requires mobile APP (iOS and Android developers are required), then the above code still needs two changes:

  • Also need to giveProjectClass constructor methodIOSDeveloperandAndroidDeveloperTwo classes. And in accordance with the existing design, but also separately to provide externalwriteSwiftCodeandwriteKotlinCode.
  • ProjectOf the classstartDevelopingWe need two more in the methodelseifJudgment, specialized judgmentIOSDeveloperandAndroidDeveloperThese two classes.

Android code can also be developed in Java, but in order to distinguish between background development code and the use of Kotlin language can also be developed for Android.

Obviously, in both hypothetical scenarios, the high-level module (Project) depends on changes in the low-level module (BackEndDeveloper), so the above design does not comply with the dependency inversion principle.

So how do you design it in accordance with the dependency inversion principle?

The answer is to abstract out the way developers write code, so that the Project class doesn’t depend on the concrete implementation of all the low-level developer classes, but on the abstraction. And from the bottom up, all the low-level developer classes rely on this abstraction to do their work by implementing it.

This abstraction can be done with the interface, can also be done with the abstract class way, here the author uses the interface way to explain:

Better design

First, create an interface with a method called writeCode to writeCode:

//================== DeveloperProtocol.h ==================

@protocol DeveloperProtocol <NSObject>

- (void)writeCode;

@end
Copy the code

Then, let the front-end programmer and back-end programmer classes implement this interface (following this protocol) and do it their way:

Front End Programmer class:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject<DeveloperProtocol>
@end



//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeCode{
    NSLog(@"Write JavaScript code");
}
@end
Copy the code

Backend programmer class:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject<DeveloperProtocol>
@end



//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{
    NSLog(@"Write Java code");
}
@end
Copy the code

Finally, let’s look at the newly designed Project class:

//================== Project.h ==================

#import "DeveloperProtocol.h"

@interface Project : NSObject

// Just pass in an array of developerProtocol-compliant objects
- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers;

// Start development
- (void)startDeveloping;

@end


//================== Project.m ==================

#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray <id <DeveloperProtocol>>* _developers;
}


- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers{
    
    if (self = [super init]) {
        _developers = developers;
    }
    return self;
    
}


- (void)startDeveloping{
    
    // Each loop sends the writeCode method directly to the object
    [_developers enumerateObjectsUsingBlock:^(id<DeveloperProtocol>  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        [developer writeCode];
    }];
    
}

@end
Copy the code

The new Project constructor simply passes in an array of DeveloperProtocol compliant objects. This is also more realistic: you only need to know how to write code to be added to a project.

In the new startDeveloping method, we simply send the writeCode method directly to the current object on each loop, without any need to determine the type of the programmer. This is because the object must conform to the DeveloperProtocol interface, and objects that conform to that interface must implement the writeCode method (even if they don’t, it won’t cause a major error).

Now that the new design has been accepted, let’s make a comparison with the previous design through the two assumptions above:

Hypothesis 1: The backend development language is changed to GO

In this case, simply change the implementation of the writeCode method for the DeveloperProtocol interface in the BackEndDeveloper class:

//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{

    / / Old:
    //NSLog(@"Write Java code");
    
    //New:
    NSLog(@"Write Golang code");
}
@end
Copy the code

You don’t need to change any code in Project, because the Project class only relies on the WriteCode interface method, not on its implementation.

Let’s move on to the second hypothesis:

Hypothesis 2: The boss requires mobile APP (iOS and Android developers are required)

In this new scenario, we just need to implement the writeCode method of the DeveloperProtocol interface for the two newly created developer classes: IOSDeveloper and AndroidDeveloper.

Similarly, the Project interface and implementation code need not be modified: the client simply adds instances of the two new classes to the Project’s build method array parameter, and does not need to add type determination to the startDeveloping method, for the same reason.

As you can see, the new design nicely adds a layer of abstraction between the high level (Project) and the low level (various developer classes), uncoupling them from the old design so that changes in the low level classes do not affect the high level classes.

Also abstract, the new design can also take the form of an abstract class: create an abstract class of Developer and provide a writeCode method, and let different Developer classes inherit from it and implement writeCode methods in their own way. In this case, the constructor in the Project class passes in an array of elements of type Developer. Interested partners can achieve their own ~

Here’s a look at the UML class diagrams for the two designs to see the differences more vividly:

UML class diagram comparison

The principle of unpracticed dependence inversion:

Put into practice the principle of dependence inversion:

In a UML class diagram that practices the principle of inversion, we can see that the Project only relies on the new interface; And the low-level FrondEndDevelope and BackEndDevelope classes implement the interface in their own way: the dependencies are removed through the interface. (In UML class diagrams, the dashed triangle arrow represents the solid line of the interface, with the implementer pointing to the interface.)

How to practice

In the future, when dealing with the interaction of high and low level modules (classes), try to remove the dependency between them through an abstract way, which can be implemented by interface or abstract class.

Principle 4: Interface Segregation Principle

define

Many client specific interfaces are better than one general purpose interface.

That is, multiple specific client interfaces are better than a single universal interface.

Define the interpretation

  • A client should not rely on interfaces it does not need to implement.
  • Instead of creating bloated interfaces, you should refine the interfaces as much as possible with as few methods as possible.

Note that the granularity of the interface should not be too small. If it is too small, the number of interfaces will be too large and the design will be complicated.

advantages

Avoid methods that contain different types of responsibilities in the same interface. The division of interface responsibilities is more clear, in line with the idea of high cohesion and low coupling.

The code on

The interface separation principle is illustrated with a restaurant service example.

Demand point

In addition to providing traditional in-store services, most restaurants now support online ordering and online payment. Write some interface methods to cover all of the restaurant’s order and pay functions.

Bad design

//================== RestaurantProtocol.h ==================

@protocol RestaurantProtocol <NSObject>

- (void)placeOnlineOrder;         // Place an order: online
- (void)placeTelephoneOrder;      // Place an order: by phone
- (void)placeWalkInCustomerOrder; // Place an order: in the store

- (void)payOnline;                // Pay the order: online
- (void)payInPerson;              // Pay the order: pay in store

@end
Copy the code

Here we declare an interface that contains several ways to place an order and pay for it:

  • Place the order:

    • The online order
    • Telephone orders
    • Order in store (in-store service)
  • pay

    • Online payment (for customers who place orders online and over the phone)
    • Pay in store (in-store service)

Let’s not discuss whether customers who order by phone will pay online or in-store.

Correspondingly, we have customers ordering in three ways:

1. Customers who place orders online and pay online

//================== OnlineClient.h ==================

#import "RestaurantProtocol.h"

@interface OnlineClient : NSObject<RestaurantProtocol>
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOnlineOrder{
    NSLog(@"place on line order");
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}
@end

Copy the code

2. Customers who place orders by phone and pay online

//================== TelephoneClient.h ==================

#import "RestaurantProtocol.h"

@interface TelephoneClient : NSObject<RestaurantProtocol>
@end



//================== TelephoneClient.m ==================

@implementation TelephoneClient

- (void)placeOnlineOrder{
    //not necessarily
}

- (void)placeTelephoneOrder{
    NSLog(@"place telephone order");
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}

@end
Copy the code

3. Customers who place orders and pay in the store:

//================== WalkinClient.h ==================

#import "RestaurantProtocol.h"

@interface WalkinClient : NSObject<RestaurantProtocol>
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOnlineOrder{
   //not necessarily
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOnline{
   //not necessarily
}

- (void)payInPerson{
    NSLog(@"pay in person");
}

@end
Copy the code

We found that not all customers need to implement RestaurantProtocol’s methods. Because the design of the interface method creates redundancy, the design does not comply with the interface isolation principle.

Note that protocols in Objective-C can set methods that do not have to be implemented using the @optional keyword. This feature does not conflict with the interface separation principle: interfaces that belong to the same class of responsibilities can be placed in the same interface.

So how do you comply with the interface isolation principle? Let’s look at a better design.

Better design

To comply with the interface isolation principle, you only need to separate different types of interfaces. We split the original RestaurantProtocol interface into two interfaces: the single interface and the payment interface.

Single interface:

//================== RestaurantPlaceOrderProtocol.h ==================

@protocol RestaurantPlaceOrderProtocol <NSObject>

- (void)placeOrder;

@end
Copy the code

Payment interface:

//================== RestaurantPaymentProtocol.h ==================

@protocol RestaurantPaymentProtocol <NSObject>

- (void)payOrder;

@end
Copy the code

Now that we have the order and payment interfaces, we can let different customers implement the order and payment operations in their own way:

First create a parent class for all clients to follow these two interfaces:

//================== Client.h ==================

#import "RestaurantPlaceOrderProtocol.h"
#import "RestaurantPaymentProtocol.h"

@interface Client : NSObject<RestaurantPlaceOrderProtocol.RestaurantPaymentProtocol>
@end
Copy the code

Then the online order, the phone order, and the in-store order customers inherit this parent class, respectively implementing the methods of these two interfaces:

1. Customers who place orders online and pay online

//================== OnlineClient.h ==================

#import "Client.h"
@interface OnlineClient : Client
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOrder{
    NSLog(@"place on line order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
Copy the code

2. Customers who place orders by phone and pay online

//================== TelephoneClient.h ==================
#import "Client.h"
@interface TelephoneClient : Client
@end



//================== TelephoneClient.m ==================
@implementation TelephoneClient

- (void)placeOrder{
    NSLog(@"place telephone order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
Copy the code

3. Ordering and paying customers in the store:

//================== WalkinClient.h ==================

#import "Client.h"
@interface WalkinClient : Client
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOrder{
    NSLog(@"pay in person");
}

@end
Copy the code

Because we separate the interfaces with different responsibilities, the responsibilities of the interfaces are clearer and more concise. Different clients can implement in their own way according to their needs and follow the required interface.

In the future, if there are methods related to order or payment, they can be added to their respective interfaces to avoid interface bloat and improve the cohesion of the program.

Here’s a look at the UML class diagrams for the two designs to see the differences more vividly:

UML class diagram comparison

Unpracticed interface separation principle:

Practice the interface separation principle:

By adhering to the principle of interface separation, interface design is made simpler, and the various customer classes do not need to implement interfaces that they do not need to implement.

How to practice

When designing interfaces, and especially when adding methods to existing interfaces, we need to carefully consider whether these methods handle the same kind of task: if so, they can be grouped together; If not, you need to break it up.

Those of you who do iOS development are familiar with the protocols UITableViewDelegate and UITableView datasource. The methods in both protocols are related to UITableView, but the designers of the iOS SDK put these methods in two different protocols. The reason for this is that the methods contained in these two protocols handle two different tasks:

  • UITableViewDelegate: Contains the method isUITableViewThe instance of tells its agent some click event methods, i.eEvent passingAnd the direction is fromUITableViewTo its proxy.
  • UITableViewDataSource: Contains the method isUITableViewAgent ofUITableViewSome necessary data forUITableViewShow, i.eData transferAnd the direction is fromUITableViewThe agent toUITableView.

Clearly, the designers of the UITableView protocol practiced the principle of interface separation well, and it is worth learning from all of us.

Principle 5: Law of Demeter

define

You only ask for objects which you directly need.

That is, an object should touch as few objects as possible, that is, only those objects that really need to be touched.

Define the interpretation

  • Demeter’s law, also known as the Least Know Principle, states that a class should communicate only with classes in its member variables, method inputs, and return parameters, and should not introduce other classes (indirect communication).

advantages

The practice of Demeter’s law can reduce the coupling between classes, reduce the degree of association between classes, and make the collaboration between classes more direct.

The code on

Let’s illustrate Demeter’s law with a simple example of a car.

Demand point

Design a car class that contains the car’s brand name, engine, and other member variables. Provides a method to return the brand name of the engine.

Bad design

The Car class:


//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

// The constructor
- (instancetype)initWithEngine:(GasEngine *)engine;

// Return the private member variable: instance of the engine
- (GasEngine *)usingEngine;

@end




//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{
    
    self = [super init];
    
    if (self) {
        _engine = engine;
    }
    return self;
}

- (GasEngine *)usingEngine{
    
    return _engine;
}

@end
Copy the code

As you can see, the Car constructor needs to pass in an instance object of the engine. And because the engine instance object is assigned to the private member variable of the Car object. So the Car class provides an external method that returns the engine object: usingEngine.

This engine class GasEngine has a brandName member variable for the brandName:

//================== GasEngine.h ==================
@interface GasEngine : NSObject

@property (nonatomic.copy) NSString *brandName;

@end

Copy the code

In this way, the client can get the engine brand name:

//================== Client.m ==================

#import "GasEngine.h"
#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{

    GasEngine *engine = [car usingEngine];
    NSString *engineBrandName = engine.brandName;// Get the engine brand name
    return engineBrandName;
}
Copy the code

The above design fulfils the requirement, but violates Demeter’s law. The reason is that a GasEngine object that is independent of the input parameter (Car) and return value (NSString) is introduced in findCarEngineBrandName: on the client side. Increased client coupling with GasEngine. This coupling is obviously unnecessary and avoidable.

Let’s look at how we can design to avoid this coupling:

Better design

In the same Car class, we removed the original method that returns the engine object and added a method that returns the engine brand name directly:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

// The constructor
- (instancetype)initWithEngine:(GasEngine *)engine;

// Return the engine brand name
- (NSString *)usingEngineBrandName;

@end


//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{
    
    self = [super init];
    
    if (self) {
        _engine = engine;
    }
    return self;
}


- (NSString *)usingEngineBrandName{
    return _engine.brand;
}

@end
Copy the code

Because direct usingEngineBrandName returns the brand name of the engine directly, you can get the value directly from the client, rather than indirectly through the original GasEngine instance.

Let’s take a look at the changes in client operations:

//================== Client.m ==================

#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{
    
    NSString *engineBrandName = [car usingEngineBrandName]; // Get the engine brand name directly
    return engineBrandName;
}
Copy the code

Unlike previous designs, the GasEngine class is not introduced on the client side, and the required data is retrieved directly from the Car instance.

The advantage of this design is that if the engine of the car is changed to electric (the GasEngine class is changed to ElectricEngine class), the client code can be left unchanged! Because it doesn’t introduce any engine classes, it gets the brand name of the engine directly.

In this case, we just need to change the usingEngineBrandName implementation of the Car class to return the brand name of the new engine.

Here’s a look at the UML class diagrams for the two designs to see the differences more vividly:

UML class diagram comparison

Not practicing Demeter’s law:

Practiced Demeter’s law:

Obviously, in a UML class diagram that implements Demeter’s law, the coupling is reduced without the Client’s reliance on GasEngine.

How to practice

In the future, when designing the interaction between objects and objects, we should try to avoid the situation of introducing intermediate objects (classes that need to import other objects) : what objects need to be returned directly, so as to reduce the coupling degree between classes.

Principle 6: Liskov Substitution Principle

define

In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)

That is, all references to the base class must transparently use objects of its subclass. That is, objects of its subclass can replace objects of its parent class without changing the performance of the program.

Interpretation of definitions

In the inheritance system, the subclass can add its own methods or implement the abstract methods of the parent class, but it cannot override the non-abstract methods of the parent class, otherwise the inheritance relationship is not a correct inheritance relationship.

advantages

You can check the correctness of inheritance use and limit the overflow of inheritance in use.

The code on

The Richter substitution principle is illustrated here with a simple rectangle and square example.

Demand point

Create two classes: rectangle and square, both of which can set width and height (side length) and output area size.

Bad design

First declare a rectangle class, and then let the square class inherit from the rectangle.

Rectangular class:

//================== Rectangle.h ==================

@interface Rectangle : NSObject
{
@protected double _width;
@protected double _height;
}

// Set the width and height
- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

// Get width and height
- (double)width;
- (double)height;

// Get the area
- (double)getArea;

@end



//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}


- (double)getArea{
    return _width * _height;
}

@end
Copy the code

Square class:

//================== Square.h ==================

@interface Square : Rectangle
@end



//================== Square.m ==================

@implementation Square

- (void)setWidth:(double)width{
    
    _width = width;
    _height = width;
}

- (void)setHeight:(double)height{
    
    _width = height;
    _height = height;
}

@end
Copy the code

As you can see, since the square class inherited the Rectangle class, in order to ensure that the sides are always the same length, the square class deliberately forces the width and height to be set to the values passed in. This overwrites the two set methods of the parent Rectangle class. However, the Richter substitution principle states that a subclass cannot override a method of its parent class, so the above design violates this principle.

In addition, the Richter substitution principle includes: a subclass object can replace a parent object, and the program execution effect remains unchanged. Let’s see if the above design fits through an example:

Write a method on the client class: pass a Rectangle type and return its area:

- (double)calculateAreaOfRect:(Rectangle *)rect{
    return rect.getArea;
}
Copy the code

Let’s first try using a Rectangle object:

Rectangle *rect = [[Rectangle alloc] init];
rect.width = 10;
rect.height = 20;
    
double rectArea = [self calculateAreaOfRect:rect];//output:200
Copy the code

After the length and width are set to 10 and 20, the output is 200, no problem.

Now let’s use the Rectange’s subclass Square to replace the original Rectange object, and see what happens:

Square *square = [[Square alloc] init];
square.width = 10;
square.height = 20;
    
double squareArea = [self calculateAreaOfRect:square];//output:400
Copy the code

The result output is 400. The result is inconsistent, which again indicates that the above design does not comply with the Richter substitution principle, because after the subclass’s object square replaces the superclass’s object rect, the result of program execution changes.

If it does not conform to the Richter substitution principle, the inheritance relationship is not correct. That is to say, the square class cannot inherit from the rectangle class, and the program needs to be redesigned.

Now let’s look at some of the better designs.

Better design

Since a square cannot inherit from a rectangle, can both inherit from another parent class? The answer is yes.

Since they inherit from the other parent class, their parent class must have what both shapes have in common: four edges. So let’s define a Quadrangle class: Quadrangle.

//================== Quadrangle.h ==================

@interface Quadrangle : NSObject
{
@protected double _width;
@protected double _height;
}

- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

- (double)width;
- (double)height;

- (double)getArea;
@end
Copy the code

Next, let the Rectangle and Square classes inherit from it:

A Rectangle class:

//================== Rectangle.h ==================

#import "Quadrangle.h"

@interface Rectangle : Quadrangle

@end



//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}


- (double)getArea{
    return _width * _height;
}

@end
Copy the code

Square:

//================== Square.h ==================

@interface Square : Quadrangle
{
    @protected double_sideLength; } - (void)setSideLength:(double)sideLength; - (double)sideLength;

@end



//================== Square.m ==================

@implementation Square- (void)setSideLength:(double)sideLength{ _sideLength = sideLength; } - (double)sideLength{
    return _sideLength;
}

- (void)setWidth:(double)width{
    _sideLength = width;
}

- (void)setHeight:(double)height{
    _sideLength = height;
}

- (double)width{
    return _sideLength;
}

- (double)height{
    return _sideLength;
}


- (double)getArea{
    return _sideLength * _sideLength;
}

@end
Copy the code

As you can see, the Rectange and Square classes implement the public methods of their parent class Quadrangle in their own way. And because Square is special, it also declares its own unique member variable, _sideLength, and its corresponding public method.

Note that here, Rectange and Square do not override the public methods of their parent class, but rather implement their abstract methods.

Here’s a look at the UML class diagrams for the two designs to see the differences more vividly:

UML class diagram comparison

Not practicing the Richter substitution principle:

The Richter substitution principle is practiced:

How to practice

The Substitution principle of Richter is a test of inheritance: to test whether it really conforms to inheritance in order to avoid the abuse of inheritance. Therefore, before using inheritance, it is necessary to think and confirm whether the inheritance relationship is correct or whether the current inheritance system can support the subsequent requirement changes. If not, it is necessary to refactor in time to adopt a better way to design the program.

The last word

That concludes the presentation of the six design principles. The Demo and UML class diagrams shown in this article are in a dedicated GitHub library maintained by the author: Object-Oriented Design. If you want to see the Demo and the UML diagram, you can click on the link. Welcome to the fork, welcome to the PR that gives different examples of more languages, and will add code and UML class diagrams for design patterns later.

One final point to emphasize about these design principles is that while design principles are the cornerstone of design patterns, it is difficult to get all of them to be satisfied in a single design in actual development. Therefore, we need to grasp the particularity of specific design scenarios and selectively follow the most appropriate design principles.

This section has been synchronized to my blog: Six Design Principles for Object-oriented Design (with Demo and UML class diagrams)


The author recently opened a personal public account, mainly to share programming, reading notes, thinking articles.

  • Programming articles: a selection of my previous technical articles, as well as subsequent technical articles (mainly original), will gradually move away from iOS content and focus on improving programming capabilities.
  • Reading notes: Share your reading notes on programming, thinking, psychology, and workplace books.
  • Thinking articles: share my thoughts on technology and life.

Because there is a limit to the number of messages published on the official account every day, so far we have not published all the selected articles in the past on the official account, and will be published gradually in the future.

And because of the various limitations of the major blog platform, later will be published on the public account some short and concise, to see the big dry goods articles oh ~

Scan the qr code below and click follow, looking forward to growing together with you ~