The Way to Clean Architecture Part 1 (Programming Paradigms & Design Principles)

Directory

1 Overview

2. Programming paradigm

2.1. Structured programming

2.2. Object-oriented programming

2.3. Functional programming

3. Design principles

3.1. Single Responsibility Principle

3.2. Opening and closing principle

3.3. Liskov substitution principle

3.4. Interface Segregation Principle

3.5. Dependency Inversion Principle

4. Summary


1. Overview

The ultimate goal of software architecture is to meet the needs of building and maintaining the system with minimal labor costs.

Engineers are ignoring a law of nature: Haphazard coding actually works more slowly than conformity, both in the short and long run. The best choice for the R&D team is to clearly recognize and avoid the overconfidence of engineers, start to take their own code structure seriously, and be responsible for its quality. If you want to run fast, you must first run steadily.

software software. “ware” means “product”, and “soft” means, self-evidently, the flexibility of software.

In the Eisenhower matrix, software’s system architecture-those things that matter-occupies the top two spots on the list, while system behavior-those things that matter urgently-occupies only the first and third places. Emphasis on software system architecture.

2. Programming paradigm

The programming paradigm refers to the writing mode of the program, which has relatively little relationship with the specific programming language. The three programming paradigms restrict the use of goto statement, function pointer and assignment statement respectively.

2.1. Structured Programming

Dijkstra discovered that some uses of the goto statement will cause a module to be recursively split into smaller, provable units; Bohm and Jocopini proved that people can use three structural structures: sequence structure, branch structure, and loop structure out of any program. Structured programming advocates replacing jump statements with if/then/else statements and do/while/until statements that we are now familiar with.

The essence is: structured programming restricts and regulates the direct transfer of program control. In the field of architectural design, functional degradation splitting is still one of the best practices

2.2. Object-Oriented Programming

It is generally believed that the three major characteristics of object-oriented are encapsulation, inheritance, and polymorphism.

Encapsulation: Enclose a group of related data and functions, so that the code outside the circle can only see part of the functions, and the data is completely invisible. Although the C language is a non-object-oriented programming language, it is a perfect package.

Inheritance: Allows us to override a certain set of variables and functions defined externally within a certain scope. Although the C language supports inheritance, it requires explicit conversion, which is not perfect.

Polymorphism: the dependency relationship (or inheritance relationship) between ML1 and interface I on the source code, the direction of the relationship is exactly opposite to the control flow, we call it dependency inversion. (The solid line is the source code dependency, and the dotted line is the control flow). In other words, no matter what source-level dependencies we face, they can be reversed. Therefore, we make both the user interface and the database a plug-in of the business logic. In other words, the source code of the business logic module does not need to introduce the two modules of the user interface and the database, and can be independently deployed and developed.

Therefore, the author believes that object-oriented programming is the ability to control the dependencies in the source code by means of polymorphism. This ability allows software architects to build a plug-in architecture that allows high-level strategic Implementing components are separated, and the underlying components can be compiled into plug-ins to achieve development and deployment independent of high-level components.

The essence is: object-oriented programming restricts and regulates the indirect transfer of program control.

2.3. Functional programming

Most functional programming languages allow changing the value of a variable only under very strict restrictions. The core idea of functional programming is to treat computation as a composition of functions. It emphasizes the purity, immutability and no side effects of functions. Take the Java program as an example: the essence is: functional programming restricts and regulates the assignment in the program.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
//Problem: Suppose we have a list of integers, we want to find the even numbers in this list, multiply them by 2, and print the result.
// Explanation: We used Java 8's stream API and Lambda expressions.
// First, we convert the list to a stream by calling the stream() method. Then, we filter out even numbers using the filter() method. In the filter() method, we pass a Lambda expression that takes an integer parameter n and checks if n is even. Next, we multiply all even numbers by 2 using the map() method. Finally, we collect the results into a new list using the collect() method and print each element using the forEach() method.
public class FunctionalProgrammingExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> evenNumbersDoubled = numbers. stream()
            .filter(n -> n % 2 == 0) // filter even numbers
            .map(n -> n * 2) // multiply even numbers by 2
            .collect(Collectors.toList()); // Collect results into a new list

        evenNumbersDoubled.forEach(System.out::println); // print the result
    }
}

3. Design principles

The main goals of the middle-level structure of software construction are: components that can tolerate changes, are easier to understand, and can be reused in multiple software systems.

The design principle is briefly described as: SOLID. They are: SRP (Single Responsibility Principle), OCP (Open-Closed Principle), LSP (Liskov Substitution Principle), ISP (Interface Segregation Principle) and DIP (Dependency Inversion Principle).

3.1. Single Responsibility Principle

The single responsibility principle means that a class is responsible for only one responsibility. In addition, we can separate the code for different responsibilities and put each responsibility in a separate method.

//In this example, we define a UserService class and a UserRepository class. The UserService class is responsible for handling user-related business logic, such as acquiring users, adding users, updating users, and deleting users. The UserRepository class is responsible for interacting with the database, including getting users, adding users, updating users, and deleting users. In this way, we separate business logic and data access logic and assign them to different classes, thereby implementing the Single Responsibility Principle.
//Using the implementation method of the following single responsibility principle:
//1. A class is only responsible for one responsibility: the UserService class is only responsible for processing user-related business logic, and the UserRepository class is only responsible for interacting with the database. In this way, each class can only be responsible for one responsibility, avoiding the ambiguity and unnecessary complexity of the responsibility of the class.
//2. Separate business logic and data access logic: UserService class and UserRepository class handle business logic and data access logic respectively. This can make the responsibilities of each class more clear and specific, and improve the readability, maintainability and scalability of the code.
//In implementation, we can implement the single responsibility principle by separating business logic and data access logic; at the same time, we also need to consider class responsibilities and class dependencies in order to better organize and manage our code. Furthermore, we can separate the code of different responsibilities and put each responsibility in an independent method, which can make the code simpler, easier to read and easier to maintain.
public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this. userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository. getUserById(id);
    }

    public void addUser(User user) {
        userRepository. addUser(user);
    }

    public void updateUser(User user) {
        userRepository. updateUser(user);
    }

    public void deleteUser(int id) {
        userRepository. deleteUser(id);
    }
}

public class UserRepository {
    private Connection connection;

    public UserRepository(Connection connection) {
        this.connection = connection;
    }

    public User getUserById(int id) {
        // Call the database API to get user information
        return null;
    }

    public void addUser(User user) {
        // Call the database API to add user information
    }

    public void updateUser(User user) {
        // Call the database API to update user information
    }

    public void deleteUser(int id) {
        // Call the database API to delete user information
    }
}

3.2. Opening and closing principle

The open-closed principle means that software entities (classes, modules, functions, etc.) should be open for extension and closed for modification.

How? You can first group codes that meet different requirements (ie SRP), and then adjust the dependencies between these groups (ie DIP) (single responsibility + dependency inversion + one-way dependency). In addition, if component A does not want to be affected by changes that occur on component B, then component B should depend on component A.

What are high-order/low-order components? Higher-order components are more reusable because they can be used in multiple components and can be used in different applications. Low-level components are components that implement specific functions, and they are usually only used in a specific application.

//We define a Shape interface, which has an area() method to calculate the area of the shape. Then, we define a Rectangle class and a Circle class, both of which implement the Shape interface, and calculate the area of the rectangle and circle respectively. Finally, we define an AreaCalculator class that takes an array of Shapes as a parameter and calculates the total area of all shapes in the array.
//Using the implementation method of the following opening and closing principles:
//1. Open to extensions: We open to extensions by defining the Shape interface and the Rectangle and Circle classes. If we need to add other shapes, we just need to implement the Shape interface and create a new class to calculate the area of the shape.
//2. Close the modification: We use the AreaCalculator class to close the modification. We don't need to modify the AreaCalculator class to add new shapes, because we have defined the Shape interface to represent all shapes, and each shape implements the area() method to calculate the area.
//In implementation, we can open to extensions by defining interfaces, abstract classes, and using base classes; at the same time, we can use strategy patterns, factory patterns, and dependency injection to close modifications.
public interface Shape {
    double area();
}

public class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }
}

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this. radius = radius;
    }

    public double area() {
        return Math.PI * radius * radius;
    }
}

public class AreaCalculator {
    public double calculateTotalArea(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea + = shape. area();
        }
        return totalArea;
    }
}

3.3. Liskov substitution principle

The Liskov Substitution Principle means that subclasses can replace their parent classes without affecting the correctness of the program.

In the implementation, we can implement the Li-style substitution principle by inheriting, implementing interfaces, and using abstract classes; at the same time, we also need to ensure that the subclass satisfies all the behaviors and attributes of the parent class, and does not add or modify the properties of the parent class. Behavior.

//We define a Rectangle class, which has a width and a height attribute, and an area() method to calculate the area of the rectangle. Then, we define a Square class, which inherits from the Rectangle class, and overrides the setWidth() and setHeight() methods to ensure that the width and height of the square are equal. In this way, we can use the Square class instead of the Rectangle class without affecting the correctness of the program.
//Using the implementation method of the following Li style substitution principle:
//1. The subclass can replace the parent class: the Square class inherits from the Rectangle class, it can replace the instance of the Rectangle class, because it inherits all the properties and methods of the Rectangle class, and has the same behavior as the Rectangle class.
//2. Does not affect the correctness of the program: the Square class overrides the setWidth() and setHeight() methods to ensure that the width and height of the square are equal, and maintains the same behavior as the Rectangle class, so use the Square class to replace Rectangle class, it will not affect the correctness of the program.
//In the implementation, we can implement the Li-style replacement principle by inheriting, implementing interfaces, and using abstract classes; at the same time, we also need to ensure that the subclass satisfies all the behaviors and properties of the parent class, and does not add or modify the parent class class behavior.
public class Rectangle {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public double area() {
        return width * height;
    }
}

public class Square extends Rectangle {
    public Square(double sideLength) {
        super(sideLength, sideLength);
    }

    public void setWidth(double width) {
        super. setWidth(width);
        super. setHeight(width);
    }

    public void setHeight(double height) {
        super. setWidth(height);
        super. setHeight(height);
    }
}

3.4. Interface isolation principle

The interface segregation principle requires that the interface design should be concise and simple, and should not contain redundant methods.

//We define two interfaces, Worker and Eater, representing workers and eaters respectively. Then, we define a Programmer class and a Waiter class, both of which implement the Worker interface and represent programmers and waiters respectively. The Waiter class also implements the Eater interface, indicating that the waiter is still a diner. Finally, we define a Robot class, which also implements the Worker interface, indicating that the robot is also a worker.
//Using the implementation method of the following interface isolation principle:
//1. The interface design is simple and simple: the Worker interface only contains the work() method, and the Eater interface only contains the eat() method. This can make the interface design more streamlined, will not contain redundant methods, and avoid interface redundancy and unnecessary complexity.
//2. The class only implements the necessary interfaces: the Programmer class only implements the Worker interface, the Waiter class implements the Worker and Eater interfaces, and the Robot class also implements the Worker interface. This allows each class to implement only the necessary interfaces, avoiding unnecessary dependencies and complexity.
//In implementation, we can implement the principle of interface isolation by defining a streamlined interface and letting the class implement only the necessary interfaces; at the same time, we also need to consider the dependencies of the interface and the ease of use of the interface in order to better organize and Manage our code.
public interface Worker {
    void work();
}

public interface Eater {
    void eat();
}

public class Programmer implements Worker {
    public void work() {
        System.out.println("Programmer works.");
    }
}

public class Waiter implements Worker, Eater {
    public void work() {
        System.out.println("Waiter works.");
    }

    public void eat() {
        System.out.println("Waiter eats.");
    }
}

public class Robot implements Worker {
    public void work() {
        System.out.println("Robot works.");
    }
}

3.5. Dependency Inversion Principle

The principle of dependency inversion means that the establishment of dependencies should rely more on abstraction than concrete implementation, and inject dependencies through constructors. The source code dependency direction is always the inversion of the control flow direction, which is why DIP is called the dependency inversion principle.

//In this example, we define an Animal interface and two implementation classes Dog and Cat, which represent dogs and cats respectively. Then, we define an AnimalFeeder class that depends on the Animal interface, rather than the concrete implementation class. The AnimalFeeder class contains a constructor that takes a parameter of type Animal and stores it inside the class. The AnimalFeeder class also has a feed() method that calls the eat() method of the Animal interface to feed the animal.
//The implementation method using the following dependency inversion principle:
//1. Dependencies are based on abstraction rather than concrete implementation: the AnimalFeeder class depends on the Animal interface, not the concrete implementation class. In this way, the AnimalFeeder class can be decoupled from the specific implementation class, thereby improving the flexibility, scalability and maintainability of the code.
//2. Dependency injection through the constructor: The AnimalFeeder class accepts a parameter of type Animal through the constructor and saves it inside the class. This can make the establishment of dependencies explicit, and it can also facilitate dependency injection.
//In implementation, we can implement the dependency inversion principle through dependency injection and interface-oriented programming; at the same time, we also need to consider interface design and class dependencies in order to better organize and manage our code.
public interface Animal {
    void eat();
}

public class Dog implements Animal {
    public void eat() {
        System.out.println("Dog eats bones.");
    }
}

public class Cat implements Animal {
    public void eat() {
        System.out.println("Cat eats fish.");
    }
}

public class AnimalFeeder {
    private Animal animal;

    public AnimalFeeder(Animal animal) {
        this.animal = animal;
    }

    public void feed() {
        animal. eat();
    }
}

Dependency injection is a technical means to realize the principle of dependency inversion, which can transfer the establishment of dependency relationship from the inside of the class to the outside. The methods are: 1. Constructor injection; 2. Setter method injection; 3. Interface injection.

//1. Constructor injection: It accepts dependent objects through the constructor of the class and saves them in the member variables of the class.
public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this. userRepository = userRepository;
    }

    //...
}

//2. Setter method injection: It accepts dependent objects through the Setter method of the class and saves them in the member variables of the class. It makes dependency injection more flexible, but it also easily leads to vague responsibilities of classes and abuse of Setter methods.
public class UserService {
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {
        this. userRepository = userRepository;
    }

    //...
}

//3. Interface injection: It defines the method of dependency injection through an interface, and the class implements the interface.
public interface UserRepositoryAware {
    void setUserRepository(UserRepository userRepository);
}

public class UserService implements UserRepositoryAware {
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {
        this. userRepository = userRepository;
    }

    //...
}

4. Summary

The goal of software architecture is to meet the needs of building and maintaining the system with the minimum human cost. If you want to run fast, you must first run steadily.

There are three programming paradigms: structured programming, object-oriented programming, and functional programming. Structured programming advocates using the if/then/else statements and do/while/until statements that we are familiar with now instead of jump statements such as goto. Object-oriented programming is the ability to control dependencies in source code by means of polymorphism, which separates high-level strategic components from low-level implementation components. Functional programming emphasizes the purity, immutability, and absence of side effects of functions.

The design principle is the SOLID principle: the principle of single responsibility means that a class is only responsible for one responsibility; the principle of opening and closing means that software entities (classes, modules, functions, etc.) A class can replace its parent class without affecting the correctness of the program; the interface isolation principle requires that the interface design should be simple and simple, and should not contain redundant methods; the dependency inversion principle means that the establishment of dependencies should rely more on abstraction rather than Specifically, inject dependencies through constructors.