Clean Code

1. Code sạch

Cái giá của sự lộn xộn

Nếu bạn là một lập trình viên đã làm việc trong 2 hoặc 3 năm, rất có thể bạn đã bị mớ code lộn xộn của người khác kéo bạn lùi lại. Nếu bạn đã là một lập trình viên lâu hơn 3 năm, rất có thể bạn đã tự làm chậm sự phát triển của bản thân bằng đống code do bạn tạo ra. Trong khoảng 1 hoặc 2 năm, các đội đã di chuyển rất nhanh ngay từ khi bắt đầu một dự án, thay vì phải di chuyển thận trọng như cách họ nhìn nhận nó. Vì vậy, mọi thay đổi mà họ tác động lên code sẽ phá vỡ vài đoạn code khác. Không có thay đổi nào là không quan trọng. Mọi sự bổ sung hoặc thay đổi chương trình đều tạo ra các mớ boòng boong, các nút thắt,… Chúng ta cố gắng hiểu chúng chỉ để tạo ra thêm sự thay đổi, và lặp lại việc tạo ra chính chúng. Theo thời gian, code của chúng ta trở nên quá “cao siêu” mà không thành viên nào có thể hiểu nổi. Chúng ta không thể “làm sạch” chúng, hoàn toàn không có cách nào cả 😥.

Khi đống code lộn xộn được tạo ra, hiệu suất của cả đội sẽ bắt đầu tuột dốc về phía 0. Khi hiệu suất giảm, người quản lý làm công việc của họ – đưa vào nhóm nhiều thành viên mới với hy vọng cải thiện tình trạng. Nhưng những nhân viên mới lại thường không nắm rõ cách hoạt động hoặc thiết kế của hệ thống, họ cũng không chắc thay đổi nào sẽ là phù hợp cho dự án. Hơn nữa, họ và những người cũ trong nhóm phải chịu áp lực khủng khiếp cho tình trạng tồi tệ của nhóm. Vậy là, càng làm việc, họ càng tạo ra nhiều code rối, và đưa cả nhóm (một lần nữa) dần tiến về cột mốc 0.

Đập đi xây lại

Đập đi xây lại

Cuối cùng, cả nhóm quyết định nổi loạn. Họ thông báo cho quản lý rằng họ không thể tiếp tục phát triển trên nền của đống code lộn xộn này nữa, rằng họ muốn thiết kế lại dự án. Dĩ nhiên ban quản lý không muốn mất thêm tài nguyên cho việc tái khởi động dự án, nhưng họ cũng không thể phủ nhận sự thật rằng hiệu suất làm việc của cả nhóm quá tàn tạ. Cuối cùng, họ chiều theo yêu cầu của các lập trình viên và cho phép bắt đầu lại dự án.

Một nhóm mới được chọn. Mọi người đều muốn tham gia nhóm này vì nó năng động và đầy sức sống. Nhưng chỉ những người giỏi nhất mới được chọn, những người khác phải tiếp tục duy trì dự án hiện tại.

2. Quy tắc đặt tên rõ nghĩa

Chọn một từ cho mỗi khái niệm

Chọn một từ cho một khái niệm và gắn bó với nó. Ví dụ, rất khó hiểu khi fetchretrieve và get là các phương thức có cùng chức năng, nhưng lại đặt tên khác nhau ở các lớp khác nhau. Làm thế nào để nhớ phương thức nào đi với lớp nào? Buồn thay, bạn phải nhớ tên công ty, nhóm hoặc cá nhân nào đã viết ra các thư viện hoặc các lớp, để nhớ cụm từ nào được dùng cho các phương thức. Nếu không, bạn sẽ mất thời gian để tìm hiểu chúng trong các đoạn code trước đó.

Các công cụ chỉnh sửa hiện đại như Eclipse và IntelliJ cung cấp các định nghĩa theo ngữ cảnh, chẳng hạn như danh sách các hàm bạn có thể gọi trên một đối tượng nhất định. Nhưng lưu ý rằng, danh sách thường không cung cấp cho bạn các ghi chú bạn đã viết xung quanh tên hàm và danh sách tham số. Bạn may mắn nếu nó cung cấp tên tham số từ các khai báo hàm. Tên hàm phải đứng một mình, và chúng phải nhất quán để bạn có thể chọn đúng phương pháp mà không cần phải tìm hiểu thêm.

Tương tự như vậy, rất khó hiểu khi controllermanager và driver lại xuất hiện trong cùng một mã nguồn. Có sự khác biệt nào giữa DeviceManager và ProtocolController? Tại sao cả hai đều không phải là controller hay manager? Hay cả hai đều cùng là driver? Tên dẫn bạn đến hai đối tượng có kiểu khác nhau, cũng như có các lớp khác nhau.

Một từ phù hợp chính là một ân huệ cho những lập trình viên phải dùng code của bạn.

Đừng chơi chữ

Tránh dùng cùng một từ cho hai mục đích. Sử dụng cùng một thuật ngữ cho hai ý tưởng khác nhau về cơ bản là một cách chơi chữ.

Nếu bạn tuân theo nguyên tắc Chọn một từ cho mỗi khái niệm, bạn có thể kết thúc nhiều lớp với một…Ví dụ, phương thức add. Miễn là danh sách tham số và giá trị trả về của các phương thức add này tương đương về ý nghĩa, tất cả đều tốt.

Tuy nhiên, người ta có thể quyết định dùng từ add khi người đó không thực sự tạo nên một hàm có cùng ý nghĩa với cách hoạt động của hàm add. Giả sử chúng tôi có nhiều lớp, trong đó add sẽ tạo một giá trị mới bằng cách cộng hoặc ghép hai giá trị hiện tại. Bây giờ, giả sử chúng tôi đang viết một lớp mới và có một phương thức thêm tham số của nó vào mảng. Chúng tôi có nên gọi nó là add không? Có vẻ phù hợp đấy, nhưng trong trường hợp này, ý nghĩa của chúng là khác nhau, vậy nên chúng tôi dùng một cái tên khác như insert hay append để thay thế. Nếu được dùng cho phương thức mới, add chính xác là một kiểu chơi chữ.

Mục tiêu của chúng tôi, với tư cách là tác giả, là làm cho code của chúng tôi dễ hiểu nhất có thể. Chúng tôi muốn code của chúng tôi là một bài viết ngắn gọn, chứ không phải là một bài nghiên cứu […].

Dùng thuật ngữ

Hãy nhớ rằng những người đọc code của bạn là những lập trình viên, vậy nên hãy sử dụng các thuật ngữ khoa học, các thuật toán, tên mẫu (pattern),…cho việc đặt tên. Sẽ không khôn ngoan khi bạn đặt tên của vấn đề theo cách mà khách hàng định nghĩa. Chúng tôi không muốn đồng nghiệp của mình phải tìm khách hàng để hỏi ý nghĩa của tên, trong khi họ đã biết khái niệm đó – nhưng là dưới dạng một cái tên khác.

Tên AccountVisitor có ý nghĩa rất nhiều đối với một lập trình viên quen thuộc với mẫu VISITOR (VISITOR pattern). Có lập trình viên nào không biết JobQueue? Có rất nhiều thứ liên quan đến kỹ thuật mà lập trình viên phải đặt tên. Chọn những tên thuật ngữ thường là cách tốt nhất.

Thêm ngữ cảnh thích hợp

Thêm ngữ cảnh thích hợp

Chỉ có một vài cái tên có nghĩa trong mọi trường hợp – số còn lại thì không. Vậy nên, bạn cần đặt tên phù hợp với ngữ cảnh, bằng cách đặt chúng vào các lớp, các hàm hoặc các không gian tên (namespace). Khi mọi thứ thất bại, tiền tố nên được cân nhắc như là giải pháp cuối cùng.

Hãy tưởng tượng bạn có các biến có tên là firstNamelastNamestreethouseNumbercitystate và zipcode. Khi kết hợp với nhau, chúng rõ ràng tạo thành một địa chỉ. Nhưng nếu bạn chỉ thấy biến state được sử dụng một mình trong một phương thức thì sao? Bạn có thể suy luận ra đó là một phần của địa chỉ không?

Bạn có thể thêm ngữ cảnh bằng cách sử dụng tiền tố: addrFirstNameaddrLastNameaddrState,… Ít nhất người đọc sẽ hiểu rằng những biến này là một phần của một cấu trúc lớn hơn. Tất nhiên, một giải pháp tốt hơn là tạo một lớp có tên là Address. Khi đó, ngay cả trình biên dịch cũng biết rằng các biến đó thuộc về một khái niệm lớn hơn.

Hãy xem xét các phương thức trong Listing 2-1. Các biến có cần một ngữ cảnh có ý nghĩa hơn không? Tên hàm chỉ cung cấp một phần của ngữ cảnh, thuật toán cung cấp phần còn lại. Khi bạn đọc qua hàm, bạn thấy rằng ba biến, numberverb và pluralModifier, là một phần của thông báo “giả định thống kê”. Thật không may, bối cảnh này phải suy ra mới có được. Khi bạn lần đầu xem xét phương thức, ý nghĩa của các biến là không rõ ràng.

Listing 2-1 Biến với bối cảnh không rõ ràng.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

private void printGuessStatistics(char candidate, int count) {

    String number;

    String verb;

    String pluralModifier;

    if (count == 0) {

        number = "no";

        verb = "are";

        pluralModifier = "s";

    } else if (count == 1) {

        number = "1";

        verb = "is";

        pluralModifier = "";

    } else {

        number = Integer.toString(count);

        verb = "are";

        pluralModifier = "s";

    }

    String guessMessage = String.format("There %s %s %s%s", verb, number, candidate, pluralModifier);

    print(guessMessage);

}

Hàm này hơi dài và các biến được sử dụng xuyên suốt. Để tách hàm thành các phần nhỏ hơn, chúng ta cần tạo một lớp GuessStatisticsMessage và tạo ra ba biến của lớp này. Điều này cung cấp một bối cảnh rõ ràng cho ba biến. Chúng là một phần của GuessStatisticsMessage. Việc cải thiện bối cảnh cũng cho phép thuật toán được rõ ràng hơn bằng cách chia nhỏ nó thành nhiều chức năng nhỏ hơn. (Xem Listing 2-2.)

Listing 2-2 Biến có ngữ cảnh

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public class GuessStatisticsMessage {

    private String number;

    private String verb;

    private String pluralModifier;

    public String make(char candidate, int count) {

        createPluralDependentMessageParts(count);

        return String.format("There %s %s %s%s",verb, number, candidate, pluralModifier );

    }

    private void createPluralDependentMessageParts(int count) {

        if (count == 0) {

            thereAreNoLetters();

        } else if (count == 1) {

            thereIsOneLetter();

        } else {

            thereAreManyLetters(count);

        }

    }  

    private void thereAreManyLetters(int count) {

        number = Integer.toString(count);

        verb = "are";

        pluralModifier = "s";

    }

    private void thereIsOneLetter() {

        number = "1";

        verb = "is";

        pluralModifier = "";

    }

    private void thereAreNoLetters() {

        number = "no";

        verb = "are";

        pluralModifier = "s";

    }

}

Tên ngắn thường tốt hơn tên dài, miễn là chúng rõ ràng. Thêm đủ ngữ cảnh cho tên sẽ tốt hơn khi cần thiết.

Tên accountAddress và customerAddress là những tên đẹp cho trường hợp đặc biệt của lớp Address nhưng có thể là tên tồi cho các lớp khác. Address là một tên đẹp cho lớp. Nếu tôi cần phân biệt giữa địa chỉ MAC, địa chỉ cổng (port) và địa chỉ web thì tôi có thể xem xét MAC, PostalAddress và URL. Kết quả là tên chính xác hơn. Đó là tâm điểm của việc đặt tên.

4. Comments thế nào cho chuẩn?

Đừng dùng comment để làm màu cho code

Một trong những động lực to lớn để viết comment là do code tồi, code tối nghĩa. Chúng ta viết một hàm và chúng ta biết nó khó hiểu, nó vô tổ chức, chúng ta biết nó là một đống hỗ lốn. Vậy nên chúng ta tự nhủ rằng: “Ồ, tốt hơn nên viết comment ở đây!”. Không! Tốt hơn bạn nên viết lại code!

Code sáng nghĩa và rõ ràng với ít comment sẽ tuyệt vời hơn so với code tối nghĩa, phức tạp với nhiều comment. Thay vì dành thời gian để viết comment giải thích mớ code hỗ lốn đó, hãy dành thời gian để dọn dẹp code.

Giải thích ý nghĩa ngay trong code

Chắc chắn có những lúc code của bạn rất khó để giải thích nó đang làm gì. Thật không may, điều này làm cho nhiều lập trình viên cho rằng code hiếm khi là một cách tốt để giải thích. Điều đó hoàn toàn sai. Bạn muốn nhìn thấy điều gì? Cái này:

1

2

// Check to see if the employee is eligible for full benefits

if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

Hay cái này?

1

if (employee.isEligibleForFullBenefits())

Chỉ mất vài giây suy nghĩ để truyền hết thông điệp của bạn vào code. Trong nhiều trường hợp, nó chỉ đơn giản là tạo ra một hàm có tên giống với comment mà bạn muốn viết

Comment tốt

Một số comment là cần thiết hoặc có ích. Chúng ta sẽ xem xet một vài trường hợp mà tôi cho là xứng đáng để bạn bỏ công ra viết. Tuy nhiên, hãy nhớ rằng comment thật sự tốt là comment không cần phải viết ra.

COMMENT PHÁP LÝ

Đôi khi các tiêu chuẩn mã hóa doanh nghiệp buộc chúng ta phải viết một số comment nhất định vì lý do pháp lý. Ví dụ, tuyên bố bản quyền và quyền tác giả là những điều cần thiết và hợp lý để đưa vào comment khi bắt đầu mỗi source code file.

1

2

3

4

5

/* Copyright (C) 2003,2004,2005 by Object Mentor, Inc.

 * All rights reserved.

 * Released under the terms of the GNU General Public License version

 * 2 or later.

 */

Đừng đưa cả hợp đồng hay những điều luật vào comment. Nếu có thể, hãy tham khảo một vài giấy phép tiêu chuẩn hay tài liệu bên ngoài khác thay vì đưa tất cả những điều khoản và điều kiện vào.

COMMENT CUNG CẤP THÔNG TIN

Cung cấp thông tin cơ bản với một vài dòng comment đôi khi rất hữu ích. Ví dụ, hãy xem cách mà comment này giải thích về giá trị trả về của một phương thức trừu tượng:

1

2

// Returns an instance of the Responder being tested.

protected abstract Responder responderInstance();

Một comment như thế này, đôi khi, có thể là hữu ích. Nhưng tốt hơn là sử dụng tên của hàm để truyền đạt thông tin nếu có thể. Ví dụ, trong trường hợp này, chúng ta có thể dọn dẹp comment trên bằng cách đặt lại tên hàm thành responderBeingTested.

Trường hợp dưới đây có vẻ tốt hơn một chút:

1

2

3

// format matched kk:mm:ss EEE, MMM dd, yyyy

Pattern timeMatcher = Pattern.compile(

  "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

Trong trường hợp này, comment cho chúng ta biết rằng biểu thức được tạo ra khớp với thời gian và ngày tháng, và được định dạng bằng hàm SimpleDateFormat.format. Tuy nhiên, nó có thể rõ ràng hơn nếu code này được chuyển sang một lớp đặc biệt chuyển đổi định dạng của ngày và thời gian. Và sau đó, bạn có thể đưa comment trên vào sọt rác.

GIẢI THÍCH MỤC ĐÍCH

Đôi khi comment không chỉ cung cấp thông tin về những dòng code mà còn cung cấp ý định đằng sau nó. Trong trường hợp sau đây, chúng tôi thấy một comment ghi lại một quyết định thú vị: khi so sánh hai đối tượng, tác giả quyết định rằng anh ta muốn sắp xếp các đối tượng của lớp mình luôn lớn hơn các đối tượng khác.

1

2

3

4

5

6

7

8

9

10

11

public int compareTo(Object o)

{

    if(o instanceof WikiPagePath)

    {

        WikiPagePath p = (WikiPagePath) o;

        String compressedName = StringUtil.join(names, "");

        String compressedArgumentName = StringUtil.join(p.names, "");

        return compressedName.compareTo(compressedArgumentName);

    }

    return 1; // we are greater because we are the right type.

}

Dưới đây là một ví dụ tốt hơn. Có thể bạn không đồng tình với cách giải quyết vấn đề của tác giả, nhưng ít ra bạn biết được anh ấy đang cố gắng làm gì

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public void testConcurrentAddWidgets() throws Exception {

    WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});

    String text = "'''bold text'''";

    ParentWidget parent =

    new BoldWidget(new MockWidgetRoot(), "'''bold text'''");

    AtomicBoolean failFlag = new AtomicBoolean();

    failFlag.set(false);

    //This is our best attempt to get a race condition

    //by creating large number of threads.

    for (int i = 0; i < 25000; i++) {

        WidgetBuilderThread widgetBuilderThread =

        new WidgetBuilderThread(widgetBuilder, text, parent,

                                failFlag);

        Thread thread = new Thread(widgetBuilderThread);

        thread.start();

    }

    assertEquals(false, failFlag.get());

}

Race condition là gì?

Hoặc link tiếng Việt

LÀM DỄ HIỂU

Đôi khi bạn cần dùng comment để diễn giải ý nghĩa của các đối số khó hiểu hoặc giá trị trả về, để biến chúng thành thứ gì đó có thể hiểu được. Nhưng tốt nhất vẫn là tìm cách làm cho các đối số hoặc giá trị trả về đó trở nên rõ ràng theo cách của bạn. Nhưng khi nó là một phần của thư viện, hoặc thuộc về một phần code mà bạn không có quyền tùy chỉnh, thì một comment giải thích dễ hiểu có thể có ích trong trường hợp này.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public void testCompareTo() throws Exception

{

    WikiPagePath a = PathParser.parse("PageA");

    WikiPagePath ab = PathParser.parse("PageA.PageB");

    WikiPagePath b = PathParser.parse("PageB");

    WikiPagePath aa = PathParser.parse("PageA.PageA");

    WikiPagePath bb = PathParser.parse("PageB.PageB");

    WikiPagePath ba = PathParser.parse("PageB.PageA");

 

    assertTrue(a.compareTo(a) == 0); // a == a

    assertTrue(a.compareTo(b) != 0); // a != b

    assertTrue(ab.compareTo(ab) == 0); // ab == ab

    assertTrue(a.compareTo(b) == -1); // a < b

    assertTrue(aa.compareTo(ab) == -1); // aa < ab

    assertTrue(ba.compareTo(bb) == -1); // ba < bb

    assertTrue(b.compareTo(a) == 1); // b > a

    assertTrue(ab.compareTo(aa) == 1); // ab > aa

    assertTrue(bb.compareTo(ba) == 1); // bb > ba

}

Dĩ nhiên, khả năng các comment dạng này cung cấp thông tin không chính xác là khá cao. Xem lại các ví dụ trước để thấy rằng việc xác nhận thông tin từ comment khó thế nào. Điều này giải thích lý do tại sao làm cho comment dễ hiểu là cần thiết, mặc dù điều đó đồng nghĩa với sự mạo hiểm. Vậy nên trước khi bạn viết những comment như thế này, hãy chắc chắn rằng không còn cách nào tối ưu hơn, vì sau đó bạn cần quan tâm đến độ chính xác của chúng mỗi khi bạn chỉnh sửa lại code.

CÁC CẢNH BÁO VỀ HẬU QUẢ

Đôi khi nó rất hữu ích để cảnh báo các lập trình viên khác về hậu quả xảy ra. Ví dụ, đây là một comment giải thích tại sao test case này lại bị tắt:

1

2

3

4

5

6

7

8

9

10

// Don't run unless you

// have some time to kill – Đừng chạy hàm này, trừ khi mày quá rảnh

public void _testWithReallyBigFile() {

    writeLinesToFile(10000000);

    response.setBody(testFile);

    response.readyToSend(this);

    String responseString = output.toString();

    assertSubString("Content-Length: 1000000000", responseString);

    assertTrue(bytesSent > 1000000000);

}

Tất nhiên là ngày nay, chúng tôi tắt các test case bằng cách sử dụng thuộc tính @Ignore với một chuỗi giải thích thích hợp: @Ignore (“It takes too long to run”). Nhưng trước khi JUnit 4 xuất hiện, việc đặt một dấu gạch dưới vào trước tên hàm là một quy tắc rất phổ biến. Các comment đồng thời được xem như là cách đánh dấu để lập trình viên chú ý đến cảnh báo của hàm hơn.

Đây là một ví dụ đau khổ khác:

1

2

3

4

5

6

7

8

public static SimpleDateFormat makeStandardHttpDateFormat()

{

    //SimpleDateFormat is not thread safe,

    //so we need to create each instance independently.

    SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");

    df.setTimeZone(TimeZone.getTimeZone("GMT"));

    return df;

}

Có thể còn nhiều cách tốt hơn. Tôi đồng ý. Nhưng comment trong trường hợp này là hoàn toàn hợp lý, nó sẽ ngăn được một số lập trình viên ham hố sử dụng một phương thức khởi tạo tĩnh.

 

 

 

Từ khóa: