天天看點

【075期】面試官問:Spring 使用了哪些設計模式?舉例三種即可

【075期】面試官問:Spring 使用了哪些設計模式?舉例三種即可

>>号外:關注“Java精選”公衆号,回複“面試資料”,免費領取資料!“Java精選面試題”小程式,3000+ 道面試題線上刷,最新、最全 Java 面試題!

關于設計模式,如果使用得當,将會使我們的代碼更加簡潔,并且更具擴充性。本文主要講解Spring中如何使用政策模式,工廠方法模式以及Builder模式。

1. 政策模式

關于政策模式的使用方式,在Spring中其實比較簡單,從本質上講,政策模式就是一個接口下有多個實作類,而每種實作類會處理某一種情況。

我們以發獎勵為例進行講解,比如我們在抽獎系統中,有多種獎勵方式可供選擇,比如積分,虛拟币和現金等。在存儲時,我們必然會使用一個類似于type的字段用于表征這幾種發放獎勵的,那麼這裡我們就可以使用多态的方式進行獎勵的發放。比如我們抽象出一個PrizeSender的接口,其聲明如下:

public interface PrizeSender {

  /**
   * 用于判斷目前執行個體是否支援目前獎勵的發放
   */
  boolean support(SendPrizeRequest request);

  /**
   * 發放獎勵
   */
  void sendPrize(SendPrizeRequest request);

}
           

該接口中主要有兩個方法:support()和sendPrize(),其中support()方法主要用于判斷各個子類是否支援目前類型資料的處理,而sendPrize()則主要是用于進行具體的業務處理的,比如這裡獎勵的發放。下面就是我們三種不同類型的獎勵發放的具體代碼:

// 積分發放
@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("發放積分");
  }
}
           
// 虛拟币發放
@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("發放虛拟币");
  }
}
           
// 現金發放
@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("發放現金");
  }
}
           

這裡可以看到,在每種子類型中,我們隻需要在support()方法中通過request的某個參數來控制目前request是否是目前執行個體能夠處理的類型,如果是,則外層的控制邏輯就會将request交給目前執行個體進行處理。關于這個類的設計,有幾個點需要注意:

  • 使用@Component注解對目前類進行标注,将其聲明為Spring容器所管理的一個bean;
  • 聲明一個傳回boolean值的類似于support()的方法,通過這個方法來控制目前執行個體是否為處理目标request的執行個體;
  • 聲明一個類似于sendPrize()的方法用于處理業務邏輯,當然根據各個業務的不同聲明的方法名肯定是不同的,這裡隻是一個對統一的業務處理的抽象;
  • 無論是support()方法還是sendPrize()方法,都需要傳一個對象進行,而不是簡簡單單的基本類型的變量,這樣做的好處是後續如果要在Request中新增字段,那麼就不需要修改接口的定義和已經實作的各個子類的邏輯;

2. 工廠方法模式

上面我們講解了如何使用Spring來聲明一個政策模式,那麼如何為不同的業務邏輯來注入不同的bean呢,或者說外層的控制邏輯是什麼樣的,這裡我們就可以使用工廠方法模式了。

所謂的工廠方法模式,就是定義一個工廠方法,通過傳入的參數,傳回某個執行個體,然後通過該執行個體來處理後續的業務邏輯。一般的,工廠方法的傳回值類型是一個接口類型,而選擇具體子類執行個體的邏輯則封裝到了工廠方法中了。通過這種方式,來将外層調用邏輯與具體的子類的擷取邏輯進行分離。如下圖展示了工廠方法模式的一個示意圖:

【075期】面試官問:Spring 使用了哪些設計模式?舉例三種即可

可以看到,工廠方法将具體執行個體的選擇進行了封裝,而用戶端,也就是我們的調用方隻需要調用工廠的具體方法擷取到具體的事例即可,而不需要管具體的執行個體實作是什麼。

上面我們講解了Spring中是如何使用政策模式聲明處理邏輯的,而沒有講如何選擇具體的政策,這裡我們就可以使用工廠方法模式。

如下是我們聲明的一個PrizeSenderFactory:

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

這裡我們聲明一個了一個工廠方法getPrizeSender(),其入參就是SendPrizeRequest,而傳回值是某個實作了PrizeSender接口的執行個體,可以看到,通過這種方式,我們将具體的選擇方式下移到了具體的子類中的,因為目前實作了PrizeSender的bean是否支援目前request的處理,是由具體的子類實作的。

在該工廠方法中,我們也沒有任何與具體子類相關的邏輯,也就是說,該類實際上是可以動态檢測新加入的子類執行個體的。這主要是通過Spring的自動注入來實作的,主要是因為我們這裡注入的是一個List,也就是說,如果有新的PrizeSender的子類執行個體,隻要其是Spring所管理的,那麼都會被注入到這裡來。下面就是我們編寫的一段用于測試的代碼來模拟調用方的調用:

@Service
public class ApplicationService {

  @Autowired
  private PrizeSenderFactory prizeSenderFactory;

  public void mockedClient() {
    SendPrizeRequest request = new SendPrizeRequest();
    request.setPrizeType(PrizeTypeEnum.POINT);  // 這裡的request一般是根據資料庫或外部調用來生成的
    PrizeSender prizeSender = prizeSenderFactory.getPrizeSender(request);
    prizeSender.sendPrize(request);
  }
}
           

在用戶端代碼中,首先通過PrizeSenderFactory擷取一個PrizeSender執行個體,然後通過其sendPrize()方法發放具體的獎勵,通過這種方式,将具體的獎勵發放邏輯與用戶端調用進行了解耦。而且根據前面的講解,我們也知道,如果新增了一種獎勵方式,我們隻需要聲明一個新的實作了PrizeSender的bean即可,而不需要對現有代碼進行任何修改。

3. Builder模式

關于Builder模式,我想使用過lombok的同學肯定會說builder模式非常的簡單,隻需要在某個bean上使用@Builder注解進行聲明即可,lombok可以自動幫我們将其聲明為一個Builder的bean。關于這種使用方式,本人不置可否,不過就我的了解,這裡主要有兩個點我們需要了解:

1、Builder模式就其名稱而言,是一個建構者,我更傾向于将其了解為通過一定的參數,通過一定的業務邏輯來最終生成某個對象。如果僅僅隻是使用lombok的這種方式,其本質上也還是建立了一個簡單的bean,這個與通過getter和setter方式建構一個bean是沒有什麼大的差別的;

2、在Spring架構中,使用設計模式最大的問題在于如果在各個模式bean中能夠注入Spring的bean,如果能夠注入,那麼将大大的擴充其使用方式。因為我們就可以真的實作通過傳入的簡單的幾個參數,然後結合Spring注入的bean進行一定的處理後,以構造出我們所需要的某個bean。顯然,這是lombok所無法實作的;

關于Builder模式,我們可以以前面獎勵發放的SendPrizeRequest的構造為例進行講解。在構造request對象的時候,必然是通過前台傳如的某些參數來經過一定的處理,最後生成一個request對象。那麼我們就可以使用Builder模式來建構一個SendPrizeRequest。

這裡假設根據前台調用,我們能夠擷取到prizeId和userId,那麼我們就可以建立一個如下的SendPrizeRequest:

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

這裡就是使用Spring維護一個Builder模式的示例,具體的 維護方式就是在Builder類上使用@Component和@Scope注解來标注該Builder類,這樣我們就可以在Builder類中注入我們所需要的執行個體來進行一定的業務處理了。關于該模式,這裡有幾點需要說明:

  • 在Builder類上必須使用@Scope注解來标注該執行個體為prototype類型,因為很明顯,我們這裡的Builder執行個體是有狀态的,無法被多線程共享;
  • 在Builder.build()方法中,我們可以通過傳入的參數和注入的bean來進行一定的業務處理,進而得到建構一個SendPrizeRequest所需要的參數;
  • Builder類必須使用static修飾,因為在Java中,如果内部類不用static修飾,那麼該類的執行個體必須依賴于外部類的一個執行個體,而我們這裡本質上是希望通過内部類執行個體來建構外部類執行個體,也就是說内部類執行個體存在的時候,外部類執行個體是還不存在的,因而這裡必須使用static修飾;
  • 根據标準的Builder模式的使用方式,外部類的各個參數都必須使用final修飾,然後隻需要為其聲明getter方法即可。

上面我們展示了如何使用Spring的方式來聲明一個Builder模式的類,那麼我們該如何進行使用呢,如下是我們的一個使用示例:

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

上述代碼中,我們主要要看一下newPrizeSendRequestBuilder()方法,在Spring中,如果一個類是多例類型,也即使用@Scope("prototype")進行了标注,那麼每次擷取該bean的時候就必須使用ApplicationContext.getBean()方法擷取一個新的執行個體,至于具體的原因,讀者可查閱相關文檔。

我們這裡就是通過一個單獨的方法來建立一個Builder對象,然後通過流式來為其設定prizeId和userId等參數,最後通過build()方法建構得到了一個SendPrizeRequest執行個體,通過該執行個體來進行後續的獎勵發放。

4. 小結