Viết code nghệ thuật, để trở thành Pro - Phần 2
code style
3
clean code
10
White

Nguyễn Anh Tuấn viết ngày 31/07/2017

Trước khi đi vào phần chính của bài viết, chúng ta cùng điểm tâm bằng một chút design nhé. Một design tốt + code chuẩn là những gì làm nên một hệ thống hoàn hảo.

alt text

Design nói đến ở đây là thiết kế các thành phần của hệ thống phần mềm. Thiết kế tốt sẽ có những đặc tính sau, sẽ có những đánh đổi mà bạn phải cân nhắc cho phù hợp với hệ thống của mình, tuy nhiên nên cố gắng đạt được càng nhiều càng tốt:

  • Độ phức tạp tối thiểu (Minimal complexity): Mục tiêu chính của thiết kế chính là giảm độ phức tạp của hệ thống. Tránh tạo ra những thiết kế "thông minh". Thiết kế "thông minh" thường khiến người dùng khó nắm bắt. Thay vào đó tập trung tạo các thiết kế "đơn giản" và "dễ hiểu". Nếu thiết kế của bạn không giúp bạn bỏ qua những thành phần khác của hệ thống khi đang tập trung làm bộ phận cụ thể, thì thiết kế đó chưa phải là một thiết kế tốt.

  • Dễ bảo trì (Ease of maintenance): Dễ bảo trì có nghĩa là thiết kế với suy nghĩ của một lập trình viên "maintain". Tưởng tượng những câu hỏi mà lập trình viên bảo trì sẽ hỏi về đoạn code bạn đang viết. Hãy nghĩ đến lập trình viên bảo trì như khán giả của mình và thiết kế một hệ thống dễ hiểu.

  • Ít liên kết (Loose coupling): Thiết kế sao cho mức liên kết giữa các thành phần của chương trình là tối thiểu. Sử dụng các nguyên tắc về trừu tượng hoá (abstraction), đóng gói (encapsulation), che giấu thông tin (information hiding) khi thiết kế class để đảm bảo mức liên kết ít nhất. Mức độ liên kết càng thấp thì càng tối thiểu hoá lượng công việc cần làm khi thực hiện tích hợp, test và bảo trì.

  • Tính mở rộng (Extensibility): Bạn có thể cải tiến hệ thống mà không gây xáo trộn đến các cài đặt bên dưới. Bạn có thể thay một thành phần của hệ thống một cách dễ dàng mà không ảnh hưởng đến các thành phần khác.

  • Tính tái sử dụng (Reusability): Thiết kế hệ thống sao cho có thể tải sử dụng các thành phần ở một hệ thống khác.

  • Tính di động (Portability): Hệ thống có thể dễ dàng chuyển sang một môi trường hoạt động khác.

  • Tính gọn gàng (Leanness): Thiết kế sao cho hệ thống không có những phần thừa. Một phần mềm hoàn chỉnh không phải là không còn gì có thể thêm vào được, mà là không có gì có thể bớt đi. Điều này thực sự quan trọng vì phần code thừa phải được review, test khi các thành phần khác thay đổi, nó làm tăng khối lượng công việc phải làm một cách vô ích.

  • Tính phân tầng (Stratification): Hệ thống được trừu tượng hoá thành các tầng. Bạn có thể review hệ thống từ một tầng bất kì và có một cái nhìn nhất quán.

Ví dụ, nếu bạn viết một hệ thống hiện đại nhưng có sử dụng nhiều code cũ, code không được thiết kế tốt, hãy viết một module mới có vai trò cầu nối giữa code cũ và hệ thống đang phát triển. Các phần còn lại của hệ thống sẽ sử dụng các API chuẩn của module này thay vì tương tác trực tiếp với code cũ. Như thế tính xấu của code cũ sẽ được ẩn đi, và nếu chúng ta có cần refactor lại phần code cũ thì không ảnh hưởng gì đến hệ thống mới mà chỉ cần thay đổi một chút ở module cầu nối.

  • Các kỹ thuật chuẩn (Standard techniques): Hệ thống không nên phụ thuộc quá nhiều vào các thành phần lạ, điều đó sẽ giảm bớt độ cực nhọc cho những người sau khi họ muốn nắm bắt cách vận hành của hệ thống. Cố gắng sử dụng những cách thức, module đã được chuẩn hoá và thông dụng nếu có thể.

Tiếp tục phần trước, nếu bạn nào chưa đọc thì có thể xem tại đây.

Viết code nghệ thuật, để trở thành Pro - Phần 1

5. Statement

Tổ chức code theo đường thẳng

Đối với các mệnh đề phải được sắp xếp theo một thứ tự nhất định để đảm bảo hoạt động đúng, sử dụng các biến và tham sổ hàm, cũng như cách đặt tên phù hợp để thể hiện mối tương quan:

data = ReadData();
results = CalculateResultsFromData( data );
PrintResults( results );

Ở đây, data phải được đọc trước khi xử lý sinh ra results, và results phải được tính toán trước khi in ra màn hình. Ba mệnh đề này có mối liên quan chặt chẽ và bắt buộc nó phải được tổ chức theo một thứ tự nhất định.

Ngược lại trong ví dụ sau, mối liên quan thứ tự đã bị ẩn đi, khiến người gọi có thể bị nhầm lẫn:

revenue.ComputeMonthly();
revenue.ComputeQuarterly();
revenue.ComputeAnnual();

Việc tính lợi nhuận theo năm phụ thuộc vào lợi nhuận theo quý đã được tính trước, và lợi nhuận theo quý phụ thuộc vào lợi nhuận theo tháng. Tuy nhiên không có cơ chế nào để bắt người sử dụng gọi các hàm này theo đúng thứ tự.

Đối với các mệnh đề mà thứ tự của chúng không ảnh hưởng đến logic, cần tổ chức sao cho việc tìm kiếm thông tin và đọc hiểu thông tin là dễ nhất. Nguyên tắc là sắp xếp code sao cho chương trình có thể đọc mượt mà liên tục từ trên xuống dưới, không cần nhảy linh tinh, cuộn lên cuộn xuống để xem hàm này cài đặt như thế nào, biến này khai báo ở đâu...

Xét ví dụ sau:

MarketingData marketingData;
SalesData salesData;
TravelData travelData;

travelData.ComputeQuarterly();
salesData.ComputeQuarterly();
marketingData.ComputeQuarterly();

salesData.ComputeAnnual();
marketingData.ComputeAnnual();
travelData.ComputeAnnual();

salesData.Print();
travelData.Print();
marketingData.Print();

Nhìn qua thì có vẻ code được tổ chức rất gọn gàng và logic. Nhưng giả sử chúng ta muốn biết marketingData được tính như thế nào. Bạn phải đi từ dòng cuối cùng và duyệt lên hết đến đầu mã nguồn, ghi nhớ xem ở từng vị trí dữ liệu được xử lý ra làm sao, rất khó khăn.

Tổ chức code lại sao cho các mệnh đề liên quan được gom nhóm với nhau sẽ cải thiện việc đọc hiểu rất nhiều.

MarketingData marketingData;
marketingData.ComputeQuarterly();
marketingData.ComputeAnnual();
marketingData.Print();

SalesData salesData;
salesData.ComputeQuarterly();
salesData.ComputeAnnual();
salesData.Print();

TravelData travelData;
travelData.ComputeQuarterly();
travelData.ComputeAnnual();
travelData.Print();

Mệnh đề if

Tổ chức sao cho trường hợp bình thường được xử lý trước, sau đó mới đến các trường hợp ngoại lệ. Ví dụ sau vi phạm quy tắc này:

OpenFile( inputFile, status )
If ( status = Status_Error ) Then
  errorType = FileOpenError
Else
  ReadFile( inputFile, fileData, status )
  If ( status = Status_Success ) Then
    SummarizeFileData( fileData, summaryData, status )
    If ( status = Status_Error ) Then
      errorType = ErrorType_DataSummaryError
    Else
      PrintSummary( summaryData )
      SaveSummaryData( summaryData, status )
      If ( status = Status_Error ) Then
        errorType = ErrorType_SummarySaveError
      Else
        UpdateAllAccounts()
        EraseUndoFile()
        errorType = ErrorType_None
      End If
    End If
  Else
    errorType = ErrorType_FileReadError
  End If
End If

Code này rất khó đọc vì phần xử lý lỗi và phần xử lý bình thường lẫn với nhau, rất khó có thể đọc được xem trong trường hợp bình thường thì code được chạy như thế nào.

Đoạn code sau đây viết lại theo cách dễ hiểu hơn:

OpenFile( inputFile, status )
If ( status = Status_Success ) Then
  ReadFile( inputFile, fileData, status )
  If ( status = Status_Success ) Then
    SummarizeFileData( fileData, summaryData, status )
    If ( status = Status_Success ) Then
      PrintSummary( summaryData )
      SaveSummaryData( summaryData, status )
      If ( status = Status_Success ) Then
        UpdateAllAccounts()
        EraseUndoFile()
        errorType = ErrorType_None
      Else
        errorType = ErrorType_SummarySaveError
      End If
    Else
      errorType = ErrorType_DataSummaryError
    End If
  Else
    errorType = ErrorType_FileReadError
  End If
Else
  errorType = ErrorType_FileOpenError
End If

Đối với các mệnh đề if-then-else liên tục nhau, xét ví dụ:

if ( inputCharacter < SPACE ) {
    characterType = CharacterType_ControlCharacter;
}
else if (
    inputCharacter == ' ' ||
    inputCharacter == ',' ||
    inputCharacter == '.' ||
    inputCharacter == '!' ||
    inputCharacter == '(' ||
    inputCharacter == ')' ||
    inputCharacter == ':' ||
    inputCharacter == ';' ||
    inputCharacter == '?' ||
    inputCharacter == '-'
) {
    characterType = CharacterType_Punctuation;
}
else if ( '0' <= inputCharacter && inputCharacter <= '9' ) {
    characterType = CharacterType_Digit;
}
else if (
    ( 'a' <= inputCharacter && inputCharacter <= 'z' ) ||
    ( 'A' <= inputCharacter && inputCharacter <= 'Z' )
) {
    characterType = CharacterType_Letter;
}

Thay thế các mệnh đề test phức tạp bằng các hàm tương ứng:

if ( IsControl( inputCharacter ) ) {
    characterType = CharacterType_ControlCharacter;
}
else if ( IsPunctuation( inputCharacter ) ) {
    characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
    characterType = CharacterType_Digit;
}
else if ( IsLetter( inputCharacter ) ) {
    characterType = CharacterType_Letter;
}

Đưa trường hợp phổ biến lên trước, ví dụ nếu chữ cái thông thường là phổ biến nhất thì ta tổ chức lại như sau:

if ( IsLetter( inputCharacter ) ) {
    characterType = CharacterType_Letter;
}
else if ( IsPunctuation( inputCharacter ) ) {
    characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
    characterType = CharacterType_Digit;
}
else if ( IsControl( inputCharacter ) ) {
    characterType = CharacterType_ControlCharacter;
}

Đảm bảo rằng tất cả các trường hợp đều được xử lý, kể cả những trường hợp bạn không có ý định cài đặt:

if ( IsLetter( inputCharacter ) ) {
    characterType = CharacterType_Letter;
}
else if ( IsPunctuation( inputCharacter ) ) {
    characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
    characterType = CharacterType_Digit;
}
else if ( IsControl( inputCharacter ) ) {
    characterType = CharacterType_ControlCharacter;
}
else {
    DisplayInternalError( "Unexpected type of character detected." );
}

Mệnh đề case

Giữ cho phần code trong mỗi case ngắn gọn. Nếu có thể hay thay thế nó bằng một hàm nào đó.

action = userCommand[ 0 ];
switch ( action ) {
  case 'c':
    Copy();
    break;
  case 'd':
    DeleteCharacter();
    break;
  case 'f':
    Format();
    break;
  case 'h':
    Help();
    break;
    ...
  default:
    HandleUserInputError( ErrorType.InvalidUserCommand );
}

Một số lời khuyên khi sắp xếp thứ tự của các case:

  • Sắp xếp theo thứ tự bảng chữ cái: Nếu vai trò của mỗi case là như nhau, sắp theo thứ tự này làm code dễ đọc hơn và việc tìm một case cụ thể dễ hơn.
  • Để các case thường dùng lên đầu tiên: Người đọc có thể tìm các case thường được thực thi nhất một cách nhanh chóng.
  • Để các case bình thường lên trước: Trong trường hợp bạn có một số case xử lý flow thông thường và một số case xử lý ngoại lệ, để case thông thường lên trước. Thêm comment chỉ rõ case nào là bình thường, case nào xử lý ngoại lệ.

Kết thúc mỗi case bắt buộc phải có break, trong trường hợp thật sự cần thiết phải bỏ qua thì bạn cần comment lại rõ ràng lý do tại sao cần code như thế:

switch ( errorDocumentationLevel ) {
  case DocumentationLevel_Full:
    DisplayErrorDetails( errorNumber );
    // FALLTHROUGH -- Full documentation also prints summary comments
  case DocumentationLevel_Summary:
    DisplayErrorSummary( errorNumber );
    // FALLTHROUGH -- Summary documentation also prints error number
  case DocumentationLevel_NumberOnly:
    DisplayErrorNumber( errorNumber );
    break;
  default:
    DisplayInternalError( "Internal Error 905: Call customer support." );
}

Hết

Hai phần vừa qua chỉ là tóm tắt một phần rất nhỏ mà mình nghĩ là đối với một developer có thể nắm bắt và áp dụng ngay được. Ngoài ra còn rất nhiều nội dụng hay mà các bạn có thể tìm hiểu thêm thông qua hai cuốn sách mình đã đề cập tới ở phần một.

Chúc các bạn thành công!

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Nguyễn Anh Tuấn

2 bài viết.
34 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
40 7
(Ảnh) _Bài viết tham khảo nội dung và hình ảnh từ hai cuốn sách gối đầu giường của dev, (Link) và (Link), nếu có thời gian thì các bạn có thể tìm ...
Nguyễn Anh Tuấn viết 1 năm trước
40 7
Bài viết liên quan
White
40 7
(Ảnh) _Bài viết tham khảo nội dung và hình ảnh từ hai cuốn sách gối đầu giường của dev, (Link) và (Link), nếu có thời gian thì các bạn có thể tìm ...
Nguyễn Anh Tuấn viết 1 năm trước
40 7
White
40 16
Nếu coding giống như một trận chiến giữa programer và problem cần giải quyết thì, như những chiến binh thực thự, chúng ta tìm kiếm đạo (phương pháp...
khanhtc viết 1 tháng trước
40 16
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
2 bài viết.
34 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!