头图

Original: Clean architecture for the rest of us

by Suragch

This is an introductory article.

If you are a senior software engineer, stop here, this article is not for you. This post is for ordinary programmers like me who write messy code and create spaghetti-like architectures but are very savvy about building clean, maintainable, adaptable things. fascinated.

foreword

I don't usually buy computer-related books because they get out of date so quickly. There is, anyway, the information in these books can be found on the Internet. But a year ago, I read Clean Code by Robert Martin, which really helped me improve the way I develop software. So, when I saw another book " Clean Architecture " published by the same author, I bought it without hesitation.

Like Clean Code , Clean Architecture addresses a number of time-tested principles that apply to any code written by anyone. If you search the title of a book online, you will find some people who disagree with the author. Obviously, I'm not here to criticize him. I just know that Robert Martin (aka Uncle Bob) has been programming for over 50 years and I haven't.

Although this book is a bit difficult to understand, I will try my best to summarize and explain the important concepts in the book so that ordinary people can understand. I am still learning and growing as a software architect, so please read what I write with a critical eye.

What is clean architecture?

Architecture refers to the overall design of a project, the organization of code into classes, files, components, modules, and the way these code units relate to each other. Architecture defines where an application performs its core functions and how those functions interact with things like databases, user interfaces, and so on.

Clean architecture is an easy-to-understand project organization that can be flexibly changed as the project develops. This is not something that can be done casually and requires conscious planning.

Features of Clean Architecture

The secret to building a large and maintainable project is to split a class or file into different components, each of which can be modified independently of the others. Let's illustrate this with a few pictures:

clean-architecture-ex-1.jpeg

In the picture above, what would you need to do if you wanted to use a knife instead of scissors? You have to untie the strings attached to the pen, ink bottle, tape and compass. Then you reattach the items to the knife. Maybe that's ok for a knife. But if pens and tape say, "Wait, we need scissors." So now the pens and tape don't work and have to make changes. This change in turn affects those related objects that are attached to them. a mess.

Compare the picture below:

clean-architecture-ex-2.jpeg

Now how should we replace the scissors? We just had to pull the string of scissors out from under the post-it note and insert the new string that was attached to the knife. Much easier. Post-it notes don't care because the string isn't even tied to it.

The architecture represented by the second diagram is obviously easier to change. The system is easy to maintain as long as the sticky notes don't need to be replaced frequently. Again, this architecture can make software easier to maintain and change.

clean-architecture-ex-3.png

The inner circle is the domain layer of the application, where business rules are placed. By "business" we don't necessarily mean the company, it just means the essence of your application, the core functionality of your code. For example: the core function of a translation app is translation, and the essence of an online store is to sell goods. These business rules are usually fairly stable, since you are unlikely to change the nature of your application very often.

The outer layer is the infrastructure layer, which includes things like UI, databases, network APIs, and software frameworks. These are more prone to change relative to domains. For example, you're more likely to change the appearance of UI buttons than to change how loans are calculated.

A clear boundary is placed between the domain and the infrastructure in order to make the domain infrastructure unaware. This means that UI and database depend on business rules, but business rules do not depend on UI and database. This makes it a plugin architecture, it doesn't matter if the UI is a web page, a desktop app, or a mobile app; it doesn't matter if the data is stored in SQL, NoSQL, or the cloud, the domain doesn't care. This makes changing infrastructure very easy.

define terms

The two circle layers in the image above can be further refined.

clean-architecture-ex-4.png

Here the domain layer is subdivided into entities and use cases , adapter layer forms the boundary between the domain and infrastructure layers. These terms can be a little confusing. Let's take a look at each.

Entities

An entity is a set of related business rules that are critical to the functionality of an application. In object-oriented programming languages, all the rules of an entity are combined into a class as member methods. These rules still exist even if they are not applied. For example, a bank might have a rule that charges 10% interest on a loan, which is 10% whether calculated on paper or using a computer. Here is an example of an entity class from the book (page 191):

clean-architecture-ex-5.png

Entities know nothing about other layers, they don't depend on anything. That is, they do not use any classes or components in the outer layer.

Use cases

Use cases are application-specific business rules that describe how to automate the operation of the system. This determines the behavior of the application. Here is an example from the book on use cases business rules (192 pages, slightly modified):

Gather Info for New Loan
    
    Input:  Name, Address, Birthdate, etc.
    Output: Same info + credit score
    
    Rules:
      1. Validate name
      2. Validate address, etc.
      3. Get credit score
      4. If credit score < 500 activate Denial
      5. Else create Customer (entity) and activate Loan Estimation

Use cases interact with entities, and depend on entities, but know nothing about the outer layers. They don't care whether it's a web page or an iPhone app, or whether the data is stored in the cloud or a local SQLite database.

This layer defines interfaces or abstract classes for use by outer layers.

Adapters

Adapters, also known as interface adapters, are translators of communication between the domain and the infrastructure. For example, they take input data from the GUI and repackage it into a form that is convenient for use cases and entities. They then take output from use cases and entities and repackage it into a form for GUI display or database storage.

Infrastructure

This layer is where all the I/O components live: UI, database, framework, devices, etc. This is the most unstable layer. Since things in this layer are likely to change, they are kept as far away from the more stable domain layer as possible. Because they are separate, it is relatively easy to modify or replace components.

Implement the principles of a clean architecture

Because some of the principles below have confusing names, I purposely did not use them in the above explanation. However, to achieve the architectural design I've described, these principles must be followed. If this part makes you dizzy, you can skip directly to the section at the end of the article.

The first five principles below are often abbreviated as SOLID to make them easier to remember. They are class-level principles, but have analogous counterparts that apply to components (collections of related classes). Component-level principles follow SOLID principles.

Single Responsibility Principle (Single Responsibility Principle - SRP)

This is the S in SOLID. What the SRP says is that a class should have only one responsibility. A class may have multiple methods, but these methods work together to accomplish one main thing. Modifications to a class should have only one reason. For example, if the finance office has a requirement to modify this class, and the human resources department also has a requirement to modify this class in a different way, then there are two reasons for modifying this class. Then, this class should be split into two separate classes to ensure that each class has only one reason to modify.

Open Closed Principle (OCP)

This is the O in SOLID. Open means open for extension. Close means closed for modification. Therefore, you should be able to add functionality to a class or component without modifying existing functionality. How to do it? First, you need to ensure that each class or component has only a single responsibility, and then hide the relatively stable classes behind the interface. This way, when a less stable class has to be modified, it won't affect a relatively stable class.

Liskov Substitution Principle (LSP)

This is the L in SOLID. I guess the L is needed to spell SOLID, but "replace" is what you need to remember. This principle means that lower-level classes or components can be replaced without affecting the behavior of higher-level classes or components. This principle can be implemented through abstract classes or interfaces. For example, in Java, both ArrayList and LinkedList implement the List interface, and they are interchangeable. If this principle is applied at the schema level, MySQL can be replaced by MongoDB without affecting the logic of the domain layer.

Interface Segregation Principle (ISP)

This is the I in SOLID. ISP refers to the separation of a class from the classes that use it using an interface that exposes only the subset of methods required by the dependent class. In this way, other methods that are not in the subset of methods exposed by the interface are modified without affecting the dependent classes.

Dependency Inversion Principle (DIP)

This is the D in SOLID. This means that relatively unstable classes and components should depend on relatively stable classes and components, not the other way around. If a stable class depends on an unstable class, then every time the unstable class changes, it will affect the stable class, so the direction of dependency needs to be reversed. How do we do this? By using abstract classes, or by hiding stable classes behind interfaces.

So, a case of a stable class using a mutable class like the following:

class StableClass {
    void myMethod(VolatileClass param) {
        param.doSomething();
    }
}

An interface should be created, and the mutable class should implement this interface:

class StableClass {
    interface StableClassInterface {
        void doSomething();
    }
    void myMethod(StableClassInterface param) {
        param.doSomething();
    }
}

class VolatileClass implements StableClass.StableClassInterface {
    @Override
    public void doSomething() {
    }
}

This flips the dependency direction. The mutable class knows the class name of the stable class, but the stable class knows nothing about the mutable class.

Using the Abstract Factory parten is another way to do this.

Reuse/Release Equivalence Principle - REP

REP is a component-level principle. Reuse refers to a set of reusable classes or modules. Release (Release) refers to the release of the version number. The principle says that anything you publish should be reusable as a cohesive unit, not a random collection of disparate classes.

Common Closure Principle (CCP)

CCP is a component-level principle. What it says is that a component should be a collection of classes that are modified at the same time for the same reason. If modifications to these classes are based on different reasons, or if the frequency of modification is inconsistent, then the component should be split. The above-mentioned principles and single responsibility principle (Single Responsibility Principle - the SRP) substantially the same.

Common Reuse Principle (CRP)

CRP is a component-level principle. What it says is that you shouldn't rely on components that contain classes you don't need. These components should be split to such an extent that the user does not have to depend on those classes that he does not use. This principle is basically the same as the Interface Segregation Principle (Interface Segregation Principle - ISP)

These three principles (REP, CCP, and CRP) contradict each other. Too many splits or too many groupings can cause problems. These principles need to be balanced against the actual situation.

Acyclic Dependency Principle (ADP)

ADP means that there should be no dependency cycles in a project. For example, if component A depends on component B, component B depends on component C, and component C depends on component A, then there is a dependency cycle.

clean-architecture-ex-6.png

Such loops can create significant problems when trying to make changes to the system. One solution to break the dependency cycle is to add an interface between components Dependency Inversion Principle (DIP) If different individuals or teams are responsible for different components, those components should be released separately with their own version numbers. This way, changes to one component do not immediately affect other teams.

Stable Dependency Principle - SDP

What this principle says is that dependencies should be built in a stable direction. That is, less stable components should depend on more stable components. This minimizes the impact of changes. It doesn't matter that some components are inherently changeable, what we need to do is not to let stable components depend on them.

Stable Abstraction Principle - SAP

What SAP says is: the more stable a component is, the more abstract it should be, that is, the more abstract classes it should contain. Abstract classes are easier to extend, so this prevents stable components from becoming too rigid.

final summary

The above summarizes the Clean Architecture , but I would like to add some other points.

test

The benefit of creating a plugin architecture is to make the code more testable. The code is hard to test when there are many dependencies in the project. But when you have a plugin framework, testing becomes a lot easier by just replacing a database dependency (or any other component) with a Mock object.

I always feel bad when testing UI. I made a test that traverses the GUI, but as soon as I make changes to the UI, the test breaks and I end up deleting the test. I realized that I should create a Presenter object adapter layer The Presenter takes the output of the business rules and formats everything it gets according to the needs of the UI view. UI view objects do nothing but display the preformatted data provided by the Presenter. After modifying the code in this way, the Presenter code can be tested independently of the UI.

Create a special test API to test business rules. It should be decoupled from the interface adapter so that tests don't break when the application structure changes.

Divide components according to use cases

I talked about the domain and infrastructure layers above. If these are viewed as horizontal hierarchies, they can be divided vertically into different groups of components based on the different user cases of the application. Like a layered cake, each slice is a use case, and each layer in the slice constitutes a component.

clean-architecture-ex-7.jpeg

For example, on a video site, a use case is for viewers to watch the video. So there is a ViewerUseCase component, a ViewerPresenter component, a ViewerView component, and so on. Another use case is for publishers who upload videos to a website. For them, there should be a PublisherUseCase component, a PublisherPresenter component, a PublisherView component, and so on. Another use case might be for site administrators. In this way, a single component is created by slicing the horizontal layer vertically.

When deploying an application, you can group components in any way that makes the most sense.

forced stratification

You may have the best architecture in the world, but if a new developer adds a dependency that bypasses the boundary, it completely defeats the purpose of the architecture. The best way to prevent this from happening is to use a compiler to protect the architecture. For example, in Java, classes can be packaged as private to hide from modules that shouldn't know about them. Another option is to use third-party software, which can help you check if something is using something it shouldn't.

Add complexity only when needed

Don't over-engineer your system from the start, and only use more architecture when needed. But maintaining some boundaries in the architecture makes it easier for components to explode in the future. As an example: first, you might deploy an external monolithic application, but internally, the classes maintain proper boundaries. Later, you might break them up into separate modules. Later, you can deploy them as services. Just follow the path of maintaining layering and boundaries, and you are free to adjust how they are deployed. In this way, you don't create unnecessary complexity that may never be used.

detail

When starting a project, the business rules should be dealt with first, and the rest are minutiae. The database is a detail, the UI is a detail, the operating system is a detail, the Web API is a detail, and the framework is a detail. Decisions on these details should be delayed as much as possible. That way, when you need them, you'll be in a great position to help you make an informed choice. This has no impact on your initial development efforts, since the domain layer knows nothing about the infrastructure layer. When you are ready to select the database, fill in the database adapter code and insert it into the schema. When you have the UI ready, fill in the UI adapter code and insert it into the schema.

One last bit of advice

  • Don't use Entity objects as data structures passed in the outer layer, use separate data model objects instead.
  • The project's top-level organizational structure should clearly tell people what the project is about. This is called screaming architecture.
  • Get out there and start putting these lessons into practice. Only by using these principles can you really learn them.

Exercise: Make a Dependency Graph

Open one of your current projects and draw a dependency diagram on a piece of paper. Draw a box for each component or class in the project, then walk through each class to see the dependencies of those classes. Any named class is a dependency. Draw an arrow from the box of the class under inspection to the box of the named class or component.

When you have traversed all the classes, consider the following questions:

  • Where are the business rules (entities and use cases)?
  • Do business rules depend on something else?
  • If you had to use a different database, UI platform, or code framework, how many classes or components would be affected?
  • Is there a dependency cycle?
  • What refactorings do you need to do in order to create the plugin architecture?

in conclusion

Clean Architecture is that you need to create a plugin architecture. Classes that need to be modified at the same time for the same reason should be grouped together into components. Business rule components are relatively more stable, and they should be ignorant of the relatively volatile infrastructure components that handle UI, database, networking, code framing, and other minutiae functions. Boundaries between component hierarchies are maintained through interface adapters. These interface adapters transfer data between layers and maintain dependencies in the direction of more stable internal components.

I have learned a lot. I hope you are too. Please let me know if I have misrepresented this book in any way. You can find my contact information GitHub profile

further study

I did my best to give a comprehensive summary of Clean Architecture , but you will find more information in the book. It's worth taking the time to read this book. In fact, I recommend reading these three books by Robet Martin I've given links to these books on Amazon, but you may find them cheaper if you buy used copies. I have listed them in the order of recommended reading. None of these books will be out of date anytime soon.


Pandorox
1 声望0 粉丝