Author: Tom Brother wechat official account: Micro technology

A programmer who understands the principles of design will write code that is highly extensible, and the code that follows will be like spring breeze. On the other hand, if the code is written like a running list, it will be laid out in a single way, and whoever takes over the maintenance will be damned.

After years of software development, CRUD seems to have formed a kind of inertia, deep into the bone marrow, according to the conventional structure of separation: presentation layer, business logic layer, data persistence layer, a function only needs an hour of code to finish.

When combined with CTRL+C and CTRL+V, feature points can be quickly cloned.

Is there a kind of dominance of the feeling, regardless of his business scenes, big ye I shuttled to the end, the world is invincible!!

But is that really the case?

The answer is self-evident!!

A lot of people go through this phase when they’re just starting out in the software industry. As time goes by, many people will be confused. Their ability has not been improved with their working years. They are anxious and have insomnia.

Savvy people can quickly pick clues from a jumble and constantly improve their abilities.

What ability?

Of course, it is the ability of software architecture. An excellent software architect should have the ability of handling and designing, abstraction, expansion and stability of complex business systems.

How do you develop this ability?

** I have made a summary of common software architecture principles as follows: **

Of course, some of these principles are complementary and some are contradictory. In actual project development, flexible responses should be made according to specific business scenarios. Must not dogmatism, mechanically copy

Single responsibility

When coding, we always like to add a variety of functions to a class to make it easier. In future business iterations, this class will be constantly modified, resulting in high maintenance costs and high coupling. The whole body is affected by a single action.

To address this problem, we often design our architecture with a single responsibility in mind

Definition:

SRP: Single Responsibility Principle, one of the five SOLID principles of object orientation. Each function has only one responsibility, so there is only one reason for change. Minimize mistakes by narrowing the scope of responsibility.

The biggest difference between the single responsibility principle and a class that does one thing is that variation is taken into account.

Code requirements:

An interface, class, or method does one thing, simple and clear.

Advantages:

It reduces the class complexity and improves the readability and maintainability of the class. This improves system maintainability and reduces risks caused by changes.

Example:

There is a UserService interface, UserService, which provides methods for users to register, log in, and query their personal information, again around user-related services, which seems reasonable.

Public interface UserService{// Register (Object param); Object login(Object param); Object queryUserInfoById(Long uid); }Copy the code

After a few days, the business side made a request that users could participate in the project. A simple way to do this is to add a joinProject() method to the UserService class

A few days later, the business side asked for a count of how many projects a user has participated in and whether we should add a countProject() method to the UserService class.

As a result, the UserService class becomes more and more responsible, the class expands, and the internal implementation becomes more and more complex. This class is responsible for both user-specific and project-specific, and any subsequent business changes will result in changes to this class.

Two different types of requirements, changed to the same class. Instead, separate the changes caused by different requirements and build a separate ProjectService class dedicated to project-specific functionality

Public interface ProjectService{// Add a project void addProject (Object Param); Void countProject(Object param); }Copy the code

The advantage of this is that user specific requirements only need to change the UserService. If it is a project management requirement, only the ProjectService needs to be changed. There is much less reason for either to move.

The open closed principle

OCP (open-closed Principle) refers to the Principle that a class, method, module, etc., is Open for extension but Closed for modification. Simply put, a software entity should make changes through extensions, not through modifications to existing code.

In my opinion, the open and closed principle is the most important among all the principles, like the 23 design patterns we are familiar with, most of them follow the open and closed principle to solve the problem of code extensibility.

Implementation idea:

The framework body is built with abstractions and the details are extended with implementations. Use different subclasses for different businesses to avoid modifying existing code.

Advantages:

  • Good reusability. After the software is complete, it can still be extended to add new functions, very flexible. As a result, the software system can constantly add new components to meet changing requirements.

  • Good maintainability. Its underlying abstraction is relatively fixed, and there is no need to worry about the stability of the original components in the software system, which makes the changing software system have a certain stability and continuity.

Example:

For example, in such a business scenario, our e-commerce payment platform needs access to some payment channels. Due to time constraints, we only access wechat payment at the beginning of the project, so our code is as follows:

Class WeixinPay {public Object pay(Object requestParam) { return new Object(); }}Copy the code

With the expansion of business, other payment channels will be gradually connected in the later stage, such as Alipay, cloud flash payment, red envelope payment, zero wallet payment, integral payment, etc. How to iterate?

Class PayGateway {public Object pay(Object requestParam) {if(wechat pay){ }esle if(alipay){// request alipay to complete the payment // omit... }esle if(cloud flash payment){// request cloud flash payment to complete the payment // omit... } // Other, the extraction, conversion, adaptation of personalized parameters of different channels // Some channels may need multiple interface requests for a payment, to obtain some pre-preparation parameters // omit... return new Object(); }}Copy the code

All the business logic is concentrated in one method, and the business logic of each payment channel itself is quite complex. With the access of more payment channels, the code logic in the Pay method will become heavier and heavier, and the maintainability will only get worse and worse. Each change required regression testing of all payment channels, which cost money and people. So what are some good design principles to solve this problem? We can try to rearrange the code according to the open close principle

Firstly, define an abstract interface class of payment channels to abstract out the skeleton of all payment channels. Design a series of insertion points and correlate the flow of several insertion points.

For those of you who have used OpenResty, you will know that set_by_lua, rewrite_by_lua, body_filter_by_lua and so on are used to process the logic of the request at the corresponding stage, which effectively avoids all kinds of derivative problems.

Abstract class AbstractPayChannel {public Object pay(Object requestParam) {// Abstract method}}Copy the code

Implement subclasses of different payment channels one by one, such as AliayPayChannel and WeixinPayChannel. Each channel is independent. If the channel is upgraded and maintained in the later stage, you only need to modify the corresponding subclass to reduce the impact of code modification.

Class AliayPayChannel extends AbstractPayChannel{public Object pay(Object requestParam) { Class WeixinPayChannel extends AbstractPayChannel{public Object pay(Object requestParam) {// According to the request parameters, if wechat payment is selected, the subsequent process will be processed // wechat processing}}Copy the code

The master dispatch entry iterates through all payment channels and determines whether the current channel is processing the request based on the parameters in requestParam.

Of course, it is also possible to use the combination of payment, for example, red envelope payment + wechat payment, you can pass some intermediate data through context parameters.

Class PayGateway {List<AbstractPayChannel> payChannelList; public Object pay(Object requestParam) { for(AbstractPayChannel channel:payChannelList){ channel.pay(requestParam); }}}Copy the code

Replacement on the Richter scale

LSP: Liskov Substitution Principle: All places that reference a base class must be able to transparently use objects from its subclasses

In simple terms, a subclass can extend the functionality of a parent class, but cannot change the functionality of the parent class (e.g., it cannot change the entry or return of the parent class), similar to polymorphism in object-oriented programming.

Polymorphism is a syntax of object – oriented programming language and an idea of code implementation. Richter’s substitution is a design principle used to guide the design of subclasses in inheritance relationship. The design of subclasses should ensure that the replacement of the parent class does not change the logic of the original program and does not damage the correctness of the original program.

Implementation idea:

  • A subclass can implement abstract methods of its parent class

  • Subclasses can add their own special methods.

  • When a subclass’s method overrides a parent class’s method, the method’s preconditions (that is, the method’s parameters) are looser than the parent method’s input parameters.

  • When a subclass’s method implements an abstract method of the parent class, the method’s postcondition (that is, the method’s return value) is stricter than the parent class’s.

Interface segregation

The ISP Interface Segregation Principle requires programmers to break bloated interfaces into smaller and more specific interfaces, so that the interfaces contain only the methods that the caller is interested in, rather than forcing the caller to rely on interfaces that it doesn’t need.

Implementation idea:

  • Keep interfaces small, but limited. An interface serves only one submodule or business logic.

  • Customize services for classes that depend on interfaces. Provide only the methods needed by the caller and mask those that are not.

  • Combine business with local conditions. Each project or product has its own specific environmental factors. In different environments, the standard of interface separation is different, which requires us to have a strong business sense

  • Improve cohesion and reduce external interactions. Make the interface do the most with the fewest methods.

Example:

The user center encapsulates a set of UserService interfaces to provide user base services for upper-layer calls (business side and management background).

Public interface UserService{// Register (Object param); Object login(Object param); Object queryUserInfoById(Long uid); }Copy the code

However, as the business evolved, we needed to provide a deletion function. The general approach was to add a deleteById method directly to the UserService interface, which was relatively simple.

However, this method may bring a security risk. If this method is mistakenly invoked by a service with common rights, users may be deleted by mistake, causing disasters.

To avoid this problem, we can use the principle of interface isolation **

Define a new interface service and provide the deleteById method. The BopsUserService interface is only available to the Bops management background system.

Public interface BopsUserService{// Delete user Object deleteById(Long uid); }Copy the code

To summarize, when designing microservice interfaces, if some of these methods are limited to a few callers, we can break them out and encapsulate them independently, rather than forcing all callers to see them.

Dependency inversion

In software design, details are changeable, but abstractions are relatively stable. In order to make good use of this feature, dependency inversion principle is introduced.

The Dependence Inversion Principle: high level modules should not directly depend on low level modules, they should depend on abstractions. Abstractions should not depend on implementation details; Implementation details should depend on abstractions.

The main idea of dependency inversion is to program for interfaces, not implementations.

Example:

A MessageSender interface is defined, and the specific instance Bean is injected into the Handler to trigger the completion of the message sending.

interface MessageSender { void send(Message message); } class Handler { @Resource private MessageSender sender; void execute() { sender.send(message); }}Copy the code

If the message is sent using Kafka messaging middleware, we need to define a Kafka Messagesender implementation class to implement the specific sending logic.

class KafkaMessageSender implements MessageSender { private KafkaProducer producer; public void send(final Message message) { producer.send(new KafkaRecord<>("topic", message)); }}Copy the code

The benefit of this implementation is to decouple the high-level modules from the low-level implementation. If the company later upgrades the messaging middleware framework and adopts Pulsar, we only need to define a PulsarMessageSender class, which will automatically inject its Bean instance dependency with the @Resource of the Spring container.

Advantages:

  • Reduces coupling between classes

  • Improve system stability

  • Reduce the risks associated with parallel development

  • Improve code readability and maintainability

Finally, to play around with the dependency inversion principle, you must be familiar with inversion of control and dependency injection. If you are a Java backend, you will be familiar with the terms, and the core design of the Spring framework relies on these two principles.

Simple principle

The ultimate architectural idea of complex systems is to reduce complexity to simplicity. Simplicity means an infinite extension of flexibility. Let’s look at this simplicity principle.

KISS: Keep It Simple and Stupid. Translation, keep it simple, keep it stupid.

Let’s take a closer look at this simplicity:

1. Simplicity is not the same as simple design or simple programming. In software development, in order to catch up with the schedule, many technical solutions are simplified or even have no technical solutions. They think that they will find time to reconstruct later. In the process of coding, the style is arbitrary and they pursue the rapid implementation of the project, resulting in a lot of technical debt. In the long run, project maintenance costs are getting higher and higher.

Keeping it simple isn’t just about doing simple design or simple programming. It’s about designing or programming with the goal of making it simple in the end, and it doesn’t matter if the process is complicated.

Simple is not equal to less. The two are not necessarily related; fewer lines of code or the introduction of an unfamiliar open source framework may seem simple but may introduce more complex problems.

How to write “simple” code?

  • Don’t patch code for long

  • Don’t flaunt programming skills

  • Don’t simply program

  • Don’t Optimize too early

  • Code Review should be done regularly

  • Choose the appropriate coding specification

  • Refactoring in time

  • Optimize gradually and purposefully

Principle of least

The least principle is also known as LoD: Law of Demeter. Demeter’s Rule defines talking only to your immediate friends, not to “strangers.”

If two software entities do not have to communicate directly, then direct calls to each other should not occur and can be forwarded by a third party. Its purpose is to reduce the degree of coupling between classes and improve the relative independence of modules.

Core ideas:

  • A class should only communicate with the classes it is directly related to

  • Each class should know the minimum it needs to know

Example:

Today’s software uses a layered architecture, such as the common Three-tier Web > Service > Dao structure. If the Service layer in the middle has little business logic, but Demeter’s law keeps the layers close, define a class that is purely used to forward calls between the Web layer and the Dao layer.

Such transfer efficiency is bound to be low, and there is a lot of code redundancy. We need to be flexible with this problem and allow the Web layer to call daOs directly in the early days. Later, as the business complexity increases, we can slowly collect the heavy business logic in the Controller and precipitate it into the Service layer. As the architecture evolved, clear layers began to settle down.

At the end, Demeter’s rule is concerned with local simplification, so it’s easy to overlook global simplification.

Express principle

The maintainability of code is also an important criterion to test the ability of engineers. Ask a person who writes a code that has a lot of problems every time it is reviewed. Do you think he is reliable?

This is where we need to introduce a principle of expression.

The principle of Program Intently and Expressible Agility (PIE), which originated in agile programming, means that programming should be done with clear programming intent and expressed explicitly through code.

The core idea of the Expression principle is that code is documentation, and through code we clearly express our true intentions.

So how do you improve the readability of your code?

1. Optimize your code presentation

Be it a variable name, class name, or method name, name it properly and express the meaning clearly and accurately. With some Chinese annotations, you can quickly get familiar with the project code and understand the intention of the original author without looking at the design document.

2. Improve control flow and logic

Control the depth of nested code, such as if else, preferably not more than three layers deep. It is best for the outer layer to make a negative judgment in advance and terminate the operation or return early. This code is logically clear. Here is an example of the correct handling:

public List<User> getStudents(int uid) { List<User> result = new ArrayList<>(); User user = getUserByUid(uid); If (null == user) {system.out.println (" failed to obtain employee information "); return result; } Manager manager = user.getManager(); If (null == manager) {system.out.println (" failed to get leadership information "); return result; } List<User> users = manager.getUsers(); If (null = = users | | users. The size () = = 0) {System. Out. Println (" failed to get employees list "); return result; } for (User user1 : users) { if (user1.getAge() > 35 && "MALE".equals(user1.getSex())) { result.add(user1); } } return result; }Copy the code

Rule of separation

World affairs, long separated must be divided. In the face of complex problems, considering the limited processing capacity of the human brain, the effective solution is to reduce the big problems to small ones, and divide the complex problems into a number of small problems, and then solve the big problems by solving the small problems.

The core idea of separation:

1. Architectural perspective

In combination with business scenarios, the boundary of several components in the whole system is divided, such as layer to layer (MVC), module to module, service to service, etc. Microservices like the popular DDD domain-driven design-guided microservices are a good way to break down services through a horizontal separation strategy.

The separation of concerns from the perspective of architectural design pays more attention to the separation of components and ensures the mutual reference of each component in the architecture through certain communication strategies.

2. Coding perspective

The coding perspective focuses on boundary demarcation between specific classes or methods. For example, filter, map and limit of Stream are processed in different stages according to different logic, and the output content is taken as the input of the next method. When all processes are finished, the results are finally summarized.

Some good examples of layering:

1. MVC model

2. Network OSI seven-layer model

A good architecture must have good layers that communicate with defined specifications, so that changes in one part of the system do not affect other parts (provided that the system is fault-tolerant enough).

Principle of contract

The same goes for software architecture. The principle of contract is how to divide the labor and cooperate with each other to ensure that everyone’s work can move forward in an orderly way.

Principles of Contract (DbC: Design by Contract). Software design should define a precise and verifiable interface specification for software components that includes preconditions, postconditions, and invariants used to extend the definition of common abstract data types.

Contract Principles Focus on:

  • The API must ensure that the input is what the receiver expects

  • The API must ensure that the output is correct

  • The API must maintain consistency in the process. If an API is modified twice, all servers in the cluster must be redeployed to ensure consistent service status.

How to design API interface well?

1. Separation of interface responsibilities. When designing an API, you should try to make each API do only one job to keep the API simple and stable. Avoid mutual interference.

2. API naming. You can guess the function of the interface by naming it, and use lowercase English as much as possible

3. Interfaces are idempotent. When an operation is performed multiple times it has the same impact as when it is performed once

4. Security policy. If the API is used externally, consider hacking and interface abuse, such as using traffic limiting policies.

5. Version management. After an API is released, it is unlikely to remain unchanged. It may cause compatibility problems between the new version and the old version due to upgrades. The solution is to version control and manage the API.

Write in the last

The core principle of software architecture is to try to separate what changes from what doesn’t, and to stabilize what doesn’t. As we know, models are relatively stable, and implementation details are the easy part. Therefore, to build a stable model layer, for any system, is crucial.


About me: I am a former ali architect who has won patents and prizes in competitions. I am a CSDN blog expert who is responsible for e-commerce transactions, community fresh food, marketing, finance and other businesses. I have years of team management experience

Recommended reading

Q: How many rows can a B+ tree store in mysql?

Learn these 10 design principles and be one step closer to being an architect!!

How to design the Redis cache for a hundred million level system??

High concurrency, high performance, high availability system design experience

Everyone is an architect?? No easy talk!!

How to design inventory deduction for e-commerce? Not oversold!