延續上一章,ApplyScholarshipService 已經完成資料取得的部分。接下來這位「獎學金申請承辦專員」要做的事還有:驗證申請是否合乎規定、填寫正式申請書、請檔案管理員歸檔。

剩下的測項:

  • 超過申請時間 ➡️ Exception 374
  • 資格不符 ➡️ Exception 375
  • Repository 儲存資料時失敗 ➡️ Exception 666

會長大的待辦清單#

「超過申請時間」原先沒列在計畫裡。這是正常的:人對需求的理解會隨著接觸而加深。Kent Beck 在《Test-Driven Development》也提過,測試代表需求,當需求成長,待辦清單自然會跟著長大。

測項六:超過申請時間#

@Test
void when_overtime_then_374() throws RepositoryAccessDataFailException {

    given_student_exists(12345L);
    given_scholarship_exists(98765L, scholarship(2021, 7, 31));
    given_today_is(2021, 8, 1);

    when_apply_and_fail_on_server_side(application_form(12345L, 98765L));

    then_client_side_error_code_is(374);

}

學生與獎學金都存在,但獎學金截止日為 7/31,今日 8/1,預期回 374 表示申請逾期。

public void apply(ApplicationForm applicationForm) throws ClientSideErrorException, ServerSideErrorException {

    // 調閱學生資料
    Student student = findStudent(applicationForm);

    // 調閱獎學金規定的資料
    Scholarship scholarship = findScholarship(applicationForm);

    // 查驗是否符合資格
    LocalDate deadline = scholarship.getDeadline();
    LocalDate now = LocalDate.now();
    if (now.isAfter(deadline)) {
        throw new ClientSideErrorException("application over time", 374);
    }

    // 查驗是否符合資格
    // 填寫正式申請書
    // 存檔

}

抽象程度不一致,把日期判斷抽出去:

public void apply(ApplicationForm applicationForm) throws ClientSideErrorException, ServerSideErrorException {

    // 調閱學生資料
    Student student = findStudent(applicationForm);

    // 調閱獎學金規定的資料
    Scholarship scholarship = findScholarship(applicationForm);

    // 查驗是否符合資格
    checkDeadline(scholarship);

    // 查驗是否符合資格
    // 填寫正式申請書
    // 存檔

}

真實工作上若發現重複,做完新功能後應該直接移除舊的或抽出共用,因為重複永遠是不被歡迎的對象。本系列為了便於對照而保留兩處示範。

測項七:資格不符#

假設此獎學金「只有博士班」可申請,學生卻是大學生,預期回 375:

private static final LocalDate july31 = LocalDate.of(2021, 7, 31);

@Test
void when_disqualified_then_375() throws RepositoryAccessDataFailException {

    given_student_exists(12345L);

    given_scholarship_exists(98765L, scholarship(july31));

    given_today_is(july31);

    when_apply_with_form_and_client_side_error_happens(application_form(12345L, 98765L));

    then_client_side_error_code_is(375);

}
public void apply(ApplicationForm applicationForm) throws ClientSideErrorException, ServerSideErrorException {

    // 調閱學生資料
    Student student = findStudent(applicationForm);

    // 調閱獎學金規定的資料
    Scholarship scholarship = findScholarship(applicationForm);

    // 查驗是否符合資格
    checkDeadline(scholarship);

    // 查驗是否符合資格
    checkProgramIsPhD(student);
    // 填寫正式申請書
    // 存檔

}

這裡其實藏著一個明顯的壞味道:Feature Envy。Service 對 ScholarshipStudent 的內部資料碰得太多,這部分會留到下一章與 Entity 一起處理。

測項八:寫入申請單失敗#

最後一個動作是請檔案管理員把正式申請書歸檔。如果歸檔失敗,Service 要轉拋 Controller 看得懂的錯誤:

@Test
void when_DB_fail_on_writing_application_to_DB_then_666() throws RepositoryAccessDataFailException {

    given_student_exists(12345L, "PhD");

    given_scholarship_exists(98765L, scholarship());

    given_today_is(july31);

    assume_DB_would_fail_on_creating_application_data();

    when_apply_and_fail_on_server_side(application_form(12345L, 98765L));

    then_server_side_error_code_should_be(666);

}
public void apply(ApplicationForm applicationForm) throws ClientSideErrorException, ServerSideErrorException {

    // 調閱學生資料
    Student student = findStudent(applicationForm);

    // 調閱獎學金規定的資料
    Scholarship scholarship = findScholarship(applicationForm);

    // 查驗是否符合資格
    checkDeadline(scholarship);

    // 查驗是否符合資格
    checkProgramIsPhD(student);

    // 填寫正式申請書
    Application application = applicationForm.toApplication();

    // 存檔
    createApplication(application);

}

思考題:重複申請怎麼處理?#

這時會發現一個新問題:同一個人重複申請時該如何處理?

直覺上可以先問 Repository 是否已存在,但在微服務、多台機器同時上線的場景下,前一刻檢查時還沒有的資料,幾百毫秒後可能已經被另一台寫入。問題本身不算難也不算容易,因為決策因素眾多:儲存裝置是否為 RDB、是本機儲存還是委託其他服務、資料設計是否有可辨識的欄位、需要 Consistent 還是 Eventually Consistent 即可。

這類問題必須綜合真實世界完整 Context 才能下決定,本章不直接給標準答案,留給讀者依自身環境思考。

結論#

兩章下來,使用 TDD 演練了 Use Case 層的 Service 該如何完成自動化流程。剩下 Repository 與 Entity 兩個角色將在下一章接著討論。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10271853