PDF:

Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo
kiểm thử, phần 2
Bàn luận thêm về việc cho phép dùng kiểm thử để định hướng và cải
thiện thiết kế của bạn
Neal Ford ([email protected])
Kiến trúc phần mềm
ThoughtWorks
11 12 2009
Kiểm thử chỉ là một tác dụng phụ của việc phát triển hướng theo kiểm thử (TDD - test-driven
development); khi được thực hiện đúng cách, TDD sẽ cải thiện thiết kế tổng thể của mã của bạn.
Phần thứ 2 này của bài viết Kiến trúc tiến hóa và thiết kế nổi dần sẽ hoàn tất các bước hướng
dẫn về một ví dụ được mở rộng, cho thấy cách làm thế nào để thiết kế có thể xuất hiện dần từ
các mối quan tâm nảy sinh trong quá trình kiểm thử.
Xem thêm bài trong loạt bài này
Đây là phần thứ hai của bài viết gồm hai phần, nghiên cứu cách sử dụng TDD như thế nào để cho
phép làm nổi dần các bước thiết kế tốt hơn từ quá trình viết kiểm thử trước khi bạn viết mã. Tại
phần 1, tôi đã viết một phiên bản của trình tìm số hoàn hảo (perfect numbers), sử dụng cách phát
triển kiểm thử sau (viết các phép kiểm thử sau khi viết mã). Sau đó, tôi đã viết một phiên bản sử
dụng TDD (viết các phép kiểm thử trước khi viết mã, cho phép kiểm thử chi phối thiết kế mã lệnh).
Ở cuối phần 1, tôi thấy rằng tôi đã mắc phải một lỗi cơ bản khi suy nghĩ về loại cấu trúc dữ liệu được
sử dụng để lưu giữ danh sách các số hoàn hảo: bản năng mách bảo tôi bắt đầu bằng một danh
sách mảng (ArrayList), nhưng tôi thấy rằng phép trừu tượng hóa thành kiểu tập hợp (Set). Tôi sẽ
bắt đầu từ điểm này, mở rộng các thảo luận theo cách mà bạn có thể cải thiện chất lượng của các
phép kiểm thử của bạn và kiểm tra chất lượng của mã lệnh cuối cùng.
Chất lượng kiểm thử
Phép kiểm thử sử dụng cách trừu tượng hóa thành kiểu Set tốt hơn có trong liệt kê 1:
© Copyright IBM Corporation 2009
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Nhẫn hiệu đăng ký
Trang 1 của 12
developerWorks®
ibm.com/developerWorks/vn/
Liệt kê 1. Kiểm thử đơn vị với cách trừu tượng hóa thành Set tốt hơn
@Test public void add_factors() {
Set<Integer> expected =
new HashSet<Integer>(Arrays.asList(1, 2, 3, 6));
Classifier4 c = new Classifier4(6);
c.addFactor(2);
c.addFactor(3);
assertThat(c.getFactors(), is(expected));
}
Mã này kiểm thử một trong những phần quan trọng nhất trong miền bài toán của tôi: lấy các ước
số của một số. Tôi muốn kiểm tra hành vi đó một cách kỹ lưỡng bởi vì nó là phần phức tạp nhất
của bài toán, dễ bị gặp lỗi nhất. Tuy nhiên, nó chứa một cấu trúc cồng kềnh, đó là: new HashSet
(Arrays.asList (1, 2, 3, 6)). Ngay cả với sự hỗ trợ của IDE hiện đại, cấu trúc này làm cho mã lệnh rắc
rối: gõ nhập new, gõ nhập Has và để mã bên trong tiếp tục; gõ nhập <Int và để mã bên trong tiếp
tục, thật chán. Tôi sẽ làm cho điều này trở nên dễ dàng hơ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 chịu trách nhiệm 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.
Kiểm thử theo nguyên tắc Moist
Một trong những câu “châm ngôn” để viết mã lệnh tốt có trong cuốn The Pragmatic Programmer
(Lập trình viên thực dụng) của các tác giả Andy Hunt và Dave Thomas (xem mục Tài nguyên) —
là nguyên tắc DRY (Don't Repeat Yourself – Đừng lặp lại chính mình). Nó khuyên nhủ bạn tránh
mọi sự lặp lại mã của bạn vì điều này thường gây ra các vấn đề. Tuy nhiên, nguyên tác DRY không
áp dụng cho các kiểm thử đơn vị. Các kiểm thử đơn vị thường phải kiểm tra các sắc thái hành vi
của mã được kiểm thử, dẫn đến các tình huống tương tự và trùng lặp nhau. Mã sao chép và dán để
tạo ra kết quả mong đợi trong Liệt kê 1 (hàm HashSet (Arrays.asList (1, 2, 3, 6)) mới) là một ví dụ
tuyệt vời về điều này bởi vì bạn sẽ muốn có rất nhiều biến thể của nó trong các phép kiểm thử khác
nhau. (N.D: tác giả chơi chữ ở đây khi đưa ra nguyên tắc Moist. “Moist” – nghĩa là “ẩm ướt” đối lập
với DRY- nghĩa là “khô”).
Quy tắc ngón tay cái TDD của tôi là các kiểm thử chỉ là ẩm (moist) chứ không phải là ướt sũng
nước (drenched). Ý tôi muốn nói là một số trùng lắp trong các phép kiểm thử có thể chấp nhận
được (và không tránh khỏi), nhưng bạn không nên đi quá xa, tạo ra các cấu trúc cồng kềnh lặp đi
lặp lại. Để đạt mục đích này, tôi sẽ tái cấu trúc phép kiểm thử của mình để cung cấp một phương
thức phụ trợ riêng tư (private) để giúp tôi xử lý cách viết hàm tạo phổ biến này, nó có trong liệt kê
2:
Liệt kê 2. Phương thức phụ trợ để giữ cho phép thử của tôi ở mức “ẩm”
private Set<Integer> expectationSetWith(Integer... numbers) {
return new HashSet<Integer>(Arrays.asList(numbers));
}
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 2 của 12
ibm.com/developerWorks/vn/
developerWorks®
Mã trong Liệt kê 2 làm cho tất cả các phép kiểm thử của tôi về các ước số trở nên sạch hơn nhiều,
như đã thấy trong phép kiểm thử thể hiện trong Liệt kê 3, được viết lại từ liệt kê 1:
Liệt kê 3. Phép kiểm thử “ẩm hơn” để kiểm tra các ước số của một số
@Test public void factors_for_6() {
Set<Integer> expected = expectationSetWith(1, 2, 3, 6);
Classifier4 c = new Classifier4(6);
c.calculateFactors();
assertThat(c.getFactors(), is(expected));
}
Bởi vì bạn đang viết các phép kiểm thử không có nghĩa là bạn phải vứt bỏ đi các nguyên tắc thiết
kế tốt. Phép kiểm thử là các loại mã lệnh khác, nhưng các nguyên tắc tốt (mặc dù khác) cũng được
áp dụng đối với chúng.
Các điều kiện biên
TDD khuyến khích các nhà phát triển phần mềm viết một phép kiểm thử không thực hiện được khi
viết phép kiểm thử đầu tiên cho một chức năng mới nào đó. Điều này tránh việc phép kiểm thử vô
tình chạy thông suốt trong mọi trường hợp, làm cho phép kiểm thử thực sự không kiểm tra bất cứ
điều gì (phép kiểm thử thừa – tautology test). Các phép kiểm thử cũng có thể xác minh hành vi mà
bạn nghĩ rằng bạn là đúng nhưng chưa kiểm tra đủ để tự tin. Các phép kiểm thử này không nhất
thiết phải là trước tiên thất bại (mặc dù thất bại khi bạn nghĩ rằng phép kiểm thử sẽ thông suốt là
điều hoàn toàn tốt bởi vì bạn đã tìm ra một lỗi tiềm tàng). Suy nghĩ về việc kiểm thử dẫn bạn đến
xem xét những gì có thể kiểm thử được.
Một số trường hợp kiểm thử thường không được chú ý là các điều kiện biên: mã của bạn sẽ làm
gì khi phải đối mặt với đầu vào bất thường? Khi viết nhiều phép kiểm thử đối với phương thức
getFactors() sẽ mở ra cho bạn suy nghĩ về những đầu vào hợp lý và không hợp lý nào có thể xảy ra.
Với mục đích này, tôi sẽ bổ sung một số phép thử dành cho các điều kiện biên đáng chú ý, được
thể hiện trong liệt kê 4:
Liệt kê 4. Các điều kiện biên cho phân tích ước số
@Test public void factors_for_100() {
Classifier5 c = new Classifier5(100);
c.calculateFactors();
assertThat(c.getFactors(),
is(expectationSetWith(1, 100, 2, 50, 4, 25, 5, 20, 10)));
}
@Test(expected = InvalidNumberException.class)
public void cannot_classify_negative_numbers() {
new Classifier5(-20);
}
@Test public void factors_for_max_int() {
Classifier5 c = new Classifier5(Integer.MAX_VALUE);
c.calculateFactors();
assertThat(c.getFactors(), is(expectationSetWith(1, 2147483647)));
}
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 3 của 12
developerWorks®
ibm.com/developerWorks/vn/
Con số 100 dường như thú vị bởi vì nó có rất nhiều ước số. Bằng cách kiểm thử cho các số khác
nhau, tôi nhận ra rằng trong miền bài toán việc có các số âm là vô nghĩa, do đó, tôi đã viết một
phép kiểm thử (và thực sự phép thử này đã thất bại trước khi tôi sửa lỗi ấy) để loại trừ các số âm.
Nghĩ về các số âm cũng làm cho tôi nghĩ về MAX_INT: Phải chăng giải pháp của tôi nên xem xét
những gì sẽ xảy ra nếu người sử dụng hệ thống cần các số lớn, kiểu long? Giả định ban đầu của tôi
chỉ giới hạn ở các số kiểu interger, nhưng tôi cần phải chắc chắn rằng đây là một giả định hợp lệ.
Thu thập các yêu cầu là quá trình nén chịu thiệt (lossy
compression – khi nén sẽ bị mất thông tin)
Bạn hãy nhìn xung quanh mình và tìm một bức tranh hoặc tác phẩm nghệ thuật. Giả sử rằng
bức tranh đó chứa 2 triệu điểm ảnh (pixel). Điều gì sẽ xảy ra nếu bạn nén bức tranh đó để chỉ
có 2.000 điểm ảnh? Bức tranh đó vẫn còn trông như cũ không? (Có lẽ thế nếu đó là một bức
tranh của Rothko (N.D: hoạ sĩ theo trường phái trừu tượng, tranh của ông chỉ gồm các mảng
mầu), nhưng đó là một trường hợp hiếm hoi). Thao tác nén bằng cách loại bỏ các thông tin
là một thuật toán nén chịu thiệt. Nếu bạn dùng phiên bản đã nén và cố gắng khôi phục lại nó
thành 2 triệu điểm ảnh, thì bạn sẽ cần phải thực hiện một số ngón nghề. Đôi khi bạn có thể
đoán đúng, nhưng không phải trong mọi trường hợp.
Các phiên làm việc yêu cầu “big design up front" (N.D: phương thức "thiết kế hoàn hảo trước,
viết mã chương trình sau”, thường gắn với mô hình thác nước trong phát triển phần mềm)
truyền thống là quá trình nén chịu thiệt đối với những gì mà một ứng dụng cần làm. Các nhà
phân tích nghiệp vụ không thể lường trước mọi vấn đề sẽ phát sinh, do đó các nhà phát triển
sẽ phải tạo ra các thông tin để điền vào các chi tiết. Các nhà phát triển nổi tiếng là những
người làm việc này rất tệ, dẫn đến nhiều điều bực mình giữa những người xác định các yêu
cầu và những người thực hiện các yêu cầu đó.
Các quy trình lanh lẹn nỗ lực giảm bớt sự mất mát thông tin này bằng cách trì hoãn thuật toán
giải nén càng muộn càng tốt và luôn luôn trông cậy vào một ai đó có thể trả lời câu hỏi về
những điều thực sự nên làm. Thiết kế mà không có chi tiết thiết kế là điều không thể, vì vậy dù
phương thức luận của bạn là gì, thì bạn phải tìm ra một cách hoàn toàn khả dĩ để điền vào các
chi tiết chắc chắn bị loại bỏ bởi quá trình thu thập và xác định.
Việc kiểm thử các điều kiện biên buộc bạn phải đặt dấu hỏi cho các giả định của bạn. Rất dễ đưa ra
các giả định không hợp lệ khi mã hóa một giải pháp. Trong thực tế, đây là một trong những điểm
yếu của việc thu thập các yêu cầu truyền thống - nó không bao giờ có thể tập hợp đủ chi tiết để loại
bỏ các câu hỏi khi triển khai thực hiện, chắc chắn sẽ xảy ra. Quá trình thu thập các yêu cầu là một
dạng nén chịu thiệt.
Bởi vì có quá nhiều điều bị bỏ sót bởi quá trình xác định những gì mà một phần mềm phải làm, bạn
cần một cơ chế tại chỗ để giúp bạn tạo lại các câu hỏi mà bạn phải đưa ra để hiểu nó hoàn toàn.
Phỏng đoán về những gì những người kinh doanh thực sự mong muốn là điều nguy hiểm vì bạn sẽ
nhận được phần lớn câu trả sai. Sử dụng các phép kiểm thử để kiểm tra các điều kiện biên giúp bạn
tìm ra các vấn đề để hỏi, mà hầu hết chúng là câu hỏi về cách hiểu vấn đề. Việc tìm ra các câu hỏi
đúng có ý nghĩa rất nhiều trong việc đạt được một thiết kế tốt.
Các phép kiểm thử dương và âm
Khi bắt đầu việc khảo sát các vấn đề này, tôi phân rã nó thành nhiều tác vụ con. Khi tôi viết các
phép kiểm thử, tôi phát hiện một tác vụ phân rã quan trọng. Sau đây là toàn bộ danh sách các tác
vụ:
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 4 của 12
ibm.com/developerWorks/vn/
developerWorks®
1. Tôi cần các ước số của số đang xét.
2. Tôi cần phải xác định xem một số có phải là một ước số không.
3. Tôi cần phải xác định làm thế nào để bổ sung các ước số vào danh sách các ước số.
4. Tôi cần phải tính tổng các ước số.
5. Tôi cần phải xác định xem một số có là hoàn hảo không.
Hai tác vụ còn lại là tính tổng các ước số và kiểm tra tính hoàn hảo của số đang xét. Không có gì
ngạc nhiên xảy ra với hai tác vụ này; hai phép kiểm thử cuối cùng có trong liệt kê 5:
Liệt kê 5. Hai phép kiểm thử cuối cùng cho các số hoàn hảo
@Test public void sum() {
Classifier5 c = new Classifier5(20);
c.calculateFactors();
int expected = 1 + 2 + 4 + 5 + 10 + 20;
assertThat(c.sumOfFactors(), is(expected));
}
@Test public void perfection() {
int[] perfectNumbers =
new int[] {6, 28, 496, 8128, 33550336};
for (int number : perfectNumbers)
assertTrue(classifierFor(number).isPerfect());
}
Sau khi xem trang web Wikipedia để tìm một vài số hoàn hảo đầu tiên, tôi có thể viết một phép
kiểm thử, kiểm tra xem tôi thực tế có thể tìm thấy các số hoàn hảo hay không. Nhưng tôi chưa kết
thúc. Kiểm thử dương chỉ là một nửa công việc. Tôi cũng cần một phép kiểm thử để kiểm tra xem
liệu tôi có vô tình nhận nhầm một số không hoàn hảo. Với mục đích này, tôi viết một phép thử âm,
như trong liệt kê 6:
Liệt kê 6. Phép thử âm để đảm bảo rằng việc phân loại số hoàn hảo làm việc chính
xác.
@Test public void test_a_bunch_of_numbers() {
Set<Integer> expected = new HashSet<Integer>(
Arrays.asList(PERFECT_NUMS));
for (int i = 2; i < 33550340; i++) {
if (expected.contains(i))
assertTrue(classifierFor(i).isPerfect());
else
assertFalse(classifierFor(i).isPerfect());
}
}
Mã này cho biết rằng thuật toán số hoàn hảo của tôi làm việc một cách chính xác, nhưng nó rất
chậm. Tôi có thể đoán được lý do tại sao bằng cách xem phương thức calculateFactors() của tôi,
hiển thị trong liệt kê 7:
Liệt kê 7. Phương thức getFactors() đơn sơ.
public void calculateFactors() {
for (int i = 2; i < _number; i++)
if (isFactor(i))
addFactor(i);
}
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 5 của 12
developerWorks®
ibm.com/developerWorks/vn/
Vấn đề biểu hiện trong Liệt kê 7 tương tự như vấn đề trong phiên bản mã kiểm thử sau trong Phần
1 của loạt bài: Mã lệnh thu thập các ước số đi suốt toàn bộ con đường cho đến tận chính số đó. Tôi
có thể cải thiện mã này bằng cách thu thập các ước số theo cặp, cho phép tôi chỉ phân tích tới căn
bậc hai của số đang xét, như được thể hiện trong phiên bản mã đã tái cấu trúc trong liệt kê 8:
Liệt kê 8. Phiên bản đã tái cấu trúc, hoạt động tốt hơn của phương thức
calculateFactors()
public void calculateFactors() {
for (int i = 2; i < sqrt(_number) + 1; i++)
if (isFactor(i))
addFactor(i);
}
public void addFactor(int factor) {
_factors.add(factor);
_factors.add(_number / factor);
}
Đây là cách tái cấu trúc mã lệnh tương tự cách mà tôi đã làm trong phiên bản mã kiểm thử sau
(trong Phần 1), nhưng lần này có sự thay đổi trong hai phương thức khác nhau. Sự thay đổi ở đây
đơn giản hơn vì tôi đã trừu tượng hóa chức năng addFactors() thành một phương thức riêng của
nó, và phiên bản này sử dụng cách trừu tượng hóa thành Set, loại bỏ việc kiểm thử vụng về để chắc
chắn rằng tôi không nhận các ước số hai lần như trong phiên bản kiểm thử sau.
Nguyên tắc chỉ đạo của việc tối ưu hóa luôn luôn phải là làm cho đúng, sau đó làm cho nhanh. Một
bộ đầy đủ các phép kiểm thử đơn vị làm cho việc kiểm tra các hành vi trở nên dễ dàng, cho phép
bạn tự do chơi trò chơi “What if” với việc tối ưu hóa mà không cần lo lắng rằng bạn đã làm sai điều
gì đó.
Tôi đã làm xong với phiên bản mã hướng theo kiểm thử của trình tìm số hoàn hảo. Toàn bộ mã của
lớp này được hiển thị trong liệt kê 9.
Liệt kê 9. Phiên bản TDD đầy đủ của trình phân loại số
public class Classifier6 {
private Set<Integer> _factors;
private int _number;
public Classifier6(int number) {
if (number < 1)
throw new InvalidNumberException(
"Can't classify negative numbers");
_number = number;
_factors = new HashSet<Integer>();
_factors.add(1);
_factors.add(_number);
}
private boolean isFactor(int factor) {
return _number % factor == 0;
}
public Set<Integer> getFactors() {
return _factors;
}
private void calculateFactors() {
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 6 của 12
ibm.com/developerWorks/vn/
developerWorks®
for (int i = 2; i < sqrt(_number) + 1; i++)
if (isFactor(i))
addFactor(i);
}
private void addFactor(int factor) {
_factors.add(factor);
_factors.add(_number / factor);
}
private int sumOfFactors() {
int sum = 0;
for (int i : _factors)
sum += i;
return sum;
}
public boolean isPerfect() {
calculateFactors();
return sumOfFactors() - _number == _number;
}
}
Các phương thức có thể hợp thành được
Một trong những lợi ích của mã được phát triển hướng theo kiểm thử đã đề cập trong Phần 1 của
bài biết này là khả năng hợp thành, dựa trên bản mẫu phương thức hợp thành của Kent Beck (xem
mục Tài nguyên). Phương thức hợp thành khuyến khích xây dựng các phần mềm với nhiều phương
thức kết dính nhau. TDD tạo điều kiện thuận lợi cho cách làm này bởi vì bạn phải có các bó nhỏ
các chức năng để kiểm thử được. Phương thức hợp thành cũng trợ giúp việc thiết kế bởi vì nó tạo
ra các khối xây dựng có thể tái sử dụng.
Bạn có thể thấy điều này qua số hiệu và tên của các phương thức trong giải pháp mà TDD đem lại.
Dưới đây là những phương thức trong phiên bản cuối cùng của trình phân loại các số hoàn hảo theo
TDD:
•
•
•
•
•
•
isFactor()
getFactors()
calculateFactors()
addFactor()
sumOfFactors()
isPerfect()
Dưới đây là một ví dụ về những lợi ích của phương thức hợp thành. Giả sử bạn đã viết trình tìm kiếm
số hoàn hảo TDD của mình, và một số nhóm khác trong công ty của bạn đã viết một phiên bản
kiểm thử sau của trình tìm kiếm số hoàn hảo (một ví dụ về trình tìm kiếm này có tại Phần 1 của loạt
bài này). Bây giờ, những người sử dụng của bạn chạy vào phòng hốt hoảng: "Chúng ta cũng phải
xác định cả các số thừa và các số thiếu nữa!" Trong một số thừa (abundant number), thì tổng các
ước số của số đó lớn hơn chính nó, và trong một số thiếu (deficient number), thì tổng các ước số
của số đó nhỏ hơn chính nó.
Đối với phiên bản kiểm thử sau, ở đây tất cả các lô gic đều nằm trong một phương thức dài, thì họ
phải viết lại toàn bộ giải pháp, tái cấu trúc các mã lệnh chung cho các số thừa, số thiếu và số hoàn
hảo. Trong phiên bản TDD, tôi chỉ cần viết hai phương thức mới, được thể hiện trong liệt kê 10:
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 7 của 12
developerWorks®
ibm.com/developerWorks/vn/
Liệt kê 10. Hỗ trợ cho các số thừa và các số thiếu
public boolean isAbundant() {
calculateFactors();
return sumOfFactors() - _number > _number;
}
public boolean isDeficient() {
calculateFactors();
return sumOfFactors() - _number < _number;
}
Tác vụ duy nhất còn lại trong hai phương thức này là tái cấu trúc phương thức calculateFactors()
thành hàm tạo (constructor) của lớp. (Nó là vô hại trong phương thức isPerfect(), nhưng bây giờ
phương thức này được sao chép lại trong cả ba phương thức và do đó nên được tái cấu trúc).
Việc viết mã như là các khối xây dựng nhỏ làm cho mã có thêm khả năng được tái sử dụng, vì thế
điều này nên được coi là một trong những tiêu chí thiết kế chính của bạn. Việc sử dụng các phép
kiểm thử để giúp tiến triển dần thiết kế của bạn sẽ khuyến khích viết các phương thức hợp thành
được, như vậy sẽ cải thiện thiết kế của bạn.
Đo chất lượng mã
Ngay đầu Phần 1 của loạt bài viết, tôi đã tuyên bố rằng khách quan mà nói thì phiên bản mã TDD
sẽ tốt hơn phiên bản mã kiểm thử sau. Tôi đã cho các bạn thấy một số bằng chứng nhỏ nhặt, nhưng
lấy gì để chứng minh điều này? Tất nhiên, ta không có biện pháp hoàn toàn khách quan nào để
đánh giá chất lượng của mã, nhưng ta có một số thước đo có thể cho biết các kích thước nhất
định của chất lượng mã; một trong những kích thước đó là tính phức tạp đo lường (xem phần Tài
nguyên), do Thomas McCabe tạo ra để đo độ phức tạp chu số (cyclomatic complexity) của mã.
Công thức khá đơn giản: lấy số lượng các cung trừ đi số các nút rồi cộng với 2, ở đây các cung là
tuyến đường thi hành và các nút là các dòng mã. Để lấy ví dụ, bạn hãy xem xét các mã trong liệt kê
11:
Liệt kê 11. Phương thức Java đơn giản để xác định độ phức tạp chu số
public void doit() {
if (c1) {
f1();
} else {
f2();
}
if (c2) {
f3();
} else {
f4();
}
}
Nếu bạn vẽ sơ đồ phương thức trong Liệt kê 11 thành một biểu đồ dòng chảy, bạn có thể dễ dàng
đếm được số lượng các cung và các nút và tính toán độ phức tạp chu số, như trong hình 1. Phương
thức này có độ phức tạp chu số là 3 (8 -7 + 2).
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 8 của 12
ibm.com/developerWorks/vn/
developerWorks®
Hình 1. Các nút và các cung của phương thức doit ()
Để đo hai phiên bản mã của trình tìm số hoàn hảo, tôi sẽ sử dụng một công cụ mã nguồn mở đo
tính phức tạp chu số của Java gọi là JavaNCSS ("NCSS" là viết tắt của "non-commenting source
statements – các câu lệnh nguồn không giải thích", mà công cụ này cũng đo số lượng NCSS đó).
Xem phần Tài nguyên để biết thông tin tải về. (N.D: NCSS là số câu lệnh về lô gic, không bị xuống
dòng để chèn thêm các giải thích, thường sẽ ít hơn so với số dòng lệnh (Source Lines of Code SLOC) là số dòng mã nguồn về mặt vật lý).,
Việc chạy JavaNCSS với phiên bản mã kiểm thử sau cho các kết quả như trong hình 2:
Hình 2. Độ phức tạp chu số của trình tìm kiếm số hoàn hảo, phiên bản kiểm thử
sau
Chỉ có một phương thức tồn tại trong phiên bản này, và JavaNCSS cho kết quả là các phương thức
có trung bình là 13 dòng mã, với độ phức tạp chu số là 5.00. Bạn hãy so sánh kết quả này với phiên
bản TDD, như trong hình 3:
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 9 của 12
developerWorks®
ibm.com/developerWorks/vn/
Hình 3. Độ phức tạp chu số của phiên bản TDD của trình tìm kiếm số hoàn hảo
Phiên bản TDD của mã rõ ràng bao gồm nhiều phương thức hơn, trung bình 3.56 dòng mã cho mỗi
phương thức, với độ phức tạp chu số trung bình chỉ là 1.56. Đo bằng biện pháp này, phiên bản TDD
đơn giản hơn hơn ba lần so với mã kiểm thử sau. Ngay cả đối với bài toán nhỏ này, đó là một sự
khác biệt đáng kể.
Tóm tắt
Trong hai bài viết vừa qua của loạt bài Kiến trúc tiến hóa thiết kế nổi dần tôi đã trình bày sâu về các
lợi ích của kiểm thử trước khi bạn viết mã của mình. Bạn có được phương thức đơn giản hơn, được
trừu tượng hóa tốt hơn, có thể tái sử dụng dễ hơn như các khối xây dựng. Và bạn có các phép kiểm
thử miễn phí!
Việc kiểm thử có thể dẫn bạn quay lại tuyến đường thiết kế tốt hơn nếu bạn đi trệch ra. Một trong
những cái hại ngầm đến một thiết kế tốt là những nhà thiết kế và các định kiến của họ. Việc cắt
đứt khỏi ý nghĩ của bạn những phần đã vô tình quyết định sai là điều khó khăn. TDD cung cấp một
cách thức thành thói quen để cho các giải pháp nổi lên như bong bóng từ các bài toán thay vì tuôn
xuống như mưa các suy nghĩ lầm lẫn.
Trong phần tiếp theo của loạt bài viết, tôi sẽ tạm dừng nói đến kiểm thử và nói về hai mẫu hình
quan trọng của thế giới lập trình Smalltalk: phương thức hợp thành và nguyên lý chỉ một mức trừu
tượng.
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
Trang 10 của 12
ibm.com/developerWorks/vn/
developerWorks®
Tài nguyên
Học tập
• "Thiết kế hướng kiểm thử, phần 1" (Neal Ford, developerWorks, tháng 2 năm 2009): Đọc phần
đầu của bài viết này.
• Lập trình viên thực dụng (Andy Hunt và Dave Thomas, Các lập trình viên thực dụng, 2001):
Nguyên tắc DRY được cuốn sách này làm cho nổi tiếng không nhất thiết phải áp dụng cho
TDD.
• Số hoàn hảo: Từ điển Wikipedia giải thích các khái niệm toán học của số hoàn hảo.
• Phát triển hướng theo kiểm thử thông qua các ví dụ (Kent Beck, Addison-Wesley, 2003):
Beck, tác giả của Extreme Programming, sử dụng các ví dụ dựa trên tiền tệ để giải thích TDD.
• Các mẫu thực hành tốt nhất của Smalltalk (Kent Beck, Prentice Hall, 1996): Tìm hiểu thêm về
mẫu phương thức hợp thành.
• "Theo đuổi chất lượng mã: Giám sát độ phức tạp chu số" (Andrew Glover, DeveloperWorks,
tháng Ba năm 2006): Đọc tài liệu về việc sử dụng các thước đo mã đơn giản và các công cụ
dựa trên Java để đo độ phức tạp chu số.
• 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.
• Khu công nghệ Java của 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ệ
• JavaNCSS: Tải về bộ đo lường mã nguồn JavaNCSS.
Thảo luận
• Xem trang developerWorks blogs và tham gia vào cộng đồng developerWorks.
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế hướng theo kiểm
thử, phần 2
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: Thiết kế hướng theo kiểm
thử, phần 2
Trang 12 của 12