jim yeh on 七月 7th, 2015

在服務導向架構的服務實作當中,每一個服務處理的主要流程大致上會依照下列步驟來實作:

  1. 拆解訊息
  2. 驗證欄位
  3. 資料處理及運算
  4. 回應處理結果

然而,在大部分驗證欄位及少部分資料處理及運算的情況下,我們常會碰到需要流程控制分歧狀況,使得程式碼的實作變得很繁複而不容易抽離以達到重複使用的模組。舉個例子來說,付款的交易需要檢核付款者帳號、帳戶餘額、收款銀行與帳號等資訊,程式碼大概會長得像這樣:

public PaymentResult pay(PaymentRequest request) {
  PaymentResult response = PaymentResult.getNoting();

  if (isPayerValidated(request)) {
    if (isPayerBalanceEnough(request)) {
      if (isPayeeValidated(request)) {
        response = debitPayer(request);
        sendFinpay(request);
      }
      else {
        response = getBansta(request, PAYEE_IS_NOT_AUTHORITY);
      }
    }
    else {
        response = getBansta(request, BALANCE_IS_NOT_ENOUGH);
    }
  }
  else {
    response = getBansta(request, PAYER_IS_NOT_AUTHORITY);
  }
  return response;
}

當然,上面這一段程式碼還可以從巢狀 if 的寫法改成板狀 if,再加上防衛子句,程式碼會變得比較清晰可讀,但無可避免的,在程式碼當中還是免不了要處理非主流程的分支處理。這樣的缺點是增加每一個服務的實作,這些非主要流程的分支處理都無法省略,無形之中將會分散編程者聚焦程度與增加人為錯誤的疏失。

其實,要解決這個問題並不困難,但需要設計思維的轉變,只需要把命令式編程改成宣告式編程就可以了,這樣我們就可以運用延遲運算的手法來減少分支處理的出現次數。那麼這要怎麼做呢?我們先來看看下面這一段宣告式編程:

public PaymentResult pay(PaymentRequest request) {
  PaymentResultContext context  =
    processSequenceFactory.forRequest(request)
      .with(payerValidation)
      .with(payerBalanceValidation)
      .with(payeeValidation)
      .with(debitExecution)
      .with(finpayNotification);
      .getResultContext();

  return context.getResult();
}

我們看到上面這一段程式碼已經把流程的每一個步驟都變成付款模組的元件,它們可以視業務流程的需要組合在一起來彼此合作,來完成一次的流程實例,同人稱呼這流程實例叫作 ProcessSequence,同時也定義生成它的 Factory 界面。

public interface ProcessSequence<RequestType, ContextType> {
  public ProcessSequence<RequestType, ContextType> with(
    ProcessSequenceAction<RequestType, ContextType> action);

  public ContextTyep getResult()
    throws ProcessSequenceUnhandledException;

}

public interface ProcessSequenceAction<RequestType,
  ContextType> {
  public void execute(RequestType request, ContextType context)
    throws ProcessSequenceActionException;
}

public interface ProcessSequenceFactory<RequestType,
  ContextType> {
  public ProcessSequence<RequestType, ContextType> forRequest(
    RequestType request);

}

接下來,我們需要實作 ProcessSequenceBase 來當成各個實現 ProcessSequence 的 template 基礎類別,主要是封裝了抽象方法 initiateSequenceContext 用來初始化 ContextType 的實例,這個 ContextType 的實例所扮演的其實就是 GOF 建造者樣式的 Builder 角色,它會透過各個傳給 with() 的 ProcessSequenceAction 來逐步建造 ResultContext,然後透過 ProcessSequence 的 getResult() 得到最後的 ResultContext,再從 這個 ResultContext 取得回應結果。ProcessSequenceBase 的實作如下如示:

public interface ProcessSequenceExceptionHandler<ContextType> {

  public boolean processException(Throwable ex,
    ContextType result);

}

public abstract class ProcessSequenceBase<RequestType,
  ContextType> implements ProcessSequence<RequestType,
  ContextType> {

  private RequestType request;

  private List<ProcessSequenceAction<RequestType,
    ContextType>> sequenceActions =
    new ArrayList<ProcessSequenceAction<RequestType,
    ContextType>>();

  public ProcessSequenceBase(RequestType request) {
    this.request = request;
  }

  protected abstract ContextType initiateSequenceContext(
    RequestType request);

  protected ProcessSequenceExceptionHandler<ContextType>
    getExceptionHandler() {
    return null;
  }

  @Override
  public ProcessSequence<RequestType, ContextType> with(
    ProcessSequenceAction<RequestType, ContextType> action) {
      sequenceActions.add(action);
      return this;
    }

  @Override
  public ContextType getResult()
    throws ProcessSequenceUnhandledException {
    Iterator<ProcessSequenceAction<RequestType, ContextType>>
      actionIterator = sequenceActions.iterator();

    final ContextType context =
      initiateSequenceContext(request);

    boolean broken = false;
    while (!broken && actionIterator.hasNext()) {
      ProcessSequenceAction<RequestType, ContextType>
        sequenceAction = actionIterator.next();
      try {
        sequenceAction.execute(request, context);
      } catch (Throwable ex) {
        if (ex instanceof ProcessSequenceUnhandledException) {
          throw (ProcessSequenceUnhandledException) ex;
        }

        ProcessSequenceExceptionHandler<ContextType>
          exHandler = getExceptionHandler();

        broken = ((null == exHandler) ? true :
          exHandler.processException(ex, context));
      }
    }
    return context;
  }
}

上段程式碼有一個值得注意的地方是 getExceptionHandler() 預設處理 ProcessSequenceAction 拋出的異常是回傳 null,代表不做特別的異常處理,也就是當發生異常時中斷流程的處理。當然,繼承 ProcessSequenceBase 的衍生類別可以覆寫 getExceptionHandler() 來特定的異常處理而且可以決定是否中斷流程。這賦予更多的彈性,讓我們可根據不同種類的 Exception 來控制流程,當流程被中斷時,ProcessSequenceBase 會中斷 getResult() 而拋出有異常未被處理的異常 ProcessSequenceUnhandledException,攔截此異常可透過 getCause() 取得未被處理的異常。

於是,我們按照以上的規格實作出 PaymentProcessSequenceFactory 和 PaymentProcessSequence:

public class PaymentProcessSequenceFactory
  implements ProcessSequenceFactory<PaymentRequest,
  PaymentResultContext> {

  @Override
  public ProcessSequence<PaymentRequest,
    PaymentResultContext> forRequest(
      PaymentRequest request) {
    return new PaymentProcessSequence(request);
  }
}

public class PaymentProcessSequence
  extends ProcessSequenceBase<PaymentRequest,
  PaymentResultContext> {

  public PaymentProcessSequence(PaymentRequest request) {
    super(request);
  }

  @Override
  protected ClientResultContext initiateSequenceContext(
    PaymentRequest request) {
    PaymentResultContext result = new PaymentResultContext(
      request);
    return result;
  }
}

從以上以延遲運算實現流程模組化讓我們看到,這個設計樣式和同人先前分享過的語言整合查詢有一些不一樣的地方。語言整合查詢是運用表示式的套疊運算的原理;而這個設計則是引用 Context 的概念,讓各個流程模組元件可以存取並分享處理過程中的資料變化,並且運用 Exception Handler 的機制,來達到流程控制的一致性。我們發現 Context 的本質正是 GOF 的建造者樣式,有別於以 Value Object 組合回傳結果的的方式,採用 Builder 維持我們的抽象概念,也不會違反 Value Object 應該是不變物件的原則。



     

One Response to “以延遲運算實現流程模組化”

  1. [...] 對其它我們會碰到沒那麼複雜的情況,同人的經驗顯示有二種不同解決方式:第一種就是上一篇流程元件化提到的應用 builder pattern 或是更早提到訊息拆解組合應用 visitor pattern,建立一個解決問題過程的 context 脈絡,把答案組合出來,有時候問題比較簡單時,也可能只需要像本篇文章提到只需要應用 strategy pattern 就可以了、另一種方式則是利用泛函編程的高階函式,建構出解決問題的表示式,然後再讓函式一層層套疊的方式來求解,同人不久前分享的語言整合查詢就是這種解法的代表。 [...]

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="">