為什麼花一整章談 Null#
Null 並不是 Martin Fowler 在 Refactoring 中明白條列的程式碼異味(Code Smell),但因為它太常見、太討厭,值得單獨討論。
常見的 Null 出現場景#
Null 通常出現在三個地方,但討厭程度不一樣:
- 方法內部:屬於設計者自家的事,外部看不到,影響有限
- 方法 Input:呼叫者傳入了 Null,方法可以選擇預先檢查或假設遵守約定。錯誤雖由方法發出,但成因在呼叫者
- 方法 Output:最討厭。當一個方法回傳 Null,呼叫者在不知情的情況下取用,會在執行期出錯。錯誤完全是方法本身造成的
為什麼回傳 Null 是壞味道#
回傳 Null 至少有兩個問題:
- 語意不明
- 製造重複程式碼
語意不明#
當一個方法回傳 Null,可能代表:
- 找不到要的東西
- 使用者輸入有誤
- 運算過程出錯(可修復或不可修復)
- 要找的東西本身就是 Null
呼叫者不知道哪一種,必須翻文件、問作者、甚至讀原始碼才能決定怎麼處理。這在現代軟體開發中是非常浪費時間、不負責任的設計。
重複程式碼#
即便查清楚了,呼叫者也得在每次呼叫之後加一行 if (___ == null) 判斷。方法被呼叫幾次,這個 If 就被寫幾次。重複永遠是萬惡之源。
解決之道#
依語言與框架不同,可選方案有:
- Optional:Java 8+ 的 Optional 放在簽名上,等於宣告「這裡可能空值,請事先處理」。簽名上沒有 Optional 的方法就保證不會回傳 Null
- 空集合(List):查詢場景下「找不到」就回傳空 List,比回傳 Null 自然,呼叫者用 for-loop 直接跑就好
- Null Object Pattern:適用於回傳多型物件的場景。實作端發現對應物件不存在時,回傳一個「代表不存在」的特殊實作,呼叫者可以照常操作而不出錯
- Exception:若方法本身就能判定「不存在」屬於錯誤情況,直接丟例外讓呼叫者決定如何處理
範例:用 List 取代 Null#
要找某學期某課程分數最高的同學並寄獎學金通知信。可能有「一人最高」、「同分多人」、「沒人修課」三種情況。如果 Repository 用 List 回傳,Service 就非常簡潔:
public class FindTopAndNotifyService {
private TranscriptRepository repository;
private SendResultEmailService emailService;
public FindTopAndNotifyService(TranscriptRepository repository,
SendResultEmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
public void execute(String semester, long courseId) {
List transcripts = repository.findHighestScore(semester, courseId);
for (Transcript transcript : transcripts) {
long studentId = transcript.getStudentId();
this.emailService.send(studentId,
"Congratulations! You've got Scholarship");
}
}
}無論回傳一筆、多筆、還是空集合,for-loop 都能正確運行,不需任何特別判斷。
對應的測試#
class FindTopAndNotifyServiceTest {
private final TranscriptRepository repository = Mockito.mock(TranscriptRepository.class);
private final SendResultEmailService emailService = Mockito.mock(SendResultEmailService.class);
private final FindTopAndNotifyService service =
new FindTopAndNotifyService(repository, emailService);
@Test
void one_student() {
given_highest_score_students("2021-fall", 9527L, transcript(55688L));
when_execute_service("2021-fall", 9527L);
then_send_email_like(55688L, 1);
}
@Test
void many_students() {
given_highest_score_students("2021-fall", 9527L,
transcript(55688L), transcript(3345678L));
when_execute_service("2021-fall", 9527L);
then_send_email_like(55688L, 1);
then_send_email_like(3345678L, 1);
}
@Test
void NO_students() {
given_highest_score_students("2021-fall", 9527L);
when_execute_service("2021-fall", 9527L);
then_NEVER_send_emails();
}
private void then_NEVER_send_emails() {
Mockito.verify(emailService, Mockito.times(0))
.send(anyLong(), eq("Congratulations! You've got Scholarship"));
}
private void then_send_email_like(long studentId, int invokes) {
Mockito.verify(emailService, Mockito.times(invokes))
.send(studentId, "Congratulations! You've got Scholarship");
}
private void when_execute_service(String semester, long courseId) {
service.execute(semester, courseId);
}
private Transcript transcript(long studentId) {
return new Transcript(studentId);
}
private void given_highest_score_students(String semester, long courseId,
Transcript... transcripts) {
Mockito.when(repository.findHighestScore(semester, courseId))
.thenReturn(Arrays.asList(transcripts));
}
}透過適當提取方法(Extract Method),測試本身能清楚揭露使用場景與意圖。
輸入 Null 整自己,輸出 Null 害別人。盡量在介面層級就消除 Null。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10264214