The thing about design patterns, when used properly, is that they make our code much cleaner and more extensible. This article focuses on how to use the policy pattern, factory method pattern, and Builder pattern in Spring.

1. Policy mode

The use of policy patterns is relatively simple in Spring. Essentially, policy patterns are multiple implementation classes under an interface, and each implementation class handles a particular situation.

For example, in the lottery system, there are a variety of rewards to choose from, such as points, virtual currency and cash. When storing, we must use a field similar to Type to represent these types of rewards, so we can use a polymorphic way of rewards. For example, we abstracted a PrizeSender interface with the following declaration:

Public interface PrizeSender {/** * Boolean support(SendPrizeRequest Request); /** * sendPrize(SendPrizeRequest Request); }Copy the code

There are two main methods in this interface: support() and sendPrize(), where the support() method is mainly used to determine whether each subclass supports the processing of the current type of data, while sendPrize() is mainly used for specific business processing, such as the awarding of rewards here. The following is the specific code of our three different types of rewards:

@Component public class PointSender implements PrizeSender {@override public Boolean support(SendPrizeRequest) request) { return request.getPrizeType() == PrizeTypeEnum.POINT; } @override public void sendPrize(SendPrizeRequest Request) {system.out.println (" send "); }}Copy the code
Component public class VirtualCurrencySender implements PrizeSender {@override public Boolean support(SendPrizeRequest request) { return PrizeTypeEnum.VIRTUAL_CURRENCY == request.getPrizeType(); } @override public void sendPrize(SendPrizeRequest Request) {system.out.println (" send a virtual coin "); }}Copy the code
@Component public class CashSender implements PrizeSender {@override public Boolean support(SendPrizeRequest) request) { return PrizeTypeEnum.CASH == request.getPrizeType(); } @override public void sendPrize(SendPrizeRequest Request) {system.out.println (" send cash "); }}Copy the code

As you can see, in each subtype, we only need to control whether the current request is a type that the current instance can handle by passing one of the request parameters in the support() method. If so, the outer control logic will pass the request to the current instance for processing. There are a few things to note about the design of this class:

  • Annotate the current class with the @Component annotation, declaring it as a bean managed by the Spring container;
  • Declare a support() -like method that returns a Boolean value to control whether the current instance is the instance processing the target request;
  • Declare a method similar to sendPrize() for handling business logic, of course the method name will vary depending on the business declaration, but this is just an abstraction of the unified business process;
  • Both the support() and sendPrize() methods need to be passed an object, rather than simply a variable of the basic type. The advantage of this is that if a new field is added to the Request later, there is no need to change the interface definition and the logic of each subclass that has been implemented.

2. Factory method pattern

Since we’ve shown how to declare a policy pattern using Spring, how to inject different beans for different business logic, or what the outer control logic is, we can use the factory method pattern here.

The factory method pattern is to define a factory method, pass in parameters, return an instance, and then use that instance to process the subsequent business logic. In general, the return type of a factory method is an interface type, and the logic to select a specific subclass instance is encapsulated in the factory method. In this way, the outer calling logic is separated from the fetch logic of the concrete subclasses. The following figure shows a schematic of the factory method pattern:

As you can see, the factory method encapsulates the selection of the concrete instance, while the client, our caller, simply calls the concrete method of the factory to get the concrete instance, regardless of the concrete instance implementation.

Since we explained how Spring uses policy pattern declaration processing logic rather than choosing a specific policy, we can use the factory method pattern.

Here is a PrizeSenderFactory we declare:

@Component public class PrizeSenderFactory { @Autowired private List<PrizeSender> prizeSenders; public PrizeSender getPrizeSender(SendPrizeRequest request) { for (PrizeSender prizeSender : prizeSenders) { if (prizeSender.support(request)) { return prizeSender; } } throw new UnsupportedOperationException("unsupported request: " + request); }}Copy the code

Here we declare a factory method getPrizeSender(), whose input is SendPrizeRequest, and whose return value is an instance that implements the PrizeSender interface. As you can see, in this way we move the selection down to the specific subclass. Whether or not the bean currently implementing PrizeSender supports the current request processing is determined by the concrete subclass.

In this factory method, we also don’t have any logic related to concrete subclasses, which means that the class can actually dynamically detect newly added instances of subclasses. This is mainly done through Spring’s automatic injection, mainly because we are injecting a List here, which means that any new PrizeSender subclass instance that is managed by Spring will be injected here. Here is a piece of test code we wrote to simulate the caller’s call

@Service public class ApplicationService { @Autowired private PrizeSenderFactory prizeSenderFactory; public void mockedClient() { SendPrizeRequest request = new SendPrizeRequest(); request.setPrizeType(PrizeTypeEnum.POINT); / / here's request is typically generated from database or external call PrizeSender PrizeSender = prizeSenderFactory. GetPrizeSender (request); prizeSender.sendPrize(request); }}Copy the code

In the client code, you first get an instance of PrizeSender through the PrizeSenderFactory, and then issue the specific reward through its sendPrize() method, which decoubles the specific reward issuing logic from the client call. We also know from the previous section that if we add a bonus method, we only need to declare a new bean that implements PrizeSender, without making any changes to the existing code.

3. The Builder pattern

As for Builder mode, I’m sure those of you who have used Lombok will tell you that Builder mode is very simple. You just declare a bean with the @Builder annotation, and Lombok automatically declares it as a Builder bean. I can’t say whether or not it is used in this way, but as FAR as I understand it, there are two main points we need to understand:

1. Builder mode, by its name, is a Builder. I prefer to think of it as the final generation of an object through certain parameters and certain business logic. If you just use Lombok this way, you’re still essentially creating a simple bean, which is not that different from building a bean through getters and setters;

2. The biggest problem with using design patterns in the Spring framework is that if you can inject Spring beans into each pattern bean, it will greatly expand the way you use them. Because we can actually construct a bean that we need by passing in a few simple parameters and then doing some processing with the spring-injected bean. Obviously, this is something Lombok can’t do;

For the Builder pattern, we can use the construction of SendPrizeRequest as an example. When constructing a Request object, some parameters passed by the foreground must be processed to generate a request object. Then we can use the Builder pattern to build a SendPrizeRequest.

Assuming we can get prizeId and userId from the foreground call, we can create SendPrizeRequest as follows:

public class SendPrizeRequest { private final PrizeTypeEnum prizeType; private final int amount; private final String userId; public SendPrizeRequest(PrizeTypeEnum prizeType, int amount, String userId) { this.prizeType = prizeType; this.amount = amount; this.userId = userId; } @Component @Scope("prototype") public static class Builder { @Autowired PrizeService prizeService; private int prizeId; private String userId; public Builder prizeId(int prizeId) { this.prizeId = prizeId; return this; } public Builder userId(String userId) { this.userId = userId; return this; } public SendPrizeRequest build() { Prize prize = prizeService.findById(prizeId); return new SendPrizeRequest(prize.getPrizeType(), prize.getAmount(), userId); } } public PrizeTypeEnum getPrizeType() { return prizeType; } public int getAmount() { return amount; } public String getUserId() { return userId; }}Copy the code

Here is an example of using Spring to maintain a Builder pattern by annotating the Builder class with @Component and @Scope annotations. This allows us to inject the required instances into the Builder class to do some business processing. There are a few things to note about this pattern:

  • The @Scope annotation must be used on the Builder class to indicate that the instance is of type Prototype, because obviously our Builder instance is stateful and cannot be shared by multiple threads;
  • In the Builder.build() method, we can do some business with the parameters passed in and the injected bean to get the parameters needed to build a SendPrizeRequest;
  • The Builder class must use the static modifier, because in Java, if an inner class does not use the static modifier, then the instance of that class must depend on an instance of the outer class, and we essentially want to build the outer class instance from the inner class instance, which means that while the inner class instance exists, The external class instance does not yet exist, so the static modifier must be used;
  • According to the way the standard Builder pattern is used, each parameter of the external class must be final and then only need to declare getter methods for it.

We have shown how to declare a class in Builder mode using Spring. Here is an example of how to do this:

@Service public class ApplicationService { @Autowired private PrizeSenderFactory prizeSenderFactory; @Autowired private ApplicationContext context; public void mockedClient() { SendPrizeRequest request = newPrizeSendRequestBuilder() .prizeId(1) .userId("u4352234") .build(); PrizeSender prizeSender = prizeSenderFactory.getPrizeSender(request); prizeSender.sendPrize(request); } public Builder newPrizeSendRequestBuilder() { return context.getBean(Builder.class); }}Copy the code

The above code, are we going to look at the main newPrizeSendRequestBuilder () method, in the Spring, if a class is more types, namely using the @ the Scope has carried on the annotation (” prototype “), So every time to obtain the bean must be used ApplicationContext. GetBean () method to obtain a new instance, as for the specific reason, readers can refer to the documentation.

Here, we create a Builder object through a separate method, and then set parameters such as prizeId and userId for it through streaming. Finally, we build a SendPrizeRequest instance through the build() method, which is used for subsequent reward delivery.

4. Summary

This article mainly uses a reward distribution example to explain how to use the factory method pattern, the policy pattern and the Builder pattern in Spring, and highlights the points we need to pay attention to the implementation of each pattern.

The article will continue to be updated. You can add my wechat account and reply to [Information] that I have prepared the interview materials and resume templates for first-line big factories