Tải bản đầy đủ (.pdf) (22 trang)

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ế pdf

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (480.6 KB, 22 trang )

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
Neal Ford, Kiến trúc phần mềm, ThoughtWorks
Tóm tắt: 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.
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

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

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 ScheduleItemImpl và ScheduleItemForm 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

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 PortletXXX
và XXX (Ví dụ: PortletApplicationMap và ApplicationMap). Đ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

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á.

Mục lục

 Gắn kết với cơ sở hạ tầng
 Các vi phạm đối với nguyên tắc DRY
 Tóm tắt

×