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

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 ppt

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 (1.27 MB, 18 trang )

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

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));
}

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)));
}

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ụ:
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);
}

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() {
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:

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

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:

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.

Mục lục

 Chất lượng kiểm thử
 Đo chất lượng mã

 Tóm tắt

×