😄 this semester opened a new course “Software Architecture and Design Patterns”, before this, I also have a little understanding of design patterns, so I can take this semester’s course to re-learn the necessary course for the path to the architect.

😝 To that end, I’ve created a new column, Relearning Design Patterns, to document and reinforce the learning process.

⚡ This column takes you through understanding the role of design patterns in building quality software, applying common design patterns to specific problems (GOF23), and understanding common and newer software system architecture patterns (microservices architecture, cloud native architecture).

😎 If you want to revisit the design patterns of your college years and change the bad taste of your code, follow my lead and re-learn design patterns!

What are design patterns?

A design pattern is a solution to a recurring problem, not a ready-to-use class or library function, that has been developed over a long period of trial and error by many software developers.

Its purpose is to improve code reusability and maintainability, make code easier to understand and ensure code reliability.

In software engineering, software design patterns are common, reusable solutions to common problems in software design in a given context. It is not a complete design that can be directly translated into source code or machine code. It is a description or template for how to solve a problem that can be used in many different situations.

⭐ Design patterns as distinct from algorithms and software system architectures:

  • Algorithm refers to the accurate and complete description of the solution, is a series of clear instructions to solve the problem constituted by the program;
  • Design patterns are a general template/guideline for common problems and a higher level of abstraction;
  • Software system architecture is used to guide the design of various aspects of large-scale software systems. It is a higher level of abstraction over design patterns.

What are object-oriented design principles?

(1) Object-oriented design principle is the foundation of learning design pattern, and the essence of design pattern is the practical application of object-oriented design principle;

(2) Each of the following design patterns conforms to one or more object-oriented design principles;

(3) Each principle covers some object-oriented design ideas, which can improve the design level of a software structure from different angles.

💧 “object-oriented design principles” and “design patterns” are actually the compass for a reasonable “refactoring” of the system; Refactoring is also a deep science, which seeks to modify code to improve the internal structure of the program without changing the external behavior of the code.

Without further ado, here are seven commonly used object-oriented design principles. These principles are not isolated from each other; they are interdependent and complementary.

Introduction to Object-oriented Design Principles (Table)

Name of design Principle Introduction to Design Principles The importance of
Single Responsibility Principle (SRP) The responsibilities of a class should be single. You should not put too many responsibilities into one class ⭐ ⭐ ⭐ ⭐
Open-closed Principle (OCP) Software entities are open to extension, but closed to modification, that is, to extend functionality without modifying a software entity ⭐ ⭐ ⭐ ⭐ ⭐
Liskov Substitution Principle (LSP) In a software system, a place that can accept a base class object must accept a subclass object ⭐ ⭐ ⭐ ⭐
Dependency Inversion Principle (DIP) Program for an abstraction layer, not a concrete class ⭐ ⭐ ⭐ ⭐ ⭐
Interface Segregation Principle (ISP) Use multiple specialized interfaces instead of one unified interface ⭐ ⭐
Composite Reuse Principle (CRP) When reusing functionality, composite and aggregate associations should be used as much as possible, and inheritance relationships should be used as little or no ⭐ ⭐ ⭐ ⭐
Law of Demeter (LoD) The fewer references one software entity makes to other entities the better, or if two classes do not have to communicate directly with each other, they should not interact directly, but indirectly by introducing a third party ⭐ ⭐ ⭐

🌈 Single responsibility principle, open closed principle, Richter substitution principle, dependency reversal principle, interface isolation principle are also called SOLID principle.

🚁UML class diagram preknowledge

Here’s a quick overview of what the various lines and arrows in a UML class diagram mean to help you better understand the interaction between classes in the following class diagram.

associated

An association is used to represent a static structure between classes, and is a relatively “strong” relationship.

For example, object B is A member of object A

inheritance

Inheritance, also known as generalization, is used to indicate the relationship between a subclass and its parent class.

Solid line – Hollow arrow that points to base class

implementation

An implementation is a relationship between classes and interfaces.

Dotted line – Hollow arrow pointing to interface

Rely on

A dependency describes an object of a class that may use another object at runtime. This is a “weak” relationship, unlike correlation.

For example, the parameter to A member method of class A is an instance object of class B

combination

Combination is a relationship between the whole and the parts, and the parts cannot exist alone without the whole. It is a stronger relationship than aggregation.

A solid line with a solid diamond pointing towards the whole

The aggregation

Aggregation also describes the relationship between the whole and the parts, and the parts can exist independently of the whole, different from the combination.

A solid line with a hollow diamond pointing towards the whole

😐 Single responsibility principle

define

The single responsibility principle is the simplest object-oriented design principle, which is used to control the size of class granularity. It suggests that an object should contain only a single responsibility.

The more responsibilities a class has, the less likely it is to be reused because those responsibilities are coupled to the same class.

The single responsibility principle is designed to achieve high cohesion and low coupling. It is found in many code refactoring techniques. It is a simple idea but the most difficult to use because you need to be analytical to separate a highly coupled class into several separate classes with specific responsibilities.

Case analysis

Use a simple practical example to illustrate the guiding principles of the single responsibility principle.

🔎 A C/S system is designed to provide graphical user interface (GUI) login function. The original design is as follows.

The original class diagram

As shown above, the Login class method:

  • init(): Initializes controls such as buttons and text boxes
  • display()Add interface controls to the interface container and display Windows
  • validate(): called by the login button to verify the login
  • getConnection(): Gets the database connection
  • findUser(): Checks whether the user exists
  • main(): System entrance

It’s obvious: The Login class has multiple responsibilities, including methods related to the interface, methods related to the database, and even the main() entry function.

🤣 Login bears a weight his age shouldn’t

😣 Imagine that you need to manipulate the Login class whenever you want to modify the interface or the method of the database connection; Or another system may need to use the database connection, and it cannot reuse the database connection part of the code because it is already tightly coupled to other responsibility code and cannot achieve high-level reuse.

🚀 then reconstructs it using the single responsibility principle:

Obviously, refactoring improves reusability and system maintainability.

Therefore, once the responsibilities of classes and methods are clearly divided, it can not only improve the readability of the code, but also effectively reduce the risk of errors in the program, because the clear code will not hide bugs, but also facilitate the tracking of bugs, which is to reduce the maintenance cost of the program.

😎 Open and close principle

define

A software entity should be open for extension and closed for modification. This means that a module can be designed to be extended without modifying the source code.

The only constant in any software is its changing requirements, so we should try to ensure that the structure of the code is stable; As software gets bigger and maintenance costs continue to rise, the on/off principle becomes more and more important.

🎨 The entry point of the open close principle: encapsulate and abstract the variable factors of the system to the upper level.

Case analysis

🔎 A graphical interface system provides a variety of buttons of different shapes. The client code can be programmed for these buttons. Users may change their requirements and require different buttons.

The original class diagram

If the interface class LoginForm needs to change the circular button CircleButton to a rectangular button RectangleButton, then not only will the name of the button class in LoginForm be changed, but the display() method will also be changed to fit the changed button.

What if the demand keeps changing? Have you been modifying the LoginForm source code? Obviously not.

🚀 Now reconstructs the system to meet the requirements of the open and close principle:

After refactoring using the open close principle, if you want to add a Triangle button Triangle, you simply add a Triangle class that extends from the AbstractButton class. If you want to modify the buttons of the current interface class, you only need to modify

in the config. XML configuration file without modifying the LoginForm source code.

Conclusion: Abstraction is the key to the open close principle.

😋 Richter’s substitution principle

define

The Richter substitution principle requires that all references to a base class (parent class) must transparently use objects from its subclasses.

In software, if the base class object is replaced by a subclass object, the program will not error/exception; But the reverse is not true.

For example: IF I like animals (base), I must like dogs (subclass); But if I like dogs (subclass), I don’t necessarily like animals (base class), like snakes (also animals)

⭐ The core of the open and close principle is abstraction of the system, and from abstraction to concretization, the process from abstraction to concretization needs the guidance of Richter’s substitution principle.

☢ Thus, Richter substitution can check the rationality of inheritance and avoid the abuse of inheritance.

Case analysis

🔎 A CS game system needs to expand the types of firearms on the premise of realizing the open and close principle. The original design scheme of expanding ToyGun is as follows.

The original class diagram

AbstractGun subclass ToyGun can killEnemy. ! Clearly unreasonable, clearly a classic example of inheritance abuse.

🚀 Now the system is reconstructed to meet the Richter substitution principle:

The reconstructed system perfectly conforms to the open – close principle and Richter’s substitution principle.

😮 relies on the inversion principle

define

High-level modules should not depend on low-level modules; they should all depend on abstractions. Abstraction should not depend on details, details should depend on abstractions.

To understand the dependency inversion principle simply, program for interfaces, not implementations.

The high layer depends on the low layer ❌

Higher and lower grades depend on abstraction

The dependency inversion principle aims to subvert traditional procedural systems by programming interfaces: high levels depend on low levels.

You can also easily think of dependency inversion as being linked to IOC. One of the common implementations of this principle is to use abstract classes in code and put concrete classes (implementation classes) in configuration files. This brings us to dependency injection in several ways:

  • Tectonic injection
  • Set a value into
  • Interface injection

Case analysis

🔎 A system is designed to allow drivers to drive as many brands of cars as they want. The original design is as follows.

The original class diagram

Imagine a scenario where Audi brand cars are added, the Driver class needs to be modified again, which is obviously not conducive to the stability of the code structure.

🚀 Now reconstructs the system to meet the dependency reversal principle:

After realizing the refactoring guided by the principle of dependency reversal, the framework is built through abstraction to reduce the coupling between classes and avoid the situation of affecting the whole body, so as to improve the maintainability of code.

😏 Interface isolation rules

define

The interface isolation principle requires us to refine some of the larger interfaces and replace a single master interface with multiple specialized interfaces, that is, clients using an interface need only know the interfaces associated with it.

When splitting an interface using the interface isolation principle, a set of related operations must be defined in an interface to meet the single responsibility principle, and as few methods in the interface as possible under the premise of high cohesion.

In the system design, customized services can be used, that is, to provide different interfaces for different clients, providing only the behavior that users need, and hiding the behavior that users do not need.

Case analysis

🔎 below shows a system with multiple customer classes in which a huge interface, AbstractService, is defined to service all of them.

The original class diagram

AbstractService implementation Class ConcreteService must implement the three methods declared in AbstractService if the ClientA class only needs to program against the method operatorA(), but because it provides a fat interface, AbstractService implementation class ConcreteService must implement the three methods declared in AbstractService. ClientA can also see two unrelated methods, operatorB() and operatorC(), in addition to operatorA(), greatly reducing system encapsulation.

🚀 The interface isolation principle is used to reconstruct the interface:

Whether you use one implementation class (as shown) or three implementation classes, ClientA can only access its corresponding methods, not the other two, which are not relevant to its business, because they are programmed against an abstract interface.

☢ Pay attention to the interface granularity when using the interface isolation principle. If the interface granularity is too small, the interface overflow will be detrimental to maintenance. If the interface is too large, the interface isolation principle will be violated, resulting in poor flexibility.

🤪 Principles of composite multiplexing

define

Use object composition rather than inheritance for reuse purposes.

The principle of composite reuse (also known as the principle of composite/aggregate reuse) is also a very important principle. In order to reduce the degree of coupling between classes in the system, this principle advocates the use of more association relations, less use of inheritance relations in the reuse of functions.

Compare the two reuse mechanisms:

  • Inheritance reuse: the subclass only needs to overwrite the methods of the parent class, but the key problem is that the internal details of the parent class are completely exposed to the subclass, which destroys the encapsulation of the system. And when the superclass changes, the subclass has to change too, making it less flexible.
  • Combination/aggregation reuse: since combination/aggregation transforms other object B into A member object of object A, the internal implementation details of other object B are completely invisible to A, realizing the encapsulation of the system.

Case analysis

🔎 The original design of database access class of a teaching management system is as follows.

The original class diagram

In the class diagram, the DBUtil class is used to connect to a database and provides a getConnection() method to return a database connection object. Because you need to connect to a database in both StudentDAO and TeacherDAO, inherit the DBUtil class to reuse getConnection().

Imagine a scenario where you need to change the database connection mode to a database connection pool connection, then you need to modify the DBUtil class source code; If the StudentDAO and TeacherDAO are connected in different ways, a new DBUtil needs to be added, which violates the open/close principle and has poor scalability.

🚀 now uses the principle of synthetic reuse to reconstruct it:

If you need to add a new database connection method, you can simply add a subclass to DBUtil, which fully conforms to the open closed principle and the composite reuse principle.

🤨 Demeter’s rule

define

Demeter’s law instructs that software entities should interact with other entities as little as possible.

Demeter’s Law (also known as the Least Knowledge Principle) requires that you don’t talk to “strangers” and only communicate directly with your friends.

A friend is defined as follows:

  • The current object itself (this)
  • Dependent objects (objects passed to member methods as parameters)
  • The member object of the current object
  • If a member object is a collection, then the elements in the collection are also friends
  • The object created by the current object

Any object that meets the above conditions is a “friend” and vice versa is a “stranger”.

⭐ Even if the current object and “stranger” maintain a minimal dependence, they are still in a coupling state, which requires the third-party object “friend” to forward and call the communication between the two, so as to reduce the coupling degree of the system, so that the loose coupling relationship between classes is maintained; However, it also reduces the communication efficiency between different modules of the system.

Case analysis

🔎 below is the current coupling system between the object and both “friends” and “strangers”.

The original class diagram

public class Someone {
    private Stranger stranger = new Stranger();
    
    public void operation1(Friend friend) { Stranger stranger = friend.provideStranger(); stranger.operation3(); }}Copy the code

The Someone class needs to call the Stranger’s operation3() method, so it retrieves the Stranger object through the friend’s provideStranger() method, and then calls operation3() of that object.

It seems very common and reasonable, because we write code like this, but is common always reasonable? There is a weak dependency between Someone and Stranger, because the Stranger object provided by Friend is still called in the Someone method, and there is still coupling, violating the Demeter principle.

🚀 now uses Demeter’s rule to reconstruct the system:

public class Someone {
    public void operation1(Friend friend) { friend.forward(); }}Copy the code
public class Friend {
    private Stranger stranger = new Stranger();
    
    public void forward(a) { stranger.operation3(); }}Copy the code

The Friend forward call decouples Someone from Stranger, and once Stranger is replaced, it doesn’t affect the changes to Someone’s source code.

The later model of facade and agent is actually the application of Demeter’s rule

The last

🔮 Good code is like a good joke — it needs no explanation!

🛕 Recommended reading: Design-pattern-for-humans

🚫 the author level is limited, if there is a wrong statement, please do not hesitate to comment.