preface

Some programming principles need to be considered when designing applications to build reusable and maintainable applications. Familiarity with common design principles can also lay the foundation for learning design patterns.

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 Rule (least Know rule)
ISP Interface Segregation Principle Interface separation principle
DIP Dependency Inversion Principle Dependency inversion principle

Note that the design principles commonly referred to as SOLID (the initials of the table above, from top to bottom) do not contain Demeter’s law, but only the other five. In addition, this paper does not include the synthetic/polymeric reuse principle (CARP), as I believe it is not typical of the other six principles and is not easily violated in practice. Interested students can learn by themselves.

In each design pattern presentation, one application scenario (requirement point) is described, followed by a comparison of the code of the two designs: the code of the relatively bad design is presented first, and the code of the relatively good design is presented later.

Six Design principles

This article explains the six design principles in rough order of difficulty. The open close principle is explained here first, because it is simple to understand and is the cornerstone of other design principles.

Note:

  1. The examples used in the explanation of the six principles are not related to each other, so the order of reading can be determined by the reader’s preference.
  2. Interfaces in the Java language are called protocols in Objective-C. The Demo is written in Objective-C, but because protocol is called by a few people, we will use the term interface instead of protocol.

Principle 1: Open Close Principle

define

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

That is, software entities such as classes, modules, and functions should be open for extension and closed for modification.

Interpretation of definitions

  • Build the framework with abstractions and extend the details with implementations.
  • New requirements should not be implemented by modifying existing classes, but rather by implementing preabstracted interfaces (or concrete classes inheriting abstract classes).

advantages

The advantage of practicing the open close principle is that you can extend functionality without changing the original code. The expansibility of the program is increased, and the maintenance cost of the program is reduced.

The code on

Here is a simple example of an online course that illustrates the open close principle in practice.

Demand point

Designing an online class:

Due to limited teaching resources, at the beginning, there were only text-based courses, similar to blogs. 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

The first word course:

//================== Course.h ================== @interface Course : NSObject @property (nonatomic, copy) NSString *courseTitle; @property (nonatomic, copy) NSString *courseIntroduction; @property (nonatomic, copy) NSString *teacherName; @property (nonatomic, copy) NSString *content; // course content @end copy the codeCopy the code

The Course class declares the data required for the initial online Course:

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

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

//================== Course.h ================== @interface Course : NSObject @property (nonatomic, copy) NSString *courseTitle; @property (nonatomic, copy) NSString *courseIntroduction; @property (nonatomic, copy) NSString *teacherName; @property (nonatomic, copy) NSString *content; @property (nonatomic, copy) NSString *videoUrl; @property (nonatomic, copy) NSString *audioUrl; @property (nonatomic, copy) NSString *liveUrl; @end copies the codeCopy the code

The three newly added courses all add corresponding urls in the original Course class. In other words, every time a new type of Course is added, it will be modified in the original Course class: the data required by this kind of Course will be added.

As a result, the video Course object instantiated from the Course class will contain data that does not belong to it: audioUrl and liveUrl: this causes redundancy. The video Course object is not a pure video Course object, but contains audio address, live address and other members.

Obviously, this design is not a good design because (corresponding to the above two statements) :

  1. As the requirements increase, the previously created classes need to be modified repeatedly.
  2. Creating unnecessary redundancy for new classes.

The two flaws are caused by the fact that instead of following the open and closed principle of closed for changes and open for extensions, the design does the opposite: open for changes and does not facilitate extensions.

How is it difficult to follow the open close principle? Here’s a look at a good design that follows the open close principle:

Better design

First, in the Course class, only the data contained in all courses will be retained:

//================== Course.h ================== @interface Course : NSObject @property (nonatomic, copy) NSString *courseTitle; @property (nonatomic, copy) NSString *courseIntroduction; @property (nonatomic, copy) NSString *teacherName; // Lecturer name copy codeCopy the code

Then, for the text Course, video Course, audio Course and live Course, the inheritance of Course is adopted. And after inheritance, add its own unique data:

Text courses:

//================== TextCourse.h ================== @interface TextCourse : Course @property (nonatomic, copy) NSString *content; // Text @end copies the codeCopy the code

Video courses:

//================== VideoCourse.h ================== @interface VideoCourse : Course @property (nonatomic, copy) NSString *videoUrl; // The video address @end copies the codeCopy the code

Audio courses:

//================== AudioCourse.h ================== @interface AudioCourse : Course @property (nonatomic, copy) NSString *audioUrl; // The audio address @end copies the codeCopy the code

Live courses:

//================== LiveCourse.h ================== @interface LiveCourse : Course @property (nonatomic, copy) NSString *liveUrl; // Live address @end copies the codeCopy the code

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

  1. As the class type increases, there is no need to repeatedly modify 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 can have hd address, video acceleration function. You only need to add one of these options to the VideoCourse category, since they are unique to video courses. Likewise, live courses can also be followed by an online q&A feature, which can be added only to LiveCourse.

As we can see, it is because the initial program design is sound that the subsequent increase in requirements is handled well.

In the UML class diagram that practices the open and closed principle, four Course classes inherit the Course class and add their own unique attributes.

How to practice

In order to best practice the open close principle, it is important at the beginning of the design to figure out which data (or behaviors) in the scenario are fixed (or hard to change) and which are easily changeable. The latter can be abstracted into interfaces or abstract methods so that different requirements can be addressed in the future by creating concrete implementations.

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 reason for the change of that class.

Interpretation of definitions

  • Changes in class responsibilities are often the cause of class changes: that is, if a class has more than one responsibility, there will be more than one reason for that class to change, making maintenance of that class difficult.
  • Often in software development, as the requirements continue to increase, it is possible to add some responsibilities to the original class that do not belong to it, thus violating the single responsibility principle. If we find that the current class has more than one responsibility, we should separate out responsibilities that are not really part of the class.
  • Not only classes, but also functions (methods) follow the single responsibility principle: a function (method) does only one thing. If you find a function (method) with different tasks, you need to separate out the different tasks in the form of another function (method).

advantages

A clear division of responsibilities between classes and methods not only improves the readability of the code, but also substantially reduces the risk of errors, because clear code leaves bugs nowhere to hide and facilitates bug tracking, which in turn reduces maintenance costs.

The code on

The demo of the single responsibility principle is simple enough to cover the design of objects (properties) without the need for specific client calls. Let’s first look at the demand points:

Demand point

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

New requirements: Add two methods:

  • Determine whether an employee will be promoted this year
  • Calculate employee salaries

Let’s start with bad design:

Bad design

//================== Employee.h ================== @interface Employee : NSObject / / = = = = = = = = = = = = initial requirements = = = = = = = = = = = = @ property (nonatomic, copy) nsstrings * name; @property (nonatomic, copy) NSString *address; @property (nonatomic, copy) NSString *employeeID; / / / / employee ID = = = = = = = = = = = = new demand = = = = = = = = = = = = / / calculate salary - (double) calculateSalary; // Whether to be promoted this year - (BOOL)willGetPromotionThisYear; @end copies the codeCopy the code

As can be seen from the above code:

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

The new requirements approach seems fine because they are employee-related, but it violates the single responsibility principle: the two methods are not the responsibility of the employee.

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

However, the above design imposes responsibilities that do not belong to the employees themselves into the employee class, and the original purpose of this class (original responsibilities) is simply to retain some information of the employees. So doing so introduces new responsibilities to the class, and thus violates the single responsibility principle.

One simple way to think about this is to change the current class if the employee promotion mechanism changes, or if tax policies and other factors that affect employee salaries change.

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

Better design

We keep basic information for employees:

//================== Employee.h ================== @interface Employee : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *address; @property (nonatomic, copy) NSString *employeeID; Copy the codeCopy the code

Then create a new accounting department class:

/ / = = = = = = = = = = = = = = = = = = FinancialApartment. H = = = = = = = = = = = = = = = = = = # import "Employee. H / / accounting department class @ interface FinancialApartment : NSObject // calculateSalary - (double)calculateSalary: Employee *) Employee; @end copies the codeCopy the code

And Personnel Department:

/ / = = = = = = = = = = = = = = = = = = HRApartment. H = = = = = = = = = = = = = = = = = = # import "Employee. H / / hr category" @ interface HRApartment: NSObject / / promotions this year - (BOOL) willGetPromotionThisYear: the Employee *) the Employee; @end copies the codeCopy the code

The two tasks (responsibilities) were separated by creating the classes FinancialApartment and HRApartment, which respectively specialized in handling salaries and promotions, accounting and personnel, leaving the duties to be handled by the classes that were supposed to handle them.

In this way, the principle of single responsibility is not only satisfied in the new requirement, but also can be directly added to the hr and accounting departments if additional tasks are needed in the future.

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

Here’s a quick example:

The default navigation bar for your APP looks like this:

  • white
  • Black title
  • There’s a shadow on the bottom

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

/ / the default style of the navigation bar - (void) createDefaultNavigationBarWithTitle title: (nsstrings *) {/ / create white color background view / / create Black color title //create shadow bottom} Copy the codeCopy the code

Now we can use this method to create a unified default navigation bar. But after a while, a new requirement came, some pages need to make the navigation bar transparent, so we need a transparent style navigation bar:

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

For this requirement, we can add a method:

/ / transparent style navigation bar - (void) createTransParentNavigationBarWithTitle title: (nsstrings *) {/ / create transparent color background view //create white color title} copy the codeCopy the code

See the problem? In the two methods, the difference between the methods to create background view and title color title is only the color difference, and the other parts of the code is repeated. So we should pull these two methods out:

/ / according to the color of the incoming parameter sets the background color of the navigation bar - (void) createBackgroundViewWithColor: UIColor) color; / / parameter is set according to the title of the incoming string and color title - (void) createTitlewWithColorWithTitle title: (nsstrings *) color: UIColor) color; Copy the codeCopy the code

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

- (void)createShadowBottom; Copy the codeCopy the code

Thus, the original two methods can be written as:

/ / the default style of the navigation bar - (void) createDefaultNavigationBarWithTitle title: (nsstrings *) {/ / set white background [the self createBackgroundViewWithColor:[UIColor whiteColor]]; / / set the black title [self createTitlewWithColorWithTitle: the title color: [UIColor blackColor]]. // Set bottom shadow [self createShadowBottom]; } / / transparent style navigation bar - (void) createTransParentNavigationBarWithTitle title: (nsstrings *) {/ / set transparent background [the self createBackgroundViewWithColor:[UIColor clearColor]]; / / set white title [self createTitlewWithColorWithTitle: the title color: [UIColor whiteColor]]. } Duplicate codeCopy the code

And we can also take the inside method out and call it from outside:

Set the default style for the navigation bar:

/ / set the white background [navigationBar createBackgroundViewWithColor: [UIColor whiteColor]]. / / set the black title [navigationBar createTitlewWithColorWithTitle: the title color: [UIColor blackColor]]. // Set shadow [navigationBar createShadowBottom]; Copy the codeCopy the code

Set the transparent style navigation bar:

/ / set the background transparent color [navigationBar createBackgroundViewWithColor: [UIColor clearColor]]. / / set white title [navigationBar createTitlewWithColorWithTitle: the title color: [UIColor whiteColor]]. Copy the codeCopy the code

This way, whether called inside a large method or outside it, you can clearly see 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. And there will probably be more navigation styles in the future, so we just need to change the color values passed in separately, without a lot of repetitive code, and it will be easy to change.

How to practice

As for the employee example above, it may be because we know the proper organizational structure of a company and take it for granted. In practice, however, it is very easy to combine different responsibilities, which should be noted by developers.

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 abstraction, not implementation.
  • Abstraction 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 for interfaces, not implementations.
  • Try not to derive from concrete classes, but instead implement 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-making ability. The business layer is naturally placed in the upper module, and the logical layer and data layer are naturally categorized as the bottom layer.

advantages

Through abstraction to build the framework, the establishment of class and class association, in order to reduce the coupling between classes. Moreover, the system built by abstraction is more stable, more expansible and easier to maintain than the system built by concrete implementation.

The code on

The dependency inversion principle is illustrated with a simulated project development example.

Demand point

Implement the following requirements:

The code simulates a real project development scenario where front-end and back-end developers work on the same project.

Bad design

Start by generating two classes for front-end and back-end developers:

Front-end developer:

//================== FrondEndDeveloper.h ================== @interface FrondEndDeveloper : NSObject - (void)writeJavaScriptCode; @end //================== FrondEndDeveloper.m ================== @implementation FrondEndDeveloper - (void)writeJavaScriptCode{ NSLog(@"Write JavaScript code"); } @end copies the codeCopy the code

Backend developers:

//================== BackEndDeveloper.h ================== @interface BackEndDeveloper : NSObject - (void)writeJavaCode; @end //================== BackEndDeveloper.m ================== @implementation BackEndDeveloper - (void)writeJavaCode{ NSLog(@"Write Java code"); } @end copies the codeCopy 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 an array of developers - (instancetype)initWithDevelopers:(NSArray *)developers; // startDeveloping - (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 copies the codeCopy the code

In the Project class, we first pass the developer’s array to the Project instance object through a constructor method. Then, in the method startDeveloping, which I started developing, I iterated through groups of numbers and determined the type of elements in a way that allowed different types of developers to call their own functions.

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

Problem a:

If the background development language is changed to GO, the above code needs to be changed in two ways:

  • BackEndDeveloper: One needs to be provided externallywriteGolangCodeMethods.
  • ProjectOf the classstartDevelopingThe method needs to beBackEndDeveloperOf the classwriteJavaCodetowriteGolangCode.

Problem two:

If the boss later requires to make mobile APP (iOS and Android developers), then the above code still needs to be changed in two places:

  • Also need to giveProjectClass in the constructor methodIOSDeveloperandAndroidDeveloperTwo classes. And according to the existing design, it has to be provided externally separatelywriteSwiftCodeandwriteKotlinCode.
  • ProjectOf the classstartDevelopingWe need two more in the methodelseifJudge, specialize in judgmentIOSDeveloperandAndroidDeveloperThese two classes.

Android code can also be developed in Java, but to distinguish it from background development code, Kotlin language, which can also develop Android, is used here.

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

So how can the design comply with the dependency inversion principle?

The answer is to abstract away the way developers write code so that the Project class no longer relies on the concrete implementation of all the low-level developer classes, but rather on abstraction. And from the bottom up, all the underlying developer classes rely on this abstraction and implement it to do their job.

This abstraction can be done by using interfaces, or by using abstract classes. Here I use the way of using interfaces to explain:

Better design

First, create an interface that contains a method called writeCode:

//================== DeveloperProtocol.h ================== @protocol DeveloperProtocol <NSObject> - (void)writeCode; @end copies the codeCopy the code

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

Front-end programmer classes:

//================== FrondEndDeveloper.h ================== @interface FrondEndDeveloper : NSObject<DeveloperProtocol> @end //================== FrondEndDeveloper.m ================== @implementation FrondEndDeveloper - (void)writeCode{ NSLog(@"Write JavaScript code"); } @end copies the codeCopy the code

Backend programmer classes:

//================== BackEndDeveloper.h ================== @interface BackEndDeveloper : NSObject<DeveloperProtocol> @end //================== BackEndDeveloper.m ================== @implementation BackEndDeveloper - (void)writeCode{ NSLog(@"Write Java code"); } @end copies the codeCopy 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 objects that follow the DeveloperProtocol - (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers; // startDeveloping - (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{// For each loop, send the writeCode method to the object. Don't need to decide [_developers enumerateObjectsUsingBlock: ^ (id < DeveloperProtocol > _Nonnull developer, NSUInteger independence idx. BOOL * _Nonnull stop) { [developer writeCode]; }]; } @end copies the codeCopy the code

The new Project constructor simply passes in an array of developerProtocol-compliant objects. This is also more realistic: you only need to be able to write code to be part of a project.

The new startDeveloping method, on the other hand, sends the writeCode method directly to the current object each time through the loop, regardless of the programmer’s type. Because the object must follow the DeveloperProtocol interface, and objects that follow that interface must implement the writeCode method (without causing major errors).

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 background development language is changed to GO

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

/ / = = = = = = = = = = = = = = = = = = BackEndDeveloper. M = = = = = = = = = = = = = = = = = = @ implementation BackEndDeveloper - (void) writeCode {/ / Old:  //NSLog(@"Write Java code"); //New: NSLog(@"Write Golang code"); } @end copies the codeCopy the code

There is no need to change any code in Project because the Project class only relies on the interface method WriteCode, not its implementation.

Let’s take a look at the second hypothesis:

Hypothesis 2: The boss requires us to make mobile APP (iOS and Android developers are needed)

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, respectively.

Similarly, neither the interface nor the implementation code of Project needs to be modified: the client simply needs to add instances of the two new classes to the array parameters of the Project build method, and does not need to add type judgment to the startDeveloping method for the same reason.

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

As an abstraction, the new design can also be used as an abstract class: create an abstract class of Developer and provide a writeCode method, which can be inherited by different Developer classes and implemented in their own way. In this case, the constructor in the Project class is passing in an array of elements of type Developer. Interested partners can achieve their own ~

In code that practices the dependency inversion principle, we can see that the Project only relies on the new interface; And the low-level FrondEndDevelope and BackEndDevelope classes implement this interface in their own way: by using the interface to remove the dependency

How to practice

In the future, when dealing with the interaction of high and low level modules (classes), we should try our best to remove the dependence of the two by abstract means, which can be implemented through interfaces or abstract classes.

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 one universal master interface.

Define the interpretation

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

Note that the granularity of interfaces should not be too small. If the size is too small, the number of interfaces is too large, which complicates the design.

advantages

Avoid the method that the same interface contains different kinds of responsibilities, and the interface responsibility division is more clear, in line with the idea of high cohesion and low coupling.

The code on

The following illustrates the interface separation principle using a restaurant service example.

Demand point

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

Bad design

//================== RestaurantProtocol.h ================== @protocol RestaurantProtocol <NSObject> - (void)placeOnlineOrder; // Place the order: online - (void)placeTelephoneOrder; // Place an order: by phone - (void)placeWalkInCustomerOrder; // Place an order: in the store - (void)payOnline; // Pay order: online - (void)payInPerson; // Pay the order: pay @end in the store to copy the codeCopy the code

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

  • Place the order:

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

    • Online payment (for online and phone orders)
    • In-store Payment (In-store service)

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

Accordingly, we have customers who place orders in three ways:

1. Customers who place orders 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 codeCopy the code

2. Customers who order 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 codeCopy the code

3. Customers who order 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 copies the codeCopy the code

We found that not all customers have to implement all the methods in RestaurantProtocol. Because of the redundancy caused by the design of the interface method, the design does not comply with the principle of interface isolation.

Note that protocols in Objective-C can use the @optional keyword to set methods that do not need to be implemented. This feature does not conflict with interface separation: interfaces that belong to the same class of responsibilities can be put into 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 interfaces of different types. We split the original RestaurantProtocol interface into two interfaces: the order interface and the payment interface.

Single interface:

//================== RestaurantPlaceOrderProtocol.h ================== @protocol RestaurantPlaceOrderProtocol <NSObject>  - (void)placeOrder; @end copies the codeCopy the code

Payment interface:

//================== RestaurantPaymentProtocol.h ================== @protocol RestaurantPaymentProtocol <NSObject> - (void)payOrder; @end copies the codeCopy the code

Now with the order and payment interfaces, we can let different customers order and pay in their own way:

Start by creating a parent class for all customers that follows these two interfaces:

//================== Client.h ================== #import "RestaurantPlaceOrderProtocol.h" #import "RestaurantPaymentProtocol.h" @interface Client : NSObject < RestaurantPlaceOrderProtocol RestaurantPaymentProtocol > @ end duplicate codeCopy the code

Then another online order, phone order, in-store order customer inherit this parent class, respectively to implement the two interface method:

1. Customers who place orders 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 copies the codeCopy the code

2. Customers who order 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 copies the codeCopy the code

3. Place orders and pay 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 copies the codeCopy the code

Because we separate the interfaces of different responsibilities, the responsibilities of the interfaces are more clear and concise. Different clients can implement it in their own way according to their own requirements, following the required interface.

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

By adhering to the principle of interface separation, interface design becomes more concise, and the various client 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 address the same kind of task: if so, they can be put together; If not, you need to split it.

Those of you doing iOS development should be familiar with the UITableViewDelegate and UITableViewDataSource protocols. The methods in both protocols are UITableView related, but the iOS SDK designers put these methods in two different protocols. The reason is that the two protocols contain methods that handle different kinds of tasks:

  • UITableViewDelegate: Contains the method isUITableViewThe instance tells its agent about some click event methods, that isEvent passing, the direction is fromUITableViewTo its proxy.
  • UITableViewDataSource: Contains the method isUITableViewProxy toUITableViewSome necessary data forUITableViewDisplay, i.eTransmission of data, the direction is fromUITableViewThe agent toUITableView.

Obviously, the designers of the UITableView protocol have implemented the principle of interface separation well, which is worth learning from.

Principle 5: The 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

  • Also known as the Least Know Principle, 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

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

The code on

Demeter’s law is explained by a simple example of a car.

Demand point

Design a car class, including the car brand name, engine and other member variables. Provide a method to return the brand name of the engine.

Bad design

The Car class:

//================== Car.h ================== @class GasEngine; @interface Car: NSObject - (instanceType)initWithEngine:(GasEngine *)engine; // Return a private member variable: an 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 copies the codeCopy the code

As you can see above, the Car constructor requires passing 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 to return an engine object: usingEngine.

The GasEngine class has a member variable named brandName:

//================== GasEngine.h ================== @interface GasEngine : NSObject @property (nonatomic, copy) NSString *brandName; @end copies the codeCopy the code

This way, the client can get the brand name of the engine:

//================== Client.m ================== #import "GasEngine.h" #import "Car.h" - (NSString *)findCarEngineBrandName:(Car *)car{ GasEngine *engine = [car usingEngine]; NSString *engineBrandName = engine.brandName; Return engineBrandName; } Duplicate codeCopy the code

The above design fulfils the requirements, but violates Demeter’s law. The reason is that GasEngine objects are introduced in findCarEngineBrandName: on the client side that are independent of the input parameter (Car) and return value (NSString). Added client-side coupling to GasEngine. This coupling is clearly unnecessary and avoidable.

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

Better design

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

//================== Car.h ================== @class GasEngine; @interface Car: NSObject - (instanceType)initWithEngine:(GasEngine *)engine; - (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 copies the codeCopy the code

Because usingEngineBrandName directly returns the brand name of the engine, it can be retrieved directly from the client rather than from 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]; Return engineBrandName; } Duplicate codeCopy the code

Unlike the previous design, in the client side, the GasEngine class is not introduced, but the required data is directly obtained through the Car instance.

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

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

Obviously, in code that practices Demeter’s law, the Client’s dependence on GasEngine is removed and the coupling is reduced.

How to practice

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

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 a base class must be able to use objects of its subclass transparently, that is, subclass objects can replace their parent class objects without the effect of program execution.

Interpretation of definitions

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

advantages

It can check the correctness of inheritance and restrain the overflow of inheritance in use.

The code on

Here is a simple rectangle and square example to explain the Richter substitution principle.

Demand point

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

Bad design

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

Rectangle class:

//================== Rectangle.h ================== @interface Rectangle : NSObject { @protected double _width; @protected double _height; } - (void)setWidth:(double)width; - (void)setHeight:(double)height; - (double)width; - (double)height; // getArea - (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 copies the codeCopy the code

Square:

//================== 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 copies the codeCopy the code

Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class: Rectangle class. However, the Richter substitution rule states that subclasses cannot override methods of their parent class, so the above design violates this rule.

In addition, the Richter’s substitution principle states that subclass objects can replace superclass objects, and the program execution remains the same. Let’s use an example to see if the above design fits:

On the client class, write a method that passes in a Rectangle type and returns its area:

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

Let’s try this with a Rectangle object:

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

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

Now let’s replace the Rectange with Square, a subclass of Rectange, and see what happens:

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

If rect is replaced by square, the result will change. If rect is replaced by square, the result will change.

If it does not comply with Richter’s substitution principle, the inheritance relation 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 a better design.

Better design

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

Since they inherit from other superclasses, their superclass must have something in common with both shapes: four edges. So let’s define a Quadrangle class.

//================== 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 copies the codeCopy 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 copies the codeCopy 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 copies the codeCopy the code

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

Note that here Rectange and Square do not rewrite their parent’s public method, but implement its abstract method.

How to practice

Richter’s substitution principle is a test of inheritance relation: to check whether it really conforms to inheritance relation, so as to avoid the abuse of inheritance. Therefore, before using inheritance, it is necessary to repeatedly consider and confirm whether the inheritance relationship is correct, or whether the current inheritance system can support subsequent requirement changes. If not, it is necessary to timely reconstruct and adopt a better way to design the program.

The last word

One final point to note about these design principles is that design principles are the cornerstone of design patterns, but it is difficult to satisfy them all in a single design in actual development. Therefore, we need to grasp the particularity of specific design scenes and selectively follow the most appropriate design principles.

Original link :juejin.cn/post/684490… Copy over, for reference and study, thank the author