Interviewer: what design patterns does Spring use? Just say three

Posted by bucfan99 on Wed, 05 Jan 2022 16:17:49 +0100

With regard to design patterns, if used properly, it will make our code more concise and extensible. This article mainly explains how to use policy pattern, factory method pattern and Builder pattern in Spring.

1. Strategy mode

The usage of policy pattern is actually relatively simple in Spring. In essence, policy pattern is that there are multiple implementation classes under an interface, and each implementation class will handle a certain situation.

Let's take giving rewards as an example. For example, in our lottery system, there are many reward methods to choose from, such as points, virtual currency and cash. When storing, we will inevitably use a field similar to type to represent these awards. Here, we can use polymorphic methods to distribute awards. For example, we abstract an interface of PrizeSender, and its declaration is as follows:

public interface PrizeSender {

  /**
   * Used to judge whether the current instance supports the payment of current rewards
   */
  boolean support(SendPrizeRequest request);

  /**
   * Award
   */
  void sendPrize(SendPrizeRequest request);

}

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

//Points distribution
@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("Distribute points");
  }
}
//Virtual currency issuance
@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("Issue virtual currency");
  }
}
//Cash disbursement
@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("Cash release");
  }
}

It can be seen here that in each sub type, we only need to control whether the current request is a type that can be processed by the current instance through a parameter of the request in the support() method. If so, the outer control logic will hand over the request to the current instance for processing. There are several points to note about the design of this class:

  • Annotate the current class with @ Component annotation and declare it as a bean managed by the Spring container;
  • Declare a method similar to support() that returns a boolean value, and use this method to control whether the current instance is the instance that processes the target request;
  • Declare a method similar to sendPrize() to process business logic. Of course, the declared method names must be different according to different businesses. Here is just an abstraction of unified business processing;
  • Either the support() method or the sendPrize() method needs to pass an object instead of a simple variable of basic type. The advantage of this is that if you want to add a field in the Request later, you don't need to modify the interface definition and the logic of each implemented subclass;

2. Factory method mode

We explained how to use Spring to declare a policy pattern, how to inject different bean s into different business logic, or what the outer control logic is like. Here we can use the factory method pattern.

The so-called factory method pattern is to define a factory method, return an instance through the passed in parameters, and then process the subsequent business logic through the instance. Generally, the return value type of a factory method is an interface type, and the logic of selecting a specific subclass instance is encapsulated in the factory method. In this way, the outer call logic is separated from the acquisition logic of specific subclasses. The following figure shows a schematic diagram of the factory method mode:

It can be seen that the factory method encapsulates the selection of specific instances, and the client, that is, our caller, only needs to call the specific methods of the factory to obtain specific instances, without caring about the specific instance implementation.

Above, we explained how to use the policy pattern to declare the processing logic in Spring, instead of how to select a specific policy. Here, 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);
  }
}

Here, we declare a factory method getPrizeSender(), whose input parameter is SendPrizeRequest, and the return value is an instance that implements the PrizeSender interface. We can see that in this way, we move the specific selection method down to the specific subclass, because whether the bean that implements PrizeSender supports the processing of the current request, Is implemented by specific subclasses.

In the factory method, we do not have any logic related to specific subclasses, that is, the class can actually dynamically detect newly added subclass instances. This is mainly realized through Spring's automatic injection, mainly because we inject a List here, that is, if there is a new subclass instance of PrizeSender, as long as it is managed by Spring, it will be injected here. The following is a section of code we wrote for testing 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);  //The request here is generally generated according to the database or external call
    PrizeSender prizeSender = prizeSenderFactory.getPrizeSender(request);
    prizeSender.sendPrize(request);
  }
}

In the client code, first obtain a PrizeSender instance through the PrizeSenderFactory, and then issue specific rewards through its sendPrize() method. In this way, the specific reward distribution logic is coupled with the client call. Moreover, according to the previous explanation, we also know that if a reward method is added, we only need to declare a new bean that implements PrizeSender without any modification to the existing code.

3. Builder mode

As for the Builder mode, I think students who have used lombok will say that the Builder mode is very simple. You only need to declare it on a bean with @ Builder annotation. lombok can automatically help us declare it as a Builder bean. I will not comment on this way of use, but as far as my understanding is concerned, there are two main points we need to understand:

1. In terms of its name, the Builder pattern is a Builder. I prefer to understand it as finally generating an object through certain parameters and certain business logic. If only lombok is used, it essentially creates a simple bean, which is no different from building a bean through getter s and setter s;

2. In the Spring framework, the biggest problem with using design patterns is that if Spring beans can be injected into each pattern bean, its usage will be greatly expanded. Because we can really implement the simple parameters passed in, and then conduct certain processing in combination with the beans injected by Spring to construct a bean we need. Obviously, this cannot be achieved by lombok;

For the Builder mode, we can take the structure of SendPrizeRequest for reward payment as an example. When constructing a request object, it must be processed through some parameters passed in from the foreground, and finally a request object is generated. Then we can use the Builder pattern to build a SendPrizeRequest.

Assuming that we can get the prizeId and userId according to the foreground call, we can create a 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;
  }
}

Here is an example of using Spring to maintain a Builder pattern. The specific maintenance method is to annotate the Builder class with @ Component and @ Scope annotations on the Builder class, so that we can inject the required instances into the Builder class for certain business processing. There are several points to be explained about this mode:

  • The @ Scope annotation must be used on the Builder class to indicate that the instance is of prototype type, because it is obvious that the Builder instance here is stateful and cannot be shared by multiple threads;
  • In builder In the build () method, we can conduct certain business processing through the passed in parameters and injected bean s, so as to obtain the parameters required to build a SendPrizeRequest;
  • The Builder class must be decorated with static, because in Java, if the internal class is not decorated with static, the instance of the class must depend on an instance of the external class. In essence, we want to build the external class instance through the internal class instance, that is, when the internal class instance exists, the external class instance does not exist, Therefore, static modification must be used here;
  • According to the use of the standard Builder pattern, each parameter of the external class must be decorated with final, and then you only need to declare the getter method for it.

Above, we have shown how to declare a Builder pattern class in Spring, so how can we use it? Here is an example of our use:

@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);
  }
}

In the above code, we mainly want to take a look
newPrizeSendRequestBuilder() method. In Spring, if a class is a multi instance type, that is, it is marked with @ Scope("prototype"), then ApplicationContext must be used every time the bean is obtained GetBean () method gets a new instance. For specific reasons, readers can refer to relevant documents.

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

4. Summary

This paper mainly explains how to use factory method pattern, policy pattern and Builder pattern in Spring through an example of reward distribution, and emphasizes the points we need to pay attention to when implementing each pattern.

 

Topics: Java Spring Programmer architecture