Kiến trúc tiến hóa và thiết kế nổi dần: Tái cấu trúc mã nguồn hướng theo thiết kế

Tìm và thu thập thiết kế ẩn trong mã của bạn

Các bài viết trước đây của loạt bài viết này thảo luận về việc kiểm thử đơn vị giúp bạn có một thiết kế tốt hơn như thế nào. Nhưng nếu bạn đã có rất nhiều mã, thì làm thế nào bạn có thể khám phá các yếu tố thiết kế ẩn bên trong các mã đó? Bài viết trước đã bàn về xây dựng các đích cấu trúc cho mã của bạn. Trong bài viết này, tác giả Neal Ford của của loạt bài viết mở rộng các ý tưởng đó và nói về các kỹ thuật sử dụng tái cấu trúc mã nguồn để cho phép thiết kế nổi dần lên.

Neal Ford, Kiến trúc phần mềm, ThoughtWorks

Neal Ford là một kiến trúc sư phần mềm và Meme Wrangler tại Thought Works, một văn phòng tư vấn CNTT toàn cầu. Ông cũng thiết kế và phát triển các ứng dụng, tài liệu hướng dẫn, các bài báo trên tạp chí, học liệu và các bài thuyết trình video/DVD; và ông là tác giả hoặc người biên tập các cuốn sách bao trùm nhiều loại công nghệ, bao gồm cả cuốn sách gần đây nhất là The Productive Programmer. Ông tập trung vào việc thiết kế và xây dựng ứng dụng doanh nghiệp có quy mô lớn. Ông cũng là một diễn giả được quốc tế hoan nghênh tại hội nghị của các nhà phát triển trên toàn thế giới



25 12 2009

Trong hai bài viết "Thiết kế hướng kiểm thử, phần 1" và "Thiết kế hướng kiểm thử, phần 2," tôi đã nói về cách mà việc kiểm thử có thể dẫn đến thiết kế tốt hơn cho các dự án mới. Trong phần "Phương thức hợp thành và SLAP," (N.D: SLAP là viết tắt “single level of abstraction principle” - nguyên tắc chỉ một mức trừu tượng) tôi có nói về hai mẫu trọng yếu — phương thức hợp thành và nguyên tắc chỉ một mức trừu tượng — hai mẫu này mang lại cho bạn một cái đích tổng thể cho cấu trúc mã của bạn. Hãy ghi nhớ các mẫu này. Khi bạn có một dự án phần mềm đang tồn tại rồi, thì tuyến đường để phát hiện và thu thập các yếu tố thiết kế nằm trong việc cấu trúc lại mã nguồn. Trong cuốn sách kinh điển Tái cấu trúc mã nguồn, của mình, Martin Fowler đã định nghĩa tái cấu trúc mã nguồn "là một kỹ thuật có quy tắc để cấu trúc lại phần chính yếu hiện tại của mã, thay đổi cấu trúc bên trong của nó mà không thay đổi hành vi bên ngoài của nó" (xem phần Tài nguyên). Cấu trúc lại mã nguồn là một phép chuyển đổi cấu trúc có mục đích. Có một cơ sở mã dễ cấu trúc lại là một mục tiêu đáng khen ngợi của bất kỳ dự án nào. Trong bài viết này, tôi nói về cách sử dụng việc tái cấu trúc mã nguồn như thế nào để tìm ra một thiết kế chưa được sử dụng đúng mức còn ẩn giấu trong mã của bạn.

Về loạt bài viết này

Loạt bài viết này nhằm cung cấp một phối cảnh tươi mới về các khái niệm thường được thảo luận nhưng khó nắm bắt về kiến trúc và thiết kế phần mềm. Thông qua các ví dụ cụ thể, Neal Ford mang đến cho bạn một nền tảng vững chắc cho cách làm thực tế lanh lẹn của kiến trúc tiến hóa và thiết kế nổi dần. Bằng cách trì hoãn các quyết định quan trọng về thiết kế và kiến trúc cho đến thời điểm quyết định cuối cùng, bạn có thể ngăn ngừa được những phức tạp không cần thiết không để chúng ngầm phá hoại các dự án phần mềm của bạn

Các kiểm thử đơn vị là cái lưới an toàn chính cho phép bạn tuỳ ý cải tiến cơ sở mã của mình. Nếu bạn có mức bao quát kiểm thử là 100 phần trăm mã của dự án của mình, thì bạn có thể cấu trúc lại mã của mình mà không gặp rắc rối nào. Nếu bạn không theo đuổi mức kiểm thử đó, thì việc quá hăng hái cấu trúc lại mã nguồn sẽ nguy hiểm hơn. Các thay đổi được khoanh vùng rất dễ áp dụng và bạn có thể thấy tác dụng ngay lập tức của chúng, nhưng các rạn vỡ do tác dụng phụ lâu dài về sau này sẽ làm cho bạn điêu đứng. Phần mềm sẽ dẫn đến những điểm kết dính không mong muốn, và một thay đổi nhỏ đối với một phần của mã có thể lan truyền qua cơ sở mã, gây ra lỗi cho hàng trăm dòng mã từ việc thay đổi đó. Sự tự tin để sửa đổi mã và tìm ra những lỗi lan xa này là một dấu hiệu nổi bật của kiểm thử đơn vị bao quát mọi nơi. Một dự án kéo dài trong 2 năm của công ty tư vấn ThoughtWorks đã được người phụ trách kỹ thuật tiến hành 53 lần cấu trúc lại mã nguồn khác nhau cho đến tận ngày trước khi dự án đi vào hoạt động. Ông đã làm điều này với sự tự tin thanh thản vì dự án bao trùm toàn bộ mã.

Làm thế nào để đưa cơ sở mã của bạn tới chỗ có thể thực hiện được những đợt tái cấu trúc mã nguồn rộng lớn? Một lựa chọn là từ chối viết thêm mã khác cho đến khi bạn có thời gian để thêm các phép kiểm thử cho toàn bộ dự án. Ngay khi bạn đề xuất việc này thì bạn sẽ bị đuổi việc và bạn có thể đi làm việc cho một công ty coi trọng việc kiểm thử đơn vị hơn. Cách tiếp cận này có thể là không tối ưu. Lựa chọn tốt nhất tiếp theo của bạn là làm cho những những thành viên khác trong nhóm của bạn nhận thức được giá trị của kiểm thử và bắt đầu thêm dần dần các phép kiểm thử cho các phần trọng yếu nhất của mã của bạn. Bạn hãy vạch một đường thẳng trên cát và tuyên bố một ngày trong tương lai gần: "Bắt đầu từ thứ năm tới, mức bao quát kiểm thử của chúng ta sẽ luôn tăng lên." Mỗi khi bạn viết một mã mới, thì hãy thêm một phép kiểm thử, và mỗi khi bạn sửa một lỗi, thì bạn hãy viết một phép kiểm thử. Bằng cách dần dần thêm các phép kiểm thử cho các phần nhạy cảm nhất (các tính năng mới và các vùng bị lỗi), bạn thêm các phép thử vào đúng nơi chúng có ích nhất.

Các phép kiểm thử đơn vị kiểm tra hành vi nguyên tử. Tuy nhiên, nếu cơ sở mã của bạn không tuân theo mô hình lý tưởng của phương thức hợp thành thì điều gì sẽ xảy ra? Nói cách khác, điều gì sẽ xảy ra nếu tất cả các phương thức của bạn có hàng chục hoặc hàng trăm dòng mã, và mỗi phương thức thực hiện rất nhiều tác vụ? Bạn có thể sử dụng khung công tác kiểm thử đơn vị để viết các phép kiểm thử chức năng mức thô hơn cho các phương thức đó, bạn quan tâm chủ yếu đến việc biến đổi trạng thái của đầu vào và đầu ra của của phương thức. Việc này không tốt như các phép thử đơn vị vì chúng không kiểm tra từng mảnh nhỏ của hành vi, nhưng còn hơn là không làm gì. Đối với những phần thực sự trọng yếu của mã của bạn, bạn có thể xem xét việc thêm một số kiểm thử chức năng như một lưới an toàn trước khi bạn bắt đầu cấu trúc lại mã nguồn.

Các cơ chế của việc cải tiến mã nguồn rất đơn giản, và bây giờ tất cả các môi trường phát triển tích hợp (IDE) chính đều có sự hỗ trợ cấu trúc lại mã nguồn rất tuyệt vời. Điều khó khăn là ở chỗ tìm ra cái gì để cấu trúc lại. Phần còn lại của bài viết bàn về vấn đề này.

Gắn kết với cơ sở hạ tầng

Tất cả mọi người trong thế giới Java sử dụng khung công tác để khởi động việc phát triển và cung cấp cơ sở hạ tầng quan trọng thuộc loại tốt nhất (cơ sở hạ tầng mà bạn không cần phải viết). Nhưng có một mối nguy hiểm ẩn núp trong khung công tác, cả khung công tác mã nguồn thương mại lẫn khung công tác mã nguồn mở: chúng luôn luôn cố gắng làm cho bạn kết dính quá mật thiết với chúng, điều này có thể làm cho khó nhìn thấy thiết kế được ẩn trong mã của bạn.

Các khung công tác và máy chủ ứng dụng có các lớp trợ giúp lôi kéo bạn đi theo tuyến đường phát triển đơn giản hơn nhiều: nếu bạn chỉ nhập khẩu và sử dụng một số lớp của chúng, thì để hoàn thành một tác vụ cụ thể sẽ dễ dàng hơn nhiều. Một ví dụ kinh điển là Struts, khung công tác web mã nguồn mở vô cùng phổ biến. Khung công tác Strust bao gồm một bộ các lớp trợ giúp để xử lý các việc vặt phổ biến cho bạn. Ví dụ: Nếu bạn cho phép các lớp miền của bạn mở rộng từ lớp ActionForm của Struts thì khung công tác Struts sẽ tự động điền các trường trong biểu mẫu yêu cầu, xử lý việc xác thực và các sự kiện vòng đời, và thực hiện các hành vi có ích khác. Nói cách khác, khung công tác Struts mang đến một sự đánh đổi: hãy sử dụng các lớp của chúng tôi và công việc phát triển của bạn sẽ dễ dàng hơn nhiều. Khung công tác này khuyến khích bạn tạo ra một cấu trúc như được thể hiện trong hình 1:

Hình 1. Sử dụng lớp ActionForm của Struts
Lớp mô hình mở rộng ActionForm

Hộp màu vàng bao gồm các lớp miền của bạn, nhưng khung công tác Struts khuyến khích bạn mở rộng nó từ lớp ActionForm để kế thừa được các hành vi hữu ích của nó. Tuy nhiên, bây giờ bạn đã kết dính một cách vô vọng mã của mình vào khung công tác Struts. Bạn không còn có thể sử dụng lớp miền của bạn trong bất cứ cái gì khác, ngoài một ứng dụng Struts. Nó cũng làm tổn hại đến thiết kế của các lớp miền của bạn bởi vì lớp tiện ích này bây giờ phải nằm ở trên đỉnh của hệ thống phân cấp các đối tượng của bạn, không cho phép bạn sử dụng thừa kế để củng cố các hành vi chung.

Hình 2 cho thấy một cách tiếp cận tốt hơn:

Hình 2. Thiết kế được cải tiến, bằng các sử dụng phép hợp thành để tách rời khỏi khung công tác Struts
Sử dụng phép hợp thành để tách rời khỏi khung công tác Struts

Trong phiên bản này các lớp miền của bạn không phụ thuộc vào lớp ActionForm của Struts. Thay vào đó, một giao diện xác định ngữ nghĩa cho cả lớp miền của bạn và lớp ScheduleItemForm đóng vai trò như một cầu nối giữa miền của bạn và khung công tác. Cả hai lớp ScheduleItemImplScheduleItemForm thực hiện các giao diện, và lớp ScheduleItemForm nắm giữ một tham chiếu đến lớp miền của bạn thông qua hợp thành hơn là thừa kế. Được phép để cho lớp trợ giúp của Struts duy trì một phụ thuộc vào lớp của bạn, nhưng điều ngược lại là không được: bạn không nên để cho các lớp của bạn có sự phụ thuộc vào khung công tác. Bây giờ, bạn được tự do sử dụng lớp ScheduleItem của bạn trong các kiểu ứng dụng khác (Ứng dụng Swing, tầng dịch vụ, vv).

Kết dính với cơ sở hạ tầng rất dễ dàng và phổ biến mọi nơi trong nhiều ứng dụng. Khung công tác làm cho dễ dàng hơn nữa việc tận dụng các dịch vụ của chúng khi bạn nhập khẩu các món quà của chúng. Bạn nên cưỡng lại các cám dỗ. Mẫu đặc thù (được định nghĩa trong các bài viết trước là các mẫu nhỏ, có trong ứng dụng của bạn) khó phát hiện ra hơn trong mã của bạn nếu vỏ ngoài của khung công tác che phủ mọi thứ.


Các vi phạm đối với nguyên tắc DRY

Trong cuốn sách Lập trình viên thực dụng (The Pragmatic Programmer), các tác giả Andy Hunt và Dave Thomas đã định nghĩa nguyên tắc DRY : Don't Repeat Yourself (đừng lặp lại chính bản thân bạn) (xem phần Tài nguyên). Hai khía cạnh của sự vi phạm nguyên tắc DRY — sao chép mã lệnh và sao chép cấu trúc — có thể ảnh hưởng đến thiết kế.

Mã sao chép

Sao chép trong mã lệnh làm mờ thiết kế bởi vì bạn không thể tìm thấy các mẫu đặc thù. Mã sao chép có sự các khác biệt không dễ phát hiện ở nơi này nơi khác, ngăn cản không cho bạn xác định cách sử dụng thực sự của một phương thức hay một sưu tập các phương thức. Và, tất nhiên mọi người đều biết rằng viết mã nhờ sao chép cuối cùng sẽ luôn gây phiền toái cho bạn, bởi vì bạn chắc chắn phải thay đổi hành vi, và khó theo dõi tất cả các nơi mà bạn đã sao chép mã.

Làm thế nào để bạn tìm được các đoạn sao chép đã lẻn vào cơ sở mã của bạn? Các IDE hoặc bao gồm sẵn các trình phát hiện sao chép (ví dụ như IntelliJ) hoặc cung cấp chúng dưới dạng các trình cắm thêm (ví dụ như Eclipse). Cũng có các công cụ độc lập, cả mã nguồn mở (chẳng hạn như CPD - Copy/Paste Detector - công cụ phát hiện sao chép) lẫn thương mại (chẳng hạn như Simian) (xem phần Tài nguyên).

Dự án CPD là một phần của công cụ phân tích mã nguồn PMD. Đó là một ứng dụng dựa trên Swing, ứng dụng này phân tích một số lượng cấu hình được các thẻ bài (token) cả trong một tệp tin riêng lẻ lẫn trong nhiều tệp tin. Tôi cần một cơ sở mã không tầm thường làm nạn nhân ví dụ, vì vậy tôi chọn dự án Struts đã nói ở trên. Khi chạy CPD trên cơ sở mã Struts 2 cho kết quả như trong hình 3:

Hình 3. Kết quả chạy CPD trên cơ sở mã Struts 2
Kết quả chạy CPD trên cơ sở mã Struts 2

CPD tìm thấy nhiều sự trùng lặp trong cơ sở mã Struts. Phần nhiều các trùng lặp này liên quan đến việc bổ sung hỗ trợ portlet (cổng web con) cho Struts. Trong thực tế, hầu hết các phần sao chép giữa các tệp tin là thuộc về các tệp PortletXXXXXX (Ví dụ: PortletApplicationMapApplicationMap). Điều này cho thấy sự hỗ trợ portlet đã không được thiết kế tốt. Đây là một “mùi” chính toát ra từ mã lệnh mỗi khi có nhiều trùng lặp mã như vậy để bổ sung thêm hành vi vào một khung công tác hiện có. Một cách thức “sạch” hơn là thông qua thừa kế hoặc kết hợp để mở rộng khung công tác hiện có, và thậm chí đó là lời tố cáo tệ hơn, nếu cả sự thừa kế hoặc kết hợp đều không thực hiện được.

Một vấn đề trùng lặp phổ biến khác trong cơ sở mã này nằm trong các tệp tin ApplicationMap.java và Sorter.java. Tệp ApplicationMap.java chứa một đoạn 27 dòng mã bị trùng lặp, như trong lệt kê 1:

Liệt kê 1. Mã bị trùng lặp trong tệp tin ApplicationMap.java
entries.add(new Map.Entry() {
    public boolean equals(Object obj) {
        Map.Entry entry = (Map.Entry) obj;

        return ((key == null) ? 
            (entry.getKey() == null) : 
            key.equals(entry.getKey())) && ((value == null) ? 
                (entry.getValue() == null) : 
                value.equals(entry.getValue()));
    }

    public int hashCode() {
        return ((key == null) ? 
            0 : 
            key.hashCode()) ^ ((value == null) ? 
                0 : 
                value.hashCode());
    }

    public Object getKey() {
        return key;
    }

    public Object getValue() {
        return value;
    }

    public Object setValue(Object obj) {
        context.setAttribute(key.toString(), obj);

        return value;
    }
});

Bên cạnh việc sử dụng nhiều toán tử tam phân lồng nhau (chúng luôn luôn là một chỉ báo tốt cho an toàn chỗ làm, vì không một ai khác có thể đọc được mã), phần thú vị của các mã trùng lặp này không phải là ở chính bản thân mã đó. Đó là đoạn mào đầu xuất hiện trước các đoạn mã này trong hai phương thức, nơi có sự trùng lặp. Phương thức đầu tiên được hiển thị trong liệt kê 2:

Liệt kê 2. Phần mào đầu của lần xuất hiện đầu tiên của đoạn mã trùng lặp
while (enumeration.hasMoreElements()) {
    final String key = enumeration.nextElement().toString();
    final Object value = context.getAttribute(key);
    entries.add(new Map.Entry() {
    // remaining code elided, shown in Listing 1

Liệt kê 3 cho thấy đoạn mào đầu cho lần xuất hiện thứ hai của đoạn mã trùng lặp:

Liệt kê 3. Phần mào đầu thứ hai cho đoạn mã bị trùng lặp
while (enumeration.hasMoreElements()) {
    final String key = enumeration.nextElement().toString();
    final Object value = context.getInitParameter(key);
    entries.add(new Map.Entry() {
    // remaining code elided, shown in Listing 1

Sự khác biệt duy nhất trong toàn bộ vòng lặp while là lời gọi context.getAttribute(key) trong Liệt kê 2 so với lời gọi context.getInitParameter(key) trong Liệt kê 3. Rõ ràng là phần này có thể được tham số hoá, xếp gập các đoạn mã trùng lặp thành một phương thức riêng. Ví dụ này từ khung công tác Struts là một minh hoạ hoàn hảo về mã sao chép rẻ tiền, không những không cần thiết mà còn rất dễ sửa chữa.

Thực vậy, điều này minh họa rằng cách thu thập và bổ sung các mục vào tập hợp các thuộc tính là một mẫu đặc thù trong cơ sở mã của Struts. Việc cho phép các đoạn mã giống nhau nằm ở nhiều nơi che giấu một sự thực là đây là cái gì đó mà khung công tác Struts phải luôn luôn làm, ngăn không cho gói đoạn mã đó và đưa lên một chỗ có ý nghĩa hơn. Một cách để làm sạch thiết kế của nhiều lớp trong cơ sở mã Struts là nhận thức được rằng mẫu đặc thù này tồn tại và củng cố hành vi đó.

Các trùng lặp về cấu trúc

Một hình thức trùng lặp khó phát hiện hơn và do đó xảo quyệt hơn là sự trùng lặp về cấu trúc. Các nhà phát triển, từng làm việc với một số lượng giới hạn các ngôn ngữ (đặc biệt là các ngôn ngữ có hỗ trợ siêu lập trình (metaprogramming) yếu kém, chẳng hạn như Java và C #) thì sẽ đặc biệt khó nhìn thấy vấn đề này. Hiện tượng trùng lặp cấu trúc được tóm tắt một cách chuẩn xác nhất bằng một cụm từ mà người cùng làm việc với tôi là Pat Farley sử dụng: Cùng một khoảng trống, nhưng có giá trị khác nhau. Nói cách khác, bạn đã sao chép mã, mã này gần như giống nhau (nghĩa là khoảng trống là như nhau), nhưng với các giá trị khác nhau cho các biến. Sự trùng lặp này không xuất hiện trong các công cụ như CPD bởi vì các giá trị trong mỗi cá thể của cơ sở hạ tầng được lặp lại thực sự là duy nhất, nhưng tuy nhiên nó vẫn làm tổn hại đến mã của bạn.

Dưới đây là một ví dụ. Giả sử tôi có một lớp nhân viên (employee) đơn giản với một vài trường, như trong liệt kê 4:

Liệt kê 4. Một lớp nhân viên đơn giản
public class Employee {
    private String name;
    private int salary;
    private int hireYear;

    public Employee(String name, int salary, int hireYear) {
        this.name = name;
        this.salary = salary;
        this.hireYear = hireYear;
    }

    public String getName() { return name; }
    public int getSalary() { return salary;}
    public int getHireYear() { return hireYear; }
}

Có lớp đơn giản này, tôi muốn có khả năng sắp xếp theo bất kỳ trường nào của lớp. Ngôn ngữ Java có một cơ chế để đổi khác trật tự sắp xếp thông qua việc tạo ra các lớp trình so sánh (comparator), các lớp này thực hiện giao diện Comparator. Các trình so sánh theo tên và theo lương như trong liệt kê 5:

Liệt kê 5. Các trình so sánh theo tên và theo lương
public class EmployeeNameComparator implements Comparator<Employee> {
    public int compare(Employee emp1, Employee emp2) {
        return emp1.getName().compareTo(emp2.getName());
    }
}

public class EmployeeSalaryComparator implements Comparator<Employee> {
    public int compare(Employee emp1, Employee emp2) {
        return emp1.getSalary() - emp2.getSalary();                
    }
}

Đối với một nhà phát triển Java, điều này là hoàn toàn tự nhiên. Tuy nhiên, ta hãy xem xét các mã trong hình 4, nơi tôi đã đặt hai trình so sánh cạnh nhau:

Hình 4. Các trình so sánh được đặt cạnh nhau
Các trình so sánh được đặt cạnh nhau

Như bạn có thể thấy, thành ngữ cùng khoảng trống, nhưng các giá trị khác nhau áp dụng rất đúng. Hầu hết các mã là trùng lặp; phần khác nhau duy nhất là giá trị trả về. Bởi vì tôi đang sử dụng cơ sở hạ tầng phép so sánh theo một cách "tự nhiên” (nghĩa là cách thức đã được các nhà thiết kế ngôn ngữ dự định), rất khó để nhìn thấy sự trùng lặp một cách tự nhiên, nhưng rõ ràng là có sự trùng lặp đó. Có lẽ không quá tồi tệ chỉ với ba thuộc tính, nhưng điều gì sẽ xảy ra nếu sự trùng lặp phát triển lên cho nhiều thuộc tính? Tại thời điểm nào bạn quyết định tấn công sự trùng lắp này, và bạn làm điều này như thế nào?

Tôi sẽ sử dụng phép phản xạ để tạo ra một cơ sở hạ tầng xếp thứ tự chung nhất, cơ sở hạ tầng này không có nhiều mã khuôn đúc trùng lặp. Nhằm mục đích này, tôi tạo một lớp để xử lý cả việc sắp xếp lẫn việc tạo ra các trình so sánh cho từng trường một cách tự động. Liệt kê cho ta thấy lớp EmployeeSorter:

Liệt kê 6. Lớp EmployeeSorter
public class EmployeeSorter {

    public void sort(List<DryEmployee> employees, String criteria) {
        Collections.sort(employees, getComparatorFor(criteria));
    }

    private Method getSelectionCriteriaMethod(String methodName) {
        Method m;
        methodName = "get" + methodName.substring(0, 1).toUpperCase() +
                methodName.substring(1);
        try {
            m = DryEmployee.class.getMethod(methodName);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e.getMessage());
        }
        return m;
    }

    public Comparator<DryEmployee> getComparatorFor(final String field) {
        return new Comparator<DryEmployee>() {
            public int compare(DryEmployee o1, DryEmployee o2) {
                Object field1, field2;
                Method method = getSelectionCriteriaMethod(field);
                try {
                    field1 = method.invoke(o1);
                    field2 = method.invoke(o2);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return ((Comparable) field1).compareTo(field2);
            }
        };
    }
}

Phương thức sort() sử dụng phương thức Collecions.sort() chuyển giao danh sách các nhân viên và trình so sánh đã được tạo ra, khi gọi phương thức thứ ba của lớp này. Phương thức getComparatorFor() hoạt động như một phương thức nhà máy (factory) để tạo ra một lớp so sánh vô danh ngay trong hành trình (on the fly), dựa theo tiêu chí thông qua (passed-in criteria) . Phương thức này sử dụng phép phản xạ thông qua phương thức getSelectionCriteriaMethod() để gọi ra phương thức get thích đáng của lớp nhân viên, gọi phương thức này trên từng cá thể trong hai cá thể đang được so sánh và trả về kết quả. Các phép kiểm thử đơn vị trong liệt kê 7 cho thấy lớp này hoạt động như thế nào đối với một vài trường:

Liệt kê 7. Các phép kiểm thử cho các trình so sánh khái quát
public class TestEmployeeSorter {
    private EmployeeSorter _sorter;
    private ArrayList<DryEmployee> _list;
 
    @Before public void setup() {
        _sorter = new EmployeeSorter();
        _list = new ArrayList<DryEmployee>();
        _list.add(new DryEmployee("Homer", 20000, 1975));
        _list.add(new DryEmployee("Smithers", 150000, 1980));
        _list.add(new DryEmployee("Lenny", 100000, 1982));
    }

    @Test public void name_comparisons() {
        _sorter.sort(_list, "name");
        assertThat(_list.get(0).getName(), is("Homer"));
        assertThat(_list.get(1).getName(), is("Lenny"));
        assertThat(_list.get(2).getName(), is("Smithers"));
    }

    @Test public void salary_comparisons() {
        _sorter.sort(_list, "salary");
        assertThat(_list.get(0).getSalary(), is(20000));
        assertThat(_list.get(1).getSalary(), is(100000));
        assertThat(_list.get(2).getSalary(), is(150000));
    }
}

Việc sử dụng phép phản xạ như vậy là một sự đánh đổi giữa tính phức tạp với tính ngắn gọn. Phiên bản dựa trên phép phản xạ ban đầu là khó hiểu, nhưng nó mang lại một số lợi ích. Thứ nhất là nó tự động xử lý bất cứ thuộc tính nào của lớp Employee (nhân viên), cả hiện tại và lẫn tương lai. Có mã lệnh này, bạn có thể thêm các thuộc tính mới một cách an toàn cho lớp Employee mà không phải lo lắng về việc tạo các trình so sánh để sắp xếp chúng. Thứ hai là phiên bản này xử lý một số lượng lớn các thuộc tính một cách hiệu quả hơn. Việc bỏ qua các trùng lặp về cấu trúc là có thể được nếu sự trùng lặp này không quá đáng. Nhưng bạn phải tự hỏi mình: con số ngưỡng của các thuộc tính biện minh cho việc sử dụng phép phản xạ để giải quyết bài toán này là bao nhiêu? 10, 20 hay 50 thuộc tính? Con số này sẽ thay đổi tuỳ thuộc vào các nhà phát triển phần mềm và các đội phát triển phần mềm. Tuy nhiên, nếu bạn đang tìm kiếm một thước đo ít nhiều khách quan hơn, thì tại sao bạn không đo xem phiên bản phản xạ phức tạp như thế nào so với các trình so sánh cá thể ?.

Trong bài viết "Thiết kế hướng kiểm thử, phần 2," tôi đã giới thiệu về thước đo độ phức tạp chu số, một cách đo đơn giản của độ phức tạp tương đối của chỉ một phương thức đơn lẻ. Một công cụ mã nguồn mở tốt để đo độ phức tạp chu số cho ngôn ngữ Java là công cụ mã nguồn mở JavaNCSS (xem phần Tài nguyên). Nếu tôi chạy JavaNCSS trên một trong các lớp trình so sánh đơn lẻ, thì nó trả về 1, điều này không đáng ngạc nhiên: phương thức đơn trong lớp chỉ có một dòng duy nhất và không có các khối lệnh. Khi tôi chạy JavaNCSS trên toàn bộ lớp EmployeeSorter thì tổng các độ phức tạp chu số của tất cả các phương thức là 8. Điều đó cho thấy rằng một ngưỡng hợp lý cho số lượng các thuộc tính để chuyển sang phép phản xạ là 9; đó là khi độ phức tạp của các cấu trúc vượt quá độ phức tạp của phiên bản dựa trên phản xạ. Nếu sự phản xạ làm cho bạn bực mình, thì bạn có thể gắn thêm một ít điểm nữa cho nhân tố gây bực mình đó!

Ở mọi mức độ, mỗi giải pháp đều có cả phí tổn lẫn lợi ích gắn kết với nó, và trách nhiệm của bạn là cân nhắc sự đánh đổi ấy. Tôi đã quen với phép phản xạ trong ngôn ngữ Java và các ngôn ngữ khác, vì vậy tôi có xu hướng chọn giải pháp mạnh hơn vì tôi không thích sự lặp lại dưới mọi dạng thức trong phần mềm.


Tóm tắt

Trong bài viết này, tôi bắt đầu thảo luận về việc sử dụng biện pháp cấu trúc lại mã nguồn như là một công cụ để giúp hiểu và nhận biết thiết kế nổi dần lên. Tôi đã nói về việc dính kết vào cơ sở hạ tầng và các tổn hại mà nó gây ra cho thiết kế của bạn. Phần lớn bài viết này nói về sự trùng lặp dưới nhiều khía cạnh khác nhau. Giao điểm của việc cấu trúc lại mã nguồn và thiết kế là một lĩnh vực phong phú; bài viết tiếp theo tiếp tục chủ đề này bằng việc bàn luận về cách các số đo có thể giúp bạn như thế nào trong việc tìm ra các phần của mã của bạn cần cấu trúc lại nhất, và do đó chúng có nhiều khả năng nhất chứa các mẫu đặc thù đang chờ được khám phá.

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

  • PMD: Tải về PMD (bao gồm cả CPD).
  • Simian: Trình Simian (Trình phân tích các điểm tương tự- Similarity Analyser) nhận ra các trùng lặp trong các ngôn ngữ Java, C#, C, C++, COBOL, Ruby, JSP, ASP, HTML, XML, Visual Basic, và mã nguồn Groovy.
  • JavaNCSS: Tiện ích dòng lệnh này đo hai số đo mã nguồn mở tiêu chuẩn cho ngôn ngữ lập trình Java.

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=456698
ArticleTitle=Kiến trúc tiến hóa và thiết kế nổi dần: Tái cấu trúc mã nguồn hướng theo thiết kế
publish-date=12252009