Hãy để code lên tiếng!

Đây là bài dịch, bài gốc mọi người có thể xem ở đây : https://hackernoon.com/let-the-code-speak-52d1cebf0394

Bạn đã bao giờ thấy những dòng code như thế này chưa?

public String getProductNames(List<Product> products) {
    StringBuilder strBuf = new StringBuilder();
    int i = 0;
    strBuf.append(products.get(0).name);
    while (i < products.size()) {
        strBuf.append(", ");
        strBuf.append(products.get(i++).name);
    }
    return strBuf.toString();
}

Tôi cá là chúng ta ít nhất đã có một lần nhìn thấy những dòng code tương tự thế này (nếu không thì bạn sẽ sớm gặp trong nghiệp code của mình thôi). Đoạn code như trên thường tồn tại trong những hệ thống được kế thừa từ xưa (legacy system) và thường có nhiều năm tuổi. Và ắt hẳn là bạn sẽ cảm thấy không thoải mái với những dòng code như vậy.

Vấn đề với những dòng code này, không chỉ ở việc nó rất là dài dòng, mà nguy hiểm hơn, nó còn che mất phần business logic (một số vấn đề khác sẽ được đề cập dần dần trong bài viết này). Trong những hệ thống phần mềm enterprise, chúng ta viết code để giải quyết vấn đề. Vì thế, chúng ta không nên tạo thêm vấn đề từ những dòng code mình viết ra. Lưu ý rằng với các phần như là system code hoặc các thư viện (libraries), thì mục tiêu hướng đến là hiệu năng cao hoặc các vấn đề chúng ta cần xử lý là cực kì phức tạp về mặt kỹ thuật, thì chúng ta có thể được phép tạo ra các đoạn code không dễ hiểu cho lắm, nhưng kể cả như thế, thì chúng ta cũng nên tránh việc viết các đoạn code tối nghĩa, giấu đi phần business logic.

Như Robert C. Martin (Uncle Bob) đã nói trong cuốn sách của mình Clean Code: A Handbook of Agile Software Craftsmanship rằng : Tỉ lệ giữa thời gian dành cho việc đọc code so với việc viết code thường là từ 10:1 trở lên. Khi làm việc với một số hệ thống cũ, tôi thấy mình đã mất rất nhiều thời gian chỉ để tìm ra cách đọc code chứ không phải là thực sự đọc hiểu code. Việc test và debug với các hệ thống này cũng rất là rắc rối. Và để đối phó với các hệ thống như này, chúng ta sẽ thường dùng đến một số thủ đoạn bất thường.

Mọi thứ chúng ta viết đều kể một câu chuyện

Code cũng không là ngoại lệ. Code không nên ẩn giấu phần business logic cũng như giải thuật chúng ta sử dụng để giải quyết vấn đề. Thay vào đó, chúng nên chỉ ra những nội dung này một các rõ ràng. Các tên biết mà chúng ta sử dụng, độ dài của hàm, hoặc thậm chí là format của code nên được dùng để mô tả vấn đề mà chúng ta đã giải quyết với sự tận tâm và chuyên nghiệp.

Bạn thấy thế nào về đoạn code dưới đây?

int calc(int arr[])
{
    int i;
    int val = 0;
    for ( i = 0;i<arr.length;i=i +1 )
      val = val+1;
        

    int j = 0;
    int sum = 0;
    while (arr.length>j) {sum += arr[j++] ;}
    int ret = sum - val;
    return ret;

}

Với tôi, nó trông như một bãi chiến trường vậy. Có vẻ như là mọi lập trình viên từng làm việc với đoạn code này cực kì ghét nó, và đã cố thoát ra khỏi vũng lầy này bằng mọi giá, và để lại nó ở một trạng thái tồi tệ hơn. Format code không thống nhất, đi kèm với tên biến nghèo nàn đã chứng tỏ rằng có nhiều hơn 1 lập trình viên đã thay đổi đoạn code này. Nghe có vẻ hơi giống lý thuyết cửa sổ vỡ, nhỉ? Và không hề dễ dàng cho chúng ta để nói rằng đoạn code này làm gì (không tính đến chuyện mắt bạn đã nhức nhối như thế nào khi đọc chúng). Thực ra là đoạn code này có nhiệm vụ tính tổng các phần tử trong một mảnh và trừ đi số lượng phần tử trong mảng đó. Chúng ta sẽ thử làm nó ở một cách thuận tiện hơn:

int sumMinusCount(int arr[]) {
    int sum = IntStream.of(arr).sum();
    int count = arr.length;

    return sum - count;
}

Giờ, chúng ta sẽ sử dụng một khái niệm trong Java 8 là stream để làm cho code trở nên dễ đọc và súc tích hơn.

Clean Code!

Clean Code không có nghĩa là làm code trở nên đẹp đẽ, mà có nghĩa là làm code trở nên dễ bảo trì hơn. Khi code trở nên rối rắm, thời gian đọc hiểu code sẽ tăng lên đáng kể. Từ đó dẫn tới hiệu suất của lập trình viên giảm đi đáng kể. Một hậu quả khác của code rối rắm là lập trình viên sẽ làm cho code càn trở nên rối rắm hơn so với trước khi sửa. Lý do cho việc này không phải vì sự thiếu hụt khả năng dọn sạch code, mà thông thường, là do thiếu thời gian dưới áp lực của deadline. Khi chúng ta làm việc với code rối rắm, sẽ rất khó để chúng ta estimate bao lâu thì fix xong một bug hoặc làm xong một chức năng mới, bởi kiến trúc và design của của hệ thống bị ẩn đi bởi code. Vì thế, chúng ta sẽ thoả hiệp bằng cách sử dụng những cách chỉnh sửa vá chằng vá đụp để cho xong việc, và làm tăng lượng technical debt. Trong khi đó, clean code, sẽ giúp chúng ta hiểu được mục đích của tác giả một cách rõ ràng, và vì thế nếu có bug xải ra thì việc tìm và sửa lại cũng hết sức dễ dàng. Clean code sẽ giúp chúng ta đi nhanh hơn trong thời gian dài. Có 2 quyển sách mà tôi cực kì recommend cho các bạn, đó là Clean Code: A Handbook of Agile Software Craftsmanship của Robert C. MartinRefactoring: Improving the Design of Existing Code của Martin FowlerKent Beck.

Một giải pháp cho vấn đề bảo trì các đoạn code rối rắm có thể là dành ra một vài tháng (thậm chí hơn) chỉ để refactor và làm sạch code, nhưng cơ hội để có thế làm được việc này thường rất hiếm, khi mà bên kinh doanh hầu như sẽ không chấp nhận việc dừng phát triển hệ thống cho lập trình viên refactor code. Vậy chúng ta có thể làm gì đây?

Luật Chàng Trai Trinh Sát

Ý tưởng của Luật Chàng Trai Trinh Sát - được đề xuất bởi Uncle Bob, khá là đơn giản: Hãy để code sạch hơn so với lúc bạn tìm thấy chúng. Mỗi khi bạn tìm thấy các đoạn code cũ, bạn nên dọn dẹp nó một cách đúng đắn. Đừng áp dụng những giải pháp đường tắt khiến cho code trở nên khó đọc hơn, mà hãy cẩn thận điều trị code. Luật này tập trung vào việc yêu cầu lập trình viên bỏ nhiều tinh lực hơn để họ có thể làm cho cuộc sống về lâu dài trở nên dễ dàng hơn, bằng cách khiến hệ thống trở nên dễ dàng hơn cho việc bảo trì.

Tôi phải thành thật với các bạn rằng làm việc với các hệ thống cũ thì nói dễ hơn làm trong hầu hết các trường hợp, đặc biệt là khi chúng ta không có bất kì code test nào hoặc code test không được bảo trì thường xuyên, nhưng về mặt tinh thần, chúng ta nên luôn hướng đến việc tìm các cơ hội để làm cho code trở nên sạch sẽ hơn. Có rất nhiều kĩ thuật mà chúng ta có thể áp dụng khi làm việc với các hệ thống này (một cuốn sách khá tuyệt vời mà các bạn có thể tham khảo là Working Effectively with Legacy Code bởi Michael Feathers), nhưng trong bài post này, tôi sẽ tập trung đến một vài lời khuyên chung mà tôi thấy khá là hữu ích trong việc viết code dễ hiểu.

Nghĩ trước khi viết

Có một sự nhầm lẫn về việc phát triển phần mềm khi chúng ta cho rằng lập trình viên (chỉ) viết code. Chúng ta không làm vậy. Thay vào đó, chúng ta giải quyết vấn đề bằng code. Code là phương tiện, không phải là giải pháp thực sự. Liệu việc bấm nút lung tung có được coi là viết code? Đương nhiên là không bởi chúng sẽ hầu như là bất khả thi cho máy tính để có thể biên dịch được cái đống vừa gõ là gì. Điều tương tự cũng áp dụng cho việc viết code mà không suy nghĩ về vấn đề cần phải giải quyết. Do đó, khi viết code, chúng ta cần phải cẩn thận để giải pháp mà chúng ta tạo ra là rõ ràng, không mù mờ khó hiểu. Chúng ta không nên viết code vì chúng ta phải viết code. Code chúng ta viết ra nên giải quyết vấn đề thay vì tạo thêm những vấn đề mới.

Bạn đã bao giờ được yêu cầu review code, mà chỉ để nhận ra rằng đoạn code đó hoàn toàn sai và giải pháp duy nhất nên là viết lại nó từ đầu? Tôi đã từng thấy rất nhiều lập trình viên khi được giao task, họ nhảy ngay vào IDE và bắt đầu gõ gõ. Họ nghĩ rằng khi họ làm thế là chứng tỏ được rằng họ đang làm việc. Hầu hết các trường hợp như thế này đã được kiểm nghiệm là việc tiếp cận sai cách bằng cách code mà không nghĩ đã dẫn đến hướng đi sai. Tất nhiên, một số lập trình viên kinh nghiệm có thể bắt đầu viết code ngay lập tức mà vẫn đi đúng hướng, nhưng điều đó vẫn yêu cầu một vài dự thảo cẩn thận trước khi trực tiếp gõ code.

Hãy xem một ví dụ sau:

class Customer {
    private List<Double> drinks;
    private BillingStrategy strategy;

    public Customer(BillingStrategy strategy) {
        this.drinks = new ArrayList<Double>();
        this.strategy = strategy;
    }

    public void add(final double price, final int quantity) {
        drinks.add(strategy.getActPrice(price*quantity));
    }

    // Payment of bill
    public void printBill() {
        double sum = 0;
        for (Double i : drinks) {
            sum += i;
        }
        System.out.println("Total due: " + sum);
        drinks.clear();
    }
}

interface BillingStrategy {
    double getActPrice(final double rawPrice);
}

// Normal billing strategy (unchanged price)
class NormalStrategy implements BillingStrategy {

    @Override
    public double getActPrice(final double rawPrice) {
        return rawPrice;
    }

}

Dường như không có gì sai với đoạn code ở trên đúng không? Thực tế vẫn có đó! Ở đây Strategy Pattern được sử dụng với mục đích để đoạn code trở nên linh hoạt hơn. Trong ví dụ này, khác với ví dụ nguyên gốc từ Wikipedia, chúng ta chỉ có một implementation của Strategy và không có bất kì kế hoạch ngắn hạn nào cho việc implement thêm Strategy khác. Do đó, mục đích sử dụng Strategy Pattern ở đây có thể gây hiểu nhầm cho người đọc. Đồng thời, việc áp dụng Strategy Pattern sẽ cần thêm một chút công sức, vì thế mà người đọc có thể thắc mắc một cách hết sức tự nhiên rằng, lý do để có cái Strategy Pattern này ở đây là gì? Nguyên tắc Y.A.G.N.I đại diện cho You aren't gonna need it (bạn sẽ không cần đến nó đâu) được đưa ra nhằm giúp chúng ta không làm những việc không cần thiết. Sẽ rất khó để chúng ta biết được chúng ta cần gì trong tương lai. Một số trường hợp kinh nghiệm có thể giúp chúng ta, nhưng trong hầu hết các trường hợp, thì nên giữ mọi thứ ở mức đơn giản.

Design Pattern giúp chúng ta giải quyết các vấn đề cụ thể bằng phương pháp sẵn có theo cách gọn gàng và dễ hiểu nhất khi đã nắm được ý nghĩa của chúng. Tuy nhiên nếu vấn đề không tồn tại (như ở ví dụ trên, chúng ta không cần đến việc mở rộng), thì người đọc code sẽ trở nên lạc lối và nghĩ rằng vấn đề thực sự tồn tại. Lưu ý rằng tôi không nói là phản đối các pattern. Tôi thậm chí còn yêu chúng! Nhưng vấn đề ở đây là khi người ta lạm dụng chúng và tạo ra các vấn đề để có thể áp dụng các pattern, chỉ bởi vì họ biết các pattern đó.

Vấn đề tương tự cũng xảy ra khi chúng ta thử trộn lẫn các giải pháp, các requirement về business, các pattern, vào với nhau. Lúc này tôi có thể đảm bảo với các bạn rằng, sẽ rất dễ dàng để có thể nhìn ra cách giải quyết vấn đề theo những cách không sạch sẽ cho lắm. Chỉ sau này tôi mới nhận ra được pattern nào và abstraction nào có thể khiến cho code trở nên linh hoạt và dễ đọc hơn. Luật của tôi trong các trường hợp này, cho dù có theo TDD hay không, là trước hết làm nó hoạt động và sau đó làm sạch nó (trong TDD, thì việc này là đương nhiên bởi 3 luật của TDD).

Nhớ rằng! Làm cho code hoạt động không có nghĩa là chúng ta đã xong việc! Thực tế là khi chúng ta mới đi được một nửa quãng đường mà thôi. Chúng ta cần tiếp tục làm việc để khiến code của mình giao tiếp truyền tải được mục đích của nó đến người đọc.

Chúng ta cũng có rất nhiều công cụ hỗ trợ việc này, và trách nhiện của chúng ta là sử dụng đúng công cụ vào thời điểm thích hợp. Việc sử dụng libraries và framework chỉ bởi mọi người đều dùng không có ý nghĩa gì cả. Chúng ta cần tìm hiểu vấn đề mà những libraries và thư viện đó giải quyết là gì, và sử dụng chúng mà không khiến các business logic bị ẩn đi. Đây là một bài viết tuyệt vời để tham khảo về cách làm việc với libraries và frameworks: Make the Magic go away bởi Uncle Bob.

Phấn đấu để diễn tả!

Ngày nay, rất nhiều ngôn ngữ lập trình hỗ trợ stream, như là Java, Kotlin, Javascript, vv.. để chúng ta viết những đoạn code dễ hiểu. Stream đã thay thế việc sử dụng những vòng lặp tối nghĩa với những câu lệnh điều kiện if. Stream giúp cho chúng ta nghĩ về việc biến đổi data theo cách tường thuật (declarative) hơn là mệnh lệnh (imperative). Việc duyệt qua một collection để tìm các phần tử nhỏ hơn một giá trị nào đó sẽ trở nên vô ích, so với cách đơn giản là áp dụng một filter cho stream.

Map, filter và reduce là những hàm được cung cấp bởi hầu hết các ngôn ngữ lập trình có hỗ trợ stream. Vì thế, mọi người có thể hiểu được những gì mà bạn viết khi sử dụng những hàm trên giống như cách mọi người hiểu về vòng lặp và các câu lệnh điều kiện. Một bài post tuyệt vời cho chủ đề này có thể tìm thấy ở: Collection Pipeline bởi Martin Fowler.

Những công cụ giúp cho việc diễn đạt quá trình xử lý dữ liệu ở trên sẽ cho chúng ta thêm rất nhiều sức mạnh. Đầu tiên, bạn không phải viết test cho những hàm này. Bạn có để ý thấy lỗi Off-by-one ở ví dụ đầu tiên chứ? Đồng thời, các hàm này cũng giúp chúng ta tiến đến cách tiếp cận Functional Programming trong việc viết phần mềm. Functional Programming có rất nhiều lợi ít phù hợp với mục đích của bài post này (Nếu bạn có hứng thú với chúng, tôi xin đề cử bài post tuyệt vời này: Practical Functional Programming và đương nhiên là cả một quyển sách cũng hay không kém: Structure and Interpretation of Computer Programs của Harold Abelson, Gerald Jay SussmanJulie Sussman), nhưng ở đây tôi sẽ tập trung vào việc nó giúp code của chúng ta dễ đọc như thế nào.

Dưới đây là một giải pháp sử dụng stream cho ví dụ đầu tiên của bài post:

public String getProductNames(List<Product> products) {
    return products.stream()
            .map(p -> p.name)
            .collect(Collectors.joining(", "));
}

Đơn giản và sạch sẽ. Dễ hiểu xem mục đích của chúng là gì. Giờ hãy đi đến ví dụ tiếp theo:

void getPositiveNumbers(List<Integer> l1, List<Integer> l2) {
    for (Integer el1: l1)
        if (el1 > 0)
            l2.add(el1);
}

Bạn có thể thấy là tham số thứ 2 sẽ bị thay đổi khi bạn gọi hàm này không? Đoạn code này có làm những gì nó nói ko? Tên hàm có phù hợp không? Bạn có hiểu được mục đích của đoạn code này không?

Giờ với đoạn code này:

List<Integer> getPositiveNumbers(List<Integer> numbers) {
    return numbers.stream()
        .filter(num -> num > 0)
        .collect(toList());
}

Đến đây, kết quả trả về là một đối tượng List mới. Không tham số nào bị ảnh hưởng. Chúng ta chỉ đọc dữ liệu từ tham số và tạo ra một kết quả mới. Hàm này khiến chúng ta dễ hiểu hơn rất nhiều về mục đích nó được tạo ra và cách sử dụng nó. Đồng thời hàm này cũng có thể kết hợp với nhiều hàm khác. Composition là một trong những lợi ích quan trọng nhất của stream nói riêng và functional programming nói chung. Composition cho phép chúng ta nghĩ về các khái niệm trong việc biến đổi dữ liệu, lọc, ... ở một tầng cao hơn và có thể viết code theo cách tường thuật và diễn tả được nhiều hơn so với việc code theo mệnh lệnh. Code chúng ta tạo ra, khi đó sẽ diễn tả cái gì chúng ta muốn làm chứ không phải là cách chúng ta làm như thế nào! Và đó là một bước tiến đáng kể cho việc tạo ra code dễ hiểu.

Để ý rằng hàm toList() trong Java 8 trả về một list có thể biến đổi được, trong khi với functional programming thì lại thường sử dụng các kiểu cấu trúc dữ liệu không biến đổi được. Và thực tế là việc chúng ta tạo ra kết quả mới và coi các tham số là chỉ đọc sẽ nâng cao rất nhiều khả năng đọc hiểu cũng như hành vi của hàm. Mặc dù một số hàm có thể cần có side effect, nhưng điều quan trọng đối với một hàm là nó chỉ nên có có side effect (như một câu lệnh) hoặc chỉ trả về dữ liệu (như một truy vấn) chứ không nên có cả hai. Chi tiết hơn về vấn đề này có thể tìm thấy ở bài post này

Nhưng việc viết code diễn đạt dễ hiểu không phải là một chuyện dễ dàng. Từ một câu nói nổi tiếng cừ Albert Einstein: Nếu bạn không thể giải thích nó một cách đơn giản, thì bạn chưa hiểu nó đủ sâu, đã khiến tôi mỗi khi nhìn thấy code mà các lớp trừu tượng được trộn lẫn vào với nhau như là giao diện tương tác với phần DAO, hoặc thao tác trực tiếp với DB, hoặc các tầng chi tiết thấp hơn được gọi ra một cách cần thiết, tôi có thể nói rằng các đoạn code đó không những vi phạm nguyên tắc Single Responsibility trong S.O.L.I.D, mà còn bối rối trong việc nhận thức vấn đề cần giải quyết. Sử dụng comment trong trường trường hợp này không phải là giải pháp, mà tôi sẽ nói kĩ hơn ở bài post tới. Thay vào đó, tôi tin rằng code mà được việt ra càng đơn giản và dễ hiểu, thì càng chứng tỏ lập trình viên càng hiểu rõ vấn đề.

Cổ vũ tính bất biến

Sẽ rất là bối rối khi trạng thái của một đối tượng được thay đổi mà không có bất kì chú ý nào. Nó đồng thời cũng rất nguy hiểm khi sử dụng một đối tượng có thể được tạo dựng một cách nửa vời, đặc biệt khi chúng ta phải làm việc với các phần mềm có nhiều luồng. Chia sẻ các đối tượng này sẽ trở nên rất khó khăn. Nói một cách khác, sử dụng các đối tượng bất biến sẽ giúp cho phần mềm của chúng ta xử lý đa luồng dễ dàng hơn, đồng thời cũng là một ứng cử viên sáng giá cho việc caching, bởi state của chúng sẽ không thay đổi.

Nhưng vì sao mọi người chọn các đối tượng có thể thay đổi được? Tôi tin rằng, lý do phổ biến nhất là mọi người nghĩ rằng họ sẽ có hiệu năng cao hơn bởi vì bộ nhớ sẽ được sử dụng ít hơn, vì các thay đổi đều diễn ra ở cùng một chỗ. Hơn nữa, sẽ rất là tjw nhiên khi trạng thái của đối tượng được thay đổi trong vòng đời của chúng. Đó là điều mà chúng ta đã được học ở OOP. Và trong từng ấy năng, chúng ta đã viết các phần mềm mà hầu hết các đối tượng đều có thể thay đổi.

Ngày nay, dung lượng bộ nhớ mà một hệ thống có thể có đã lớn hơn rất nhiều so với vài thập kỉ trước. Vấn đề thực sự chúng ta đang đối mặt là khả năng mở rộng - scalability. Tốc độ của bộ xử lý không còn có thể nâng cấp với tỉ lệ đã từng xảy ra ở những năm trước, nhưng chúng ta có thể có những hệ thống với nhiều con chíp và hàng tá nhân xử lý. Vì thế, để phần mềm của có thể mở rộng được, chúng ta cần tận dụng lợi thế của tình hình hiện tại. Bởi phần mềm chúng ta viết ra sẽ chạy được ở trên nhiều nhân, chúng ta cần phải viết chúng theo cách để chúng có thể chạy an toàn nhất. Nếu chúng ta sử dụng các đối tượng có thể thay đổi, chúng ta sẽ phải đối mặt với việc locking để đảm bảo tính thống nhất của state. Concurrency không phải là vấn đề khó để giải quyết, nếu cần các bạn có thể tham khảo vấn đề này chi tiết hơn trong quyển Java Concurrency in Practice của Brian Goetz. Nói cách khách, các đối tượng bất biến là hoàn toàn phù hợp cho việc chia sẽ giữa nhiều thread và bộ nhớ dựa trên những đặc tính của chính. Đồng thời, việc không yêu cầu đồng bộ sẽ cho chúng ta cơ hội để tạo ra các hệ thống có độ trễ thấp và đáp ứng được lưu lượng cao. Do đó, tính bất biến là lựa chọn an toàn hơn cho việc mở rộng.

Ngoài lợi ích cho việc mở rộng, tính bất biến sẽ giúp cho code của chúng ta trở nên sạch sẽ hơn rất nhiều. Trong ví dụ đầu tiên ở phần trước, có một tham số là collection được truyền vào và thay đổi sau khi hàm được thực thi. Nếu collection đó là bất biến, thì việc này là không được phép, và khiến chúng ta đến một giải pháp khác tốt hơn. Đồng thời, người đọc cũng không phải theo dõi, ghi nhớ quá trình state biến đổi trong đầu vì state ở đây là bất biến. Người đọc chỉ cần gán một cái tên cho giá trị cần theo dõi và không cần phải nhớ giá trị của biến.

Để tìm hiểu thêm về tính bất biến và các lời khuyên về lập trình, các bạn có thể tìm đọc quyển Effective Java (2nd Edition) của Joshua Bloch . Đồng thời bài talk sau cũng rất nên tham khảo: The Value of Values with Rich Hickey

Phần mềm phải được viết cho con người để đọc, và tình cờ thuận tiện cho máy tính để thực thi. - Harold Abelson, Structure and Interpretation of Computer Programs

Bài post vừa rồi đã tập trung vào những lời khuyên cơ bản nhất cho việc viết code dễ hiểu. Trong các bài post tới, chúng ta sẽ thảo luận về các code smells trong production code và test code. Chúng ta cũng tìm hiểu cách để tìm ra những vấn đề trong việc design chỉ bằng cách xem tests. Hãy tiếp tục theo dõi!

Đọc thêm

  1. Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin
  2. Refactoring: Improving the Design of Existing Code by Martin Fowler and Kent Beck
  3. Working Effectively with Legacy Code by Michael Feathers
  4. Structure and Interpretation of Computer Programs by Harold Abelson, Gerald Jay Sussman and Julie Sussman
  5. Java Concurrency in Practice by Brian Goetz
  6. Effective Java (2nd Edition) by Joshua Bloch
  7. Make the Magic go away
  8. Collection Pipeline
  9. Practical Functional Programming
  10. More shell, less eggs (McIlroy vs Knuth story)
  11. CommandQuerySeparation
  12. The Value of Values with Rich Hickey