程式碼自然語言化撰寫方法論
程式碼自然語言化撰寫方法論
程式碼不是寫給電腦看的,是寫給人類讀的。電腦只管執行,人類才要維護。
認知負擔:一切的出發點
人類工作記憶有限,大約一次只能處理七個項目(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分鐘內能理解主要邏輯算合格,需要解釋才能理解就要重寫。
自然語言測試:把程式碼翻譯成中文說出來。翻譯流暢自然算合格,說不清楚就改命名。
六個月後測試:假設半年後的自己要修改這段程式碼,能快速找到位置算合格,不敢動怕壞掉就要重新設計。
每一行程式碼都是一句話,每個函式都是一個段落。好的程式碼是對未來維護者的體貼——不只是風格偏好,而是降低維護成本的工程決策。