程式碼不是寫給電腦看的,是寫給人類讀的。電腦只管執行,人類才要維護。

認知負擔:一切的出發點

人類工作記憶有限,大約一次只能處理七個項目(Miller’s Law)。看到縮寫要在腦中展開、看到模糊詞要猜測含義、看到長函式要分段記憶——這些都是認知負擔。

自然語言化的目標很簡單:讓程式碼像讀文章一樣自然,把讀者的認知資源留給理解業務邏輯,而不是解碼程式碼本身。


第一原則:命名要能直接讀懂

函式命名

1// 錯誤:不知道在做什麼
2void process(data) {}
3void handle(item) {}
4
5// 正確:一眼看懂
6void calculateBookReadingProgress(Book book) {}
7void validateUserRegistrationData(User user) {}
8void enrichBookMetadataFromExternalSource(Book book) {}

變數命名

1// 錯誤:縮寫和多用途
2var usr = getCurrentUser();
3var data = loadUserData();
4data = processBookData(); // 100行後同一變數換了意思
5
6// 正確:明確且專用
7final authenticatedUser = getCurrentUser();
8final userProfileData = loadUserData();
9final enrichedBookMetadata = processBookData();

類別命名

1// 錯誤:說不清在做什麼
2class Manager {}
3class Handler {}
4class BookDAO {}
5
6// 正確:業務職責一目了然
7class BookMetadataEnrichmentService {}
8class UserRegistrationValidator {}
9class LibraryBookSearchEngine {}

布林命名

布林變數應該能讀成問句,在 if 裡就能自然被理解:

1bool isValid;      // "Is valid?"
2bool hasPermission; // "Has permission?"
3bool canEdit;       // "Can edit?"
4
5// 不好:語意不清
6bool permission;

常見命名反模式

  • 匈牙利命名法strName, intCount — 型別系統自己會提供型別資訊,名稱不用重複
  • 無意義前綴theUser, aBook — 沒帶來任何資訊,直接刪掉
  • 過度縮寫usrMgr — 迫使讀者展開,userManager 更自然
  • 數字後綴user1, user2 — 改成 primaryUser, secondaryUser 才說明關係

第二原則:函式控制在五到十行

超過十行通常表示函式承擔了多重職責。判斷是否需要拆分的最快方法:函式名稱裡有「和」或「或」的話,一定要拆。

 1// 錯誤:15行,三種職責混在一起
 2Book processBook(String isbn) {
 3  if (isbn.length != 13) throw ArgumentError('Invalid ISBN');
 4  if (!isValidISBNChecksum(isbn)) throw ArgumentError('ISBN checksum failed');
 5  final apiResponse = httpClient.get('/books/$isbn');
 6  if (apiResponse.statusCode != 200) throw Exception('API failed');
 7  final bookData = json.decode(apiResponse.body);
 8  return Book.create(
 9    id: BookId(generateUniqueId()),
10    title: BookTitle(bookData['title']),
11    source: BookSource.external(),
12  );
13}
14
15// 正確:拆成三個單一職責函式
16Book createBookFromISBN(String isbn) {
17  validateISBNFormat(isbn);
18  final bookData = fetchBookDataFromExternalAPI(isbn);
19  return buildBookFromExternalData(bookData);
20}
21
22void validateISBNFormat(String isbn) {
23  if (isbn.length != 13) throw ArgumentError('ISBN must be 13 digits');
24  if (!isValidISBNChecksum(isbn)) throw ArgumentError('ISBN checksum validation failed');
25}
26
27Map<String, dynamic> fetchBookDataFromExternalAPI(String isbn) {
28  final apiResponse = httpClient.get('/books/$isbn');
29  if (apiResponse.statusCode != 200) throw Exception('Failed to fetch book data');
30  return json.decode(apiResponse.body);
31}
32
33Book buildBookFromExternalData(Map<String, dynamic> bookData) {
34  return Book.create(
35    id: BookId(generateUniqueId()),
36    title: BookTitle(bookData['title']),
37    source: BookSource.external(),
38  );
39}

第三原則:一個變數只做一件事

同一個變數在不同地方承載不同意義,是我見過最難追蹤的 bug 來源之一。

 1// 錯誤:同一變數三種身分
 2var result = validateUser(userData);
 3if (result.isValid) {
 4  result = processPayment(paymentData);
 5  if (result.success) {
 6    result = updateDatabase(result.data);
 7  }
 8}
 9
10// 正確:每個變數都有自己的名字
11final userValidationResult = validateUser(userData);
12if (userValidationResult.isValid) {
13  final paymentProcessingResult = processPayment(paymentData);
14  if (paymentProcessingResult.success) {
15    final databaseUpdateResult = updateDatabase(paymentProcessingResult.data);
16  }
17}

變數生命週期也要管:

 1// 錯誤:books 在100行間一直換狀態
 2void processLibraryBooks() {
 3  var books = getAllBooks();
 4  books = filterAvailableBooks(books);
 5  books = sortBooksByTitle(books);
 6}
 7
 8// 正確:每個階段的狀態都有名字
 9void processLibraryBooks() {
10  final allLibraryBooks = getAllBooks();
11  final availableBooks = filterAvailableBooks(allLibraryBooks);
12  final sortedAvailableBooks = sortBooksByTitle(availableBooks);
13}

第四原則:用事件驅動表達業務流程

複雜的業務流程往往會寫成一個大函式,裡面塞滿 if/else。問題不在於 if/else 本身,而是把不同職責的邏輯混在一起。

 1// 錯誤:驗證、API呼叫、結果處理全混在一起
 2void submitForm(FormData formData) {
 3  if (formData.name.isEmpty) { showErrorMessage('姓名不能為空'); return; }
 4  if (formData.email.isEmpty) { showErrorMessage('Email不能為空'); return; }
 5  if (!isValidEmail(formData.email)) { showErrorMessage('Email格式不正確'); return; }
 6  final apiResult = submitToAPI(formData);
 7  if (apiResult.success) {
 8    showSuccessMessage('提交成功');
 9    navigateToSuccessPage();
10    clearForm();
11  } else {
12    showErrorMessage('提交失敗:' + apiResult.error);
13    highlightErrorFields(apiResult.errorFields);
14  }
15}
16
17// 正確:每個事件有自己的函式
18void submitUserRegistrationForm(UserRegistrationFormData formData) {
19  final validationResult = validateUserRegistrationData(formData);
20  if (validationResult.isValid) {
21    handleSuccessfulValidation(formData);
22  } else {
23    handleValidationFailure(validationResult.errors);
24  }
25}
26
27ValidationResult validateUserRegistrationData(UserRegistrationFormData formData) {
28  final errors = <ValidationError>[];
29  if (!isValidUserName(formData.name)) errors.add(ValidationError.invalidUserName());
30  if (!isValidUserEmail(formData.email)) errors.add(ValidationError.invalidEmail());
31  return ValidationResult.fromErrors(errors);
32}
33
34void handleSuccessfulValidation(UserRegistrationFormData formData) {
35  submitUserRegistrationToAPI(formData)
36    .then(handleSuccessfulAPIResponse)
37    .catchError(handleAPIFailure);
38}
39
40void handleValidationFailure(List<ValidationError> errors) {
41  displayValidationErrors(errors);
42  highlightInvalidFormFields(errors);
43}

狀態機也是同樣的道理:

 1// 錯誤:一個函式根據狀態做完全不同的事
 2void updateBookStatus(Book book, String newStatus) {
 3  if (newStatus == 'available') {
 4    book.status = BookStatus.available;
 5    book.borrower = null;
 6    updateSearchIndex(book);
 7    notifyWaitingUsers(book);
 8  } else if (newStatus == 'borrowed') {
 9    book.status = BookStatus.borrowed;
10    book.borrowDate = DateTime.now();
11    sendBorrowConfirmation(book);
12  } else if (newStatus == 'maintenance') {
13    book.status = BookStatus.maintenance;
14    removeFromSearchIndex(book);
15    notifyMaintenanceTeam(book);
16  }
17}
18
19// 正確:每個事件獨立
20void handleBookReturnEvent(Book book) {
21  executeBookReturn(book);
22  notifyBookBecameAvailable(book);
23}
24
25void handleBookBorrowEvent(Book book, User borrower) {
26  executeBookBorrow(book, borrower);
27  confirmBorrowingToUser(book, borrower);
28}
29
30void handleBookMaintenanceEvent(Book book) {
31  markBookForMaintenance(book);
32  notifyMaintenanceRequired(book);
33}

第五原則:可讀性優於簡潔性

程式碼的價值排序:正確性 > 可讀性 > 可維護性 > 簡潔性。

行數從來不是指標,清晰才是:

 1// 錯誤:為了少寫幾行犧牲可讀性
 2books.where((b) => b.s == 'a' && b.p > 100).map((b) => b.t).toList();
 3
 4// 正確:每一步都說清楚在做什麼
 5final availableBooksWithMoreThan100Pages = allBooks
 6    .where((book) => book.status == BookStatus.available)
 7    .where((book) => book.pageCount > 100)
 8    .toList();
 9
10final bookTitlesForDisplay = availableBooksWithMoreThan100Pages
11    .map((book) => book.title.value)
12    .toList();

如何驗證程式碼品質

陌生人測試:讓不熟悉這段程式碼的工程師讀。5分鐘內能理解主要邏輯算合格,需要解釋才能理解就要重寫。

自然語言測試:把程式碼翻譯成中文說出來。翻譯流暢自然算合格,說不清楚就改命名。

六個月後測試:假設半年後的自己要修改這段程式碼,能快速找到位置算合格,不敢動怕壞掉就要重新設計。


每一行程式碼都是一句話,每個函式都是一個段落。好的程式碼是對未來維護者的體貼——不只是風格偏好,而是降低維護成本的工程決策。