延續上一章,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 對
Scholarship與Student的內部資料碰得太多,這部分會留到下一章與 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