PDF:

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 ([email protected])
Kiến trúc phần mềm
ThoughtWorks
25 12 2009
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.
Xem thêm bài trong loạt bài này
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
© Copyright IBM Corporation 2009
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ế
Nhẫn hiệu đăng ký
Trang 1 của 12
developerWorks®
ibm.com/developerWorks/vn/
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
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ế
Trang 2 của 12
ibm.com/developerWorks/vn/
developerWorks®
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:
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ế
Trang 3 của 12
developerWorks®
ibm.com/developerWorks/vn/
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
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ế
Trang 4 của 12
ibm.com/developerWorks/vn/
developerWorks®
- 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) ?
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ế
Trang 5 của 12
developerWorks®
ibm.com/developerWorks/vn/
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
đó.
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ế
Trang 6 của 12
ibm.com/developerWorks/vn/
developerWorks®
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:
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ế
Trang 7 của 12
developerWorks®
ibm.com/developerWorks/vn/
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
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ế
Trang 8 của 12
ibm.com/developerWorks/vn/
developerWorks®
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:
getComparatorFor()
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 độ
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ế
Trang 9 của 12
developerWorks®
ibm.com/developerWorks/vn/
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á.
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ế
Trang 10 của 12
ibm.com/developerWorks/vn/
developerWorks®
Tài nguyên
Học tập
• Lập trình viên năng suất (Tác giả: Neal Ford, nhà xuất bản O'Reilly Media, 2008): Cuốn sách
này mở rộng một số chủ đề trong loạt bài viết này.
• Tái cấu trúc mã nguồn: Cải thiện thiết kế của mã hiện có (Tác giả Martin Fowler et al. nhà xuất
bản: Addison-Wesley, 1999): Cuốn sách kinh điển về cấu trúc lại mã nguồn.
• Lập trình viên thực dụng (Các tác giả: Andy Hunt và Dave Thomas, nhà xuất bản Pragmatic
Programmers, 2001): Cuốn sách này đã làm cho nguyên tắc DRY trở nên nổi tiếng.
• "Sửa lỗi với PMD" (Tác giả Elliotte Rusty Harold, developerWorks, tháng Một năm 2005):
Tìm hiểu cách sử dụng các quy tắc đã xây dựng sẵn của PMD và các bộ quy tắc tùy chỉnh của
riêng bạn để nâng cao chất lượng mã Java của bạn.
• "Tự động hóa cho mọi người: Tiếp tục tái cấu trúc mã nguồn" (Tác giả: Paul Duvall,
developerWorks, tháng Bảy 2008): Đọc về cách sử dụng các công cụ phân tích tĩnh để xác
định “mùi” mã (code smell) thích hợp để cải tiến.
• Duyệt hiệu sách công nghệ để tìm sách về chủ đề này và các chủ đề kỹ thuật khác.
• Vùng công nghệ Java trên developerWorks: Tìm hàng trăm bài viết về mọi khía cạnh của lập
trình Java.
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
• Xem qua các blog developerWorks và tham gia vào cộng đồng My developerWorks
community.
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ế
Trang 11 của 12
developerWorks®
ibm.com/developerWorks/vn/
Đôi nét về tác giả
Neal Ford
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
© Copyright IBM Corporation 2009
(www.ibm.com/legal/copytrade.shtml)
Nhẫn hiệu đăng ký
(www.ibm.com/developerworks/vn/ibm/trademarks/)
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ế
Trang 12 của 12