Project-driven learning, practice to test knowledge

preface

Design patterns are an integral part of our programming path. If we use design patterns well, we can make code have good maintainability, readability and extensibility. It seems to be the synonym of “elegant”, and it can be found in all frameworks and libraries.

Because of its benefits, many people try to apply a design pattern to a project during development, but often use it awkwardly. This is partly because the business requirements do not quite fit the design patterns used, and partly because in Web projects our objects are managed by the Ioc container of the Spring framework, and many design patterns cannot be applied directly. Then in the real project development, we need to do a flexible design pattern, so that it can be combined with the framework, in the actual development of the real advantages.

When projects are introduced into the IoC container, we typically use individual objects through dependency injection, and this is the key to combining design patterns and frameworks! This article will show you how to use dependency injection to complete our design patterns, starting from the beginning, to give you a good understanding of some of the design patterns as well as some of the advantages of dependency injection.

All the code in this article is on Github and can be cloned and run to see the effect.

In actual combat

The singleton pattern

Singletons are probably the first design pattern many people encounter. Compared to other design patterns, the concept of singletons is very simple: a class has only one instance object from beginning to end in a process. But even if the concept is simple, it still needs a little coding to achieve, R before the article back word has four ways to write, then do you know there are five ways to write singletons have a detailed explanation, here is not more about the design pattern, let’s directly take a look at how to use the pattern in the actual development.

The objects managed by the Spring IoC container are called beans, and each Bean has its scope, which can be understood as how Spring controls the Bean’s life cycle. Creation and destruction are essential nodes in the life cycle, and the singleton pattern naturally focuses on object creation. Spring’s object creation process is insensitive to us, meaning we simply configure the Bean and use the object via dependency injection:

@Service // Declare the class as a Bean to be managed by the container, as with the @component function
public class UserServiceImpl implements UserService{}@Controller
public class UserController {
    @Autowired // dependency injection
    private UserService userService;
}
Copy the code

So how do we control the creation of this object?

In fact, the Bean’s default scope is singletons; we don’t need to write singletons by hand. To verify that the Bean is a singleton, we can simply fetch the Bean from various parts of the program and print its hashCode to see if it is the same object, such as injecting UserService into two different classes:

@Controller
public class UserController {
    @Autowired
    private UserService userService;
    
    public void test(a) { System.out.println(userService.hashCode()); }}@Controller
public class OtherController {
    @Autowired
    private UserService userService;
    
    public void test(a) { System.out.println(userService.hashCode()); }}Copy the code

The printed result will be two identical Hashcodes.

Why does Spring instantiate beans as singletons by default? This is naturally because singletons save resources, and there are many classes that do not need to instantiate multiple objects.

What if we just want to create an object every time we get a Bean? We can configure the Scope of a Bean by declaring it with the @scope annotation:

@Service
@Scope("prototype")
public class UserServiceImpl implements UserService{}Copy the code

This will create an instance each time you fetch the Bean.

Beans have the following scopes, which can be configured as required, and in most cases we’ll just use the default singleton:

The name of the instructions
singleton The default scope. Only one object instance is created per IoC container.
prototype Is defined as multiple object instances.
request Restricted to the lifetime of the HTTP request. Each HTTP client request has its own object instance.
session Restricted to the lifetime of the HttpSession.
application Restricted to the lifetime of the ServletContext.
websocket Restricted to the life of the WebSocket.

An additional point to note here is that Bean singletons are not fully singletons in the traditional sense, as their scope guarantees only one object instance within the IoC container, but not one object instance within a process. That is, if you create an object instead of getting the Bean via Spring, your application will have multiple objects:

public void test(a) {
    // Create a new object
    System.out.println(new UserServiceImpl().hashCode());
}
Copy the code

This is where flexibility comes in. Spring covers every corner of our daily development, and as long as we don’t deliberately bypass Spring, ensuring a singleton in the IoC container is basically guaranteeing a singleton in the entire program.

Chain of Responsibility model

After the simple conceptual singleton, let’s look at the chain of responsibility model.

Pattern on

The pattern is not complicated: a request can be processed by multiple objects, which are connected in a chain and pass the request along the chain until an object processes it. The benefit of this pattern is that requestor and receiver are decoupled, processing logic can be added and deleted dynamically, and the responsibility for processing objects is very flexible. The common Filter and Interceptor in our development use the chain of responsibility mode.

Just looking at the introduction is confusing, but let’s take a look at how the model works.

Take the approval of leave in the work for example, when we launch a leave application, there will generally be more than one approver, each approver represents a responsibility node, has its own approval logic. We assume the following approvers:

Leader: Can only approve the leave of no more than three days;

Manager Manger: Can only approve leave of no more than seven days;

Boss Boss: Can approve any number of days.

Let’s first define an object for leave approval:

public class Request {
    /** ** request name */
    private String name;
    /** * Days of leave. For the sake of demonstration, let's simply count the whole day instead of the hours
    private Integer day;

    public Request(String name, Integer day) {
        this.name = name;
        this.day = day;
    }
    
    // omit the get and set methods
}
Copy the code

According to the traditional writing, the recipient receives the object and performs the corresponding processing through the conditional judgment:

public class Handler {
    public void process(Request request) {
        System.out.println("-");

        / / Leader for approval
        if (request.getDay() <= 3) {
            System.out.println(String.format("The Leader has approved the [% D] day leave application for [%s]", request.getName(), request.getDay()));
            return;
        }
        System.out.println(String.format("The Leader could not approve the [%d] day leave request for [%s]", request.getName(), request.getDay()));

        / / manager for approval
        if (request.getDay() <= 7) {
            System.out.println(String.format("Manger has approved the [% D] day leave request for [% S]", request.getName(), request.getDay()));
            return;
        }
        System.out.println(String.format("Manger was unable to approve [% D] day leave requests for [%s]", request.getName(), request.getDay()));

        / / Boss for approval
        System.out.println(String.format("Boss has approved the [% D] day leave application for [% S]", request.getName(), request.getDay()));

        System.out.println("-"); }}Copy the code

Simulate the approval process on the client side:

public class App {
    public static void main( String[] args ) {
        Handler handler = new Handler();
        handler.process(new Request("Zhang".2));
        handler.process(new Request("Bill".5));
        handler.process(new Request("Fifty".14)); }}Copy the code

The print result is as follows:

-- Leader has approved the [2] day leave application of [Zhang SAN] -- Leader cannot approve the [5] day leave application of [Li Si]. Manger has approved the [5] day leave application of [Li Si] Manger could not approve wang Wu's [14] day leave application Boss has approved Wang Wu's [14] day leave application --Copy the code

It’s not hard to see how the code in the Handler class smells bad! The coupling degree between each responsible node is very high. If you want to add or delete a node, you have to change this large section of code, which is very inflexible. And the approval logic demonstrated here is just a one-sentence print-out, which is far more complicated in real business and would be a disaster to change.

This is where our chain of responsibility comes in handy! We encapsulate each responsibility node into a separate object, and then combine these objects into a chain, one by one, through a unified portal.

First, we abstract out the interface of the responsible node, which is implemented by all nodes:

public interface Handler {
    /** * if the value is true, it indicates that the next node is allowed to process. */ If the value is false, it indicates that the next node is not allowed
    boolean process(Request request);
}
Copy the code

Take the Leader node as an example to realize the interface:

public class LeaderHandler implements Handler{
    @Override
    public boolean process(Request request) {
        if (request.getDay() <= 3) {
            System.out.println(String.format("The Leader has approved the [% D] day leave application for [%s]", request.getName(), request.getDay()));
            // No release after processing
            return false;
        }
        System.out.println(String.format("The Leader could not approve the [%d] day leave request for [%s]", request.getName(), request.getDay()));
        / / release
        return true; }}Copy the code

We then define a chain class dedicated to handling these handlers:

public class HandlerChain {
    // Store all handlers
    private List<Handler> handlers = new LinkedList<>();

    // Provide an external entry to add handlers
    public void addHandler(Handler handler) {
        this.handlers.add(handler);
    }

    public void process(Request request) {
        // Call Handler in turn
        for (Handler handler : handlers) {
            // Abort the call if false is returned
            if(! handler.process(request)) {break; }}}}Copy the code

Now let’s look at how the approval process is performed using the chain of responsibility:

public class App {
    public static void main( String[] args ) {
        // Build a chain of responsibility
        HandlerChain chain = new HandlerChain();
        chain.addHandler(new LeaderHandler());
        chain.addHandler(new ManagerHandler());
        chain.addHandler(new BossHandler());
        // Execute multiple processes
        chain.process(new Request("Zhang".2));
        chain.process(new Request("Bill".5));
        chain.process(new Request("Fifty".14)); }}Copy the code

Print the same result as before.

The benefits brought by this are obvious. We can easily add and delete responsible nodes, and modifying the logic of one responsible node will not affect other nodes. Each node only needs to pay attention to its own logic. In addition, the responsibility chain executes nodes in a fixed order. You can easily arrange the order by adding each object in the order you want.

In addition, there are many variations of the chain of responsibility, such as a Filter like a Servlet that needs to hold a reference to the chain when executing the next node:

public class MyFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        if(...). {// Pass through the chain reference
            chain.doFilter(req, resp);
        } else {
            // If the chain method is not called, the chain is aborted. }}}Copy the code

In addition to the different transmission modes of responsibility chains, the overall link logic can also be different.

What we have just demonstrated is that the request is handed over to a node for processing. Once one node is processed, there is no further processing. Some chains of responsibility are designed not to find a single node to deal with, but rather each node to do something, like an assembly line.

For example, in the approval process just now, we can change the logic to a leave application that requires the approval of every approver. The application will be approved when the Leader agrees, and then it will be transferred to Manger for approval, and then Manger agrees, and then it will be approved by the Boss. Only when the Boss finally agrees, will the application take effect.

There are many forms, the core concept of which is the chain of request objects, not to deviate from this can be regarded as the chain of responsibility pattern, do not adhere to the definition too much.

Cooperate with the framework

In the chain of responsibility mode, we create the responsibility node object ourselves and then add it to the chain of responsibility. There is a problem with this in real development. If we have other beans that are dependency injected into the responsible node, then creating the object manually means that the object is not managed by Spring, and those properties will not be dependency injected:

public class LeaderHandler implements Handler{
    @Autowired // Manually creating LeaderHandler will not inject the attribute
    private UserService userService;
}
Copy the code

At this point, we have to commit each node object to Spring for management as well, and then use Spring to get these object instances and place them in the responsibility chain. The Spring MVC Interceptor is used in this way:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Get the Bean and add it to the chain of responsibilities (note that this is the method called to get the object, not to new out the object)
        registry.addInterceptor(loginInterceptor());
        registry.addInterceptor(authInterceptor());
    }
    
    // Commit custom interceptors to Spring management via the @bean annotation
    @Bean
    public LoginInterceptor loginInterceptor(a) {return newLoginInterceptor(); }@Bean
    public AuthInterceptor authInterceptor(a) {return new AuthInterceptor();}
}
Copy the code

The InterceptorRegistry is like a chain class, which is passed to us by Spring MVC so that we can add interceptors, and Spring MVC will call the chain of responsibility on its own, so we don’t have to worry about it.

The responsibility chain defined by other frameworks will be called by the framework, so how should we call our own responsibility chain? An easier way to do this is to inject Bean dependencies into collections!

We use dependency injection to get individual beans in our daily development, because our declared interface or parent class usually needs only one implementation class to handle business requirements. The Handler interface will have multiple implementation classes, so we can inject multiple beans at once! Let’s change the code now.

First, declare each Handler implementation class as a Bean with the @service annotation:

@Service
public class LeaderHandler implements Handler{... }@Service
public class ManagerHandler implements Handler{... }@Service
public class BossHandler implements Handler{... }Copy the code

Then we’ll modify our chain class to declare it as a Bean, and annotate the member variables directly with @AutoWired annotations. Since dependency injection is implemented, there is no need to manually add responsible nodes, so we remove the previous method of adding nodes:

@Service
public class HandlerChain {
    @Autowired
    private List<Handler> handlers;

    public void process(Request request) {
        // Call Handler in turn
        for (Handler handler : handlers) {
            // Abort the call if false is returned
            if(! handler.process(request)) {break; }}}}Copy the code

Yes, dependency injection is powerful enough to inject not just a single object, but many! This is very convenient, we just implement the Handler interface, declare the implementation class as a Bean, and it will be injected into the chain of responsibility automatically, we don’t even have to add it manually. It is also extremely easy to implement the chain of responsibilities by simply fetching the HandlerChain and calling it:

@Controller
public class UserController {
    @Autowired
    private HandlerChain chain;

    public void process(a) {
        chain.process(new Request("Zhang".2));
        chain.process(new Request("Bill".5));
        chain.process(new Request("Fifty".14)); }}Copy the code

The execution effect is as follows:

-- Boss has approved [Zhang SAN] 's [2] day leave application -- Boss has approved [Li Si]' s [5] day leave application -- Boss has approved [Wang Wu] 's [14] day leave applicationCopy the code

Why did the first two nodes not take effect? Since we have not configured the Order in which the beans are injected into the collection, we need to control the assembly Order of the beans with the @order annotation, the smaller the number, the higher the Order:

@Order(1)
@Service
public class LeaderHandler implements Handler{... }@Order(2)
@Service
public class ManagerHandler implements Handler{... }@Order(3)
@Service
public class BossHandler implements Handler{... }Copy the code

So our custom chain of responsibility pattern fits perfectly into Spring!

The strategy pattern

Now we are going to explain a new model!

Pattern on

We often encounter requirements in development that require different actions to be performed for different situations. For example, the most common postage for shopping is different in different regions and different commodities. Suppose the demand now looks like this:

Free shipping area: no more than 10KG of goods free, 10KG above 8 yuan;

Adjacent area: no more than 10KG goods 8 yuan, more than 10KG 16 yuan;

Remote areas: no more than 10KG goods 16 yuan, 10KG above 15KG below 24 yuan, 15KG above 32 yuan.

So the way we calculate postage is like this:

// For demonstration purposes, the weight and amount are simply set to integers
public long calPostage(String zone, int weight) {
    // Shipping is inclusive
    if ("freeZone".equals(zone)) {
        if (weight <= 10) {
            return 0;
        } else {
            return 8; }}// Proximity area
    if ("nearZone".equals(zone)) {
        if (weight <= 10) {
            return 8;
        } else {
            return 16; }}// Remote areas
    if ("farZone".equals(zone)) {
        if (weight <= 10) {
            return 16;
        } else if (weight <= 15) {
            return 24;
        } else {
            return 32; }}return 0;
}
Copy the code

That’s a long code for so few postage rules, and it would have been even longer if the rules had been a little more complicated. And if the rules change, that chunk of code has to be patched together, and over time the code becomes very difficult to maintain.

The first optimization approach we came up with was to encapsulate each piece of computation as a method:

public long calPostage(String zone, int weight) {
    // Shipping is inclusive
    if ("freeZone".equals(zone)) {
        return calFreeZonePostage(weight);
    }

    // Proximity area
    if ("nearZone".equals(zone)) {
        return calNearZonePostage(weight);
    }

    // Remote areas
    if ("farZone".equals(zone)) {
        return calFarZonePostage(weight);
    }
	
    return 0;
}
Copy the code

That’s fine, and for the most part it works, but it’s still not flexible enough.

Because these rules are written into our methods, what if the caller wants to use his or her own rules, or changes them frequently? We can’t just change the code we’ve written. Knowing that postage is only a small part of the order price calculation, we can certainly write several rules to provide service, but also allow others to customize the rules. At this point, we should more highly abstract the postage calculation operation into an interface, there are different calculation rules to implement different classes. Different rules represent different strategies, and this is our strategy pattern! Let’s see what it looks like:

First, encapsulate a postage calculation interface:

public interface PostageStrategy {
    long calPostage(int weight);
}
Copy the code

We then encapsulate those locale rules into different implementation classes, taking the example of the enclosing locale:

public class FreeZonePostageStrategy implements PostageStrategy{
    @Override
    public long calPostage(int weight) {
        if (weight <= 10) {
            return 0;
        } else {
            return 8; }}}Copy the code

Finally, to apply the policy we need a special class:

public class PostageContext {
    // Hold a policy
    private PostageStrategy postageStrategy = new FreeZonePostageStrategy();
    // Allows the caller to set a new policy
    public void setPostageStrategy(PostageStrategy postageStrategy) {
        this.postageStrategy = postageStrategy;
    }
    // for the caller to execute the policy
    public long calPostage(int weight) {
        returnpostageStrategy.calPostage(weight); }}Copy the code

This way, callers can either use the policies we already have, or modify or customize them very easily:

public long calPrice(User user, int weight) {
    PostageContext postageContext = new PostageContext();
    // Customize the policy
    if ("RudeCrab".equals(user.getName())) {
        // For VIP customers, free shipping for less than 20KG, only 5 yuan for more than 20KG
        postageContext.setPostageStrategy(w -> w <= 20 ? 0 : 5);
        return postageContext.calPostage(weight);
    }
    // Regional policies are included
    if ("freeZone".equals(user.getZone())) {
        postageContext.setPostageStrategy(new FreeZonePostageStrategy());
        return postageContext.calPostage(weight);
    }
    // Neighborhood strategy
    if ("nearZone".equals(user.getZone())) {
        postageContext.setPostageStrategy(new NearZonePostageStrategy());
        returnpostageContext.calPostage(weight); }...return 0;
}
Copy the code

As you can see, simple logic can use Lambda expressions directly to implement the custom policy, or more complex logic can create a new implementation class.

This is the beauty of the policy pattern, allowing callers to use different policies to get different results for maximum flexibility!

While the benefits are numerous, the disadvantages of the strategic pattern are also obvious:

  • This can lead to too many policy classes, with as many rules as there are classes
  • The policy pattern simply distributes logic to the different implementation classes of the callerThe if and the elseNot one of them has gone down.
  • The caller needs to know all the policy classes to use the existing logic.

Most of the shortcomings can be solved with factory mode or reflection, but this adds complexity to the system. Is there a solution that can make up for the disadvantages without being complicated? Of course there is, and that’s what I’m going to talk about. While the policy pattern works with the Spring framework, it also makes up for the shortcomings of the pattern itself!

Cooperate with the framework

Through the chain of responsibility pattern, we can find that the so-called coordination framework is to hand over our objects to Spring to manage, and then call beans through Spring. In policy mode, we manually instantiate each policy class, so the first step is to declare these policy classes as beans:

@Service("freeZone") // The value in the annotation represents the name of the Bean
public class FreeZonePostageStrategy implements PostageStrategy{... }@Service("nearZone")
public class NearZonePostageStrategy implements PostageStrategy{... }@Service("farZone")
public class FarZonePostageStrategy implements PostageStrategy{... }Copy the code

We will then fetch these beans via Spring. One might naturally think that we should inject all of these implementation classes into a collection and iterate over them. That’s all right, but it’s too much trouble. Dependency injection is powerful enough to inject beans not only into collections, but also into maps!

Let’s see how it works:

@Controller
public class OrderController {
    @Autowired
    private Map<String, PostageStrategy> map;

    public void calPrice(User user, int weight) { map.get(user.getZone()).calPostage(weight); }}Copy the code

Tell me loudly, is it fresh? Simple is not simple! Excellent not elegant!

Dependency injection can inject a Bean into a Map, where the Key is the Bean name and the Value is the Bean object, which is why I set the Value on the @Service annotation earlier so that the caller can get the Bean directly through the Map’s GET method and then use the Bean object.

Instead of using the PostageContext class, whenever you want to invoke a policy, you can inject a Map directly into the call.

In this way, we not only fully integrate the policy pattern into the Spring framework, but also solve the if, else, and other problems perfectly! To add a new policy, we simply create a new implementation class and declare it as a Bean, and the original caller doesn’t need to change a line of code to take effect.

Tip: If an interface or parent class has multiple implementation classes and I only want to dependency inject a single object, I can use the @Qualifier(“Bean name “) annotation to get the specified Bean.

conclusion

This article introduces three design patterns and how they are used in the Spring framework. The dependency injection modes corresponding to these three design patterns are as follows:

  • Singleton pattern: dependency injection of a single object
  • Chain of responsibility pattern: Dependency injection collection
  • Policy pattern: Dependency injection Map

The key to matching design patterns with the Spring framework is how the objects in the pattern are managed by Spring. This is the core of this article, and it is clear that each design pattern can be used flexibly.

That’s it. All the code in this article is on Github and can be cloned and run. If it is helpful to you, you can click like attention, I will continue to update more original [project practice]!

Wechat reprint please contact the public number [RudeCrab] open the white list, other places reprint please indicate the original address, the original author!