PDF:

Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo
thử nghiệm, Phần 1
Cho phép thử nghiệm để điều khiển và cải tiến thiết kế của bạn
Neal Ford ([email protected])
Kiến trúc phần mềm
ThoughtWorks
20 05 2009
Hầu hết các nhà phát triển nghĩ rằng phần mang lại lợi ích nhất của việc áp dụng phát triển dựa
theo thử nghiệm (TDD) là các thử nghiệm. Tuy nhiên, khi đã thực hiện đúng, TDD cải thiện thiết
kế tổng thể của mã lệnh của bạn. Bài viết này trong loạt bài kiến trúc tiến hóa và thiết kế nổi
dần thông qua một ví dụ mở rộng sẽ chỉ ra thiết kế có thể rõ nét dần từ các mối quan tâm nổi
lên sau các thử nghiệm như thế nào. Việc thử nghiệm chỉ là hiệu quả phụ của TDD; phần quan
trọng là làm thế nào để nó thay đổi mã lệnh của bạn cho tốt hơn.
Xem thêm bài trong loạt bài này
Một trong những biện pháp thực tiễn phổ biến để phát triển nhanh là TDD. TDD là một phong cách
viết phần mềm có sử dụng các thử nghiệm để giúp bạn hiểu được bước cuối cùng của pha xác định
các yêu cầu. Bạn viết các thử nghiệm trước khi bạn viết mã lệnh, củng cố thêm hiểu biết của bạn về
những cái mà mã lệnh phải làm.
Hầu hết các nhà phát triển cho rằng lợi ích hàng đầu thu được từ TDD là tập hợp toàn diện các thử
nghiệm đơn vị mà bạn nhận được. Tuy nhiên, khi thực hiện đúng, TDD có thể thay đổi thiết kế tổng
thể của mã lệnh của bạn thành tốt hơn bởi vì nó trì hoãn các quyết định cho đến thời điểm hợp lý
cuối cùng. Bởi vì bạn không thực hiện các quyết định thiết kế từ trước, nó bỏ ngỏ cho bạn các tùy
chọn thiết kế tốt hơn hoặc cấu trúc lại để thiết kế tốt hơn. Bài viết này đi từng bước thông qua một
ví dụ để minh họa sức mạnh của việc cho phép thiết kế nổi rõ lên từ các quyết định xung quanh các
thử nghiệm đơn vị.
Về loạt bài viết này
Loạt bài này nhằm mục đích cung cấp một cách nhìn mới mẻ về các khái niệm thường được
bàn luận nhưng khó nắm bắt ý nghĩa của thiết kế và kiến trúc phần mềm. Thông qua các ví dụ
cụ thể, Neal Ford sẽ mang lại cho bạn một nền móng vững chắc về các biện pháp thực hành
nhanh kiến trúc tiến hóa và thiết kế nổi dần. Bằng cách lùi các quyết định thiết kế và kiến trúc
quan trọng đến thời điểm hợp lý cuối cùng, bạn có thể ngăn ngừa không cho những sự phức
tạp không cần thiết hủy hoại các dự án phần mềm của bạn.
© Copyright IBM Corporation 2009
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Nhẫn hiệu đăng ký
Trang 1 của 12
developerWorks®
ibm.com/developerWorks/vn/
Luồng công việc của TDD
Một từ quan trọng trong thuật ngữ phát triển dựa theo thử nghiệm là dựa theo, báo hiệu rằng việc
thử nghiệm điều khiển quá trình phát triển. Hình 1 cho thấy luồng công việc của TDD:
Hình 1. Luồng công việc của TDD
Luồng công việc trong hình 1 là:
1. Viết một thử nghiệm không thành công.
2. Viết mã lệnh để làm cho nó thông qua.
3. Lặp lại các bước 1 và 2.
4. Đồng thời cấu trúc lại quyết liệt.
5. Khi bạn không thể nghĩ đến bất kỳ thử nghiệm nào thêm nữa, bạn đã xong việc.
Dựa theo thử nghiệm so với thử nghiệm sau
Việc phát triển - dựa theo thử nghiệm yêu cầu các thử nghiệm xuất hiện trước. Chỉ sau khi bạn đã
viết các thử nghiệm (và thất bại) bạn mới viết mã lệnh được thử nghiệm. Nhiều nhà phát triển sử
dụng một biến thể cách làm thử nghiệm được gọi là phát triển thử nghiệm sau (TAD), ở đó bạn viết
mã lệnh và sau đó viết các thử nghiệm đơn vị. Trong trường hợp này, bạn vẫn nhận được các thử
nghiệm, nhưng bạn không nhận được các khía cạnh thiết kế nổi dần của TDD. Chẳng có gì ngăn cản
bạn viết mã lệnh cực kỳ ghớm guốc và sau đó lúng túng tìm cách để thử nghiệm nó như thế nào.
Khi viết mã lệnh trước, bạn đã nhúng các định kiến của bạn về cách thức mã sẽ hoạt động ra sao,
sau đó thử nghiệm nó. TDD đòi hỏi bạn phải làm ngược lại: viết các thử nghiệm trước và cho phép
nó thông báo cho bạn cách làm thế nào để viết mã lệnh làm cho thử nghiệm thông qua. Để minh
họa sự khác biệt quan trọng này, tôi sẽ bắt đầu một ví dụ mở rộng.
Các số hoàn hảo
Để cho thấy các lợi ích thiết kế của TDD, tôi cần một bài toán để giải quyết. Trong cuốn sách Phát
triển dựa theo thử nghiệm của mình (xem Tài nguyên), Kent Beck sử dụng tiền tệ làm một ví dụ —
một sự minh họa khá tốt về TDD, nhưng hơi đơn giản thái quá. Thách thức thực sự là phải tìm ra
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 2 của 12
ibm.com/developerWorks/vn/
developerWorks®
một ví dụ không phức tạp đến mức mà bạn bị lạc lối trong lĩnh vực của bài toán nhưng đủ phức tạp
để cho thấy giá trị thực sự.
Vì mục đích ấy, tôi đã chọn các số hoàn hảo. Đối với những bạn không theo dõi chuyện tầm phào
toán học, khái niệm này có nguồn gốc từ trước Euclid (người đã thực hiện một trong các chứng
minh sớm nhất về việc tìm ra các số hoàn hảo). Một số hoàn hảo là một số mà bằng tổng của các
thừa số của nó. Ví dụ, 6 là một số hoàn hảo bởi vì các thừa số của 6 (trừ chính số 6) là 1, 2 và 3 và
1 + 2 + 3 = 6. Một định nghĩa nhiều tính thuật toán hơn cho một số hoàn hảo là một số mà tổng
các thừa số (trừ chính số đó) bằng chính số đó. Trong ví dụ của tôi, phép tính là 1 + 2 + 3 +6 - 6 =
6.
Và đây là lĩnh vực bài toán cần giải quyết: tạo ra một trình tìm kiếm số hoàn hảo. Tôi sẽ thực hiện
lời giải cho bài toán này theo hai cách khác nhau. Trước tiên, tôi sẽ tắt một phần của não bộ của
tôi muốn thực hiện TDD và chỉ viết giải pháp, sau đó viết các thử nghiệm cho nó. Rồi sau đó, tôi sẽ
phát triển một phiên bản TDD của giải pháp để tôi có thể so sánh và đối chiếu cả hai cách tiếp cận.
Đối với ví dụ này, tôi triển khai thực hiện một trình tìm kiếm một số hoàn hảo bằng ngôn ngữ Java
(phiên bản 5 hoặc mới hơn vì tôi sẽ sử dụng các chú thích trong thử nghiệm của mình), JUnit 4.x
(phiên bản mới nhất) và các trình phối hợp Hamcrest từ kho mã của Google (xem Tài nguyên).
Các trình phối hợp Hamcrest cung cấp một cú pháp theo cách giao tiếp của con người phủ bên
trên các trình phối hợp JUnit tiêu chuẩn. Ví dụ, thay cho assertEquals(expected, actual), bạn có
thể viết assertEquals(actual, is(expected)), đọc lên nghe giống với một câu nói đời thực hơn.
Các trình phối hợp Hamcrest có kèm theo với JUnit 4.x (chỉ cần dùng lệnh nhập khẩu (import) tĩnh);
nếu bạn vẫn còn sử dụng JUnit 3.x, bạn có thể tải về một phiên bản tương thích.
Thử nghiệm sau
Listing 1 hiển thị phiên bản đầu tiên của PerfectNumberFinder:
Listing 1. The test-after PerfectNumberFinder
public class PerfectNumberFinder1 {
public static boolean isPerfect(int number) {
// get factors
List<Integer> factors = new ArrayList<Integer>();
factors.add(1);
factors.add(number);
for (int i = 2; i < number; i++)
if (number % i == 0)
factors.add(i);
// sum factors
int sum = 0;
for (int n : factors)
sum += n;
// decide if it's perfect
return sum - number == number;
}
}
Đây không phải là mã đặc biệt đẹp, nhưng nó hoàn thành được công việc. Tôi bắt đầu bằng cách
liệt kê tất cả các thừa số dưới dạng một danh sách động (một ArrayList). Tôi thêm 1 và số đích
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 3 của 12
developerWorks®
ibm.com/developerWorks/vn/
vào danh sách. (Tôi tuân thủ công thức đã cho ở trên và liệt kê tất cả các thừa số, bao gồm số 1 và
chính số đó). Sau đó, tôi duyệt qua các thừa số cho đến khi gặp chính số đó, kiểm tra lần lượt từng
số để xem nó có phải một thừa số không. Nếu đúng, tôi thêm số đó vào danh sách. Tiếp theo, tôi
lấy tổng tất cả các thừa số và cuối cùng là viết một phiên bản Java của công thức đã chỉ ra ở trên
để xác định số hoàn hảo.
Bây giờ, tôi cần một thử nghiệm đơn vị theo cách thử nghiệm sau để xác định xem chương trình
có hoạt động đúng hay không. Tôi cần ít nhất hai thử nghiệm: một để xem báo cáo kết quả các số
hoàn hảo có đúng không và thử nghiệm kia sẽ kiểm tra để tôi không nhận được các xác thực sai.
Các thử nghiệm đơn vị có trong Listing 2:
Listing 2. Các thử nghiệm đơn vị cho PerfectNumberFinder
public class PerfectNumberFinderTest {
private static Integer[] PERFECT_NUMS = {6, 28, 496, 8128, 33550336};
@Test public void test_perfection() {
for (int i : PERFECT_NUMS)
assertTrue(PerfectNumberFinder1.isPerfect(i));
}
@Test public void test_non_perfection() {
List<Integer>expected = new ArrayList<Integer>(
Arrays.asList(PERFECT_NUMS));
for (int i = 2; i < 100000; i++) {
if (expected.contains(i))
assertTrue(PerfectNumberFinder1.isPerfect(i));
else
assertFalse(PerfectNumberFinder1.isPerfect(i));
}
}
@Test public void test_perfection_for_2nd_version() {
for (int i : PERFECT_NUMS)
assertTrue(PerfectNumberFinder2.isPerfect(i));
}
@Test public void test_non_perfection_for_2nd_version() {
List<Integer> expected = new ArrayList<Integer>(Arrays.asList(PERFECT_NUMS));
for (int i = 2; i < 100000; i++) {
if (expected.contains(i))
assertTrue(PerfectNumberFinder2.isPerfect(i));
else
assertFalse(PerfectNumberFinder2.isPerfect(i));
}
assertTrue(PerfectNumberFinder2.isPerfect(PERFECT_NUMS[4]));
}
}
Tại sao dùng "_" trong các tên thử nghiệm?
Đặt dấu gạch dưới trong các tên của phương thức khi viết các thử nghiệm đơn vị là một trong
những thói quen viết mã lệnh của tôi. Tất nhiên, tiêu chuẩn Java nói rõ rằng kiểu bướu lạc
đà mới là cách đúng đắn để viết các tên của phương thức. Nhưng tôi vẫn duy trì các tên của
phương thức thử nghiệm khác với các tên của phương thức bình thường. Các tên của phương
thức thử nghiệm cần cho biết phương thức đang thử nghiệm cái gì, và do đó chúng trở thành
các tên dài, diễn tả hoàn toàn chính xác những gì bạn muốn khi phân tách ra. Tuy nhiên,
việc đọc các tên dài theo kiểu “bướu lạc đà” là khó khăn, đặc biệt là trong một trình chạy thử
nghiệm đơn vị, nơi có hàng chục hoặc hàng trăm thử nghiệm xuất hiện, vì rất nhiều các tên
thử nghiệm bắt đầu giống nhau và chỉ khác nhau ở gần phía cuối. Trong tất cả các dự án mà
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 4 của 12
ibm.com/developerWorks/vn/
developerWorks®
tôi đã tiến hành, tôi ủng hộ mạnh mẽ việc sử dụng các dấu gạch dưới (chỉ dùng cho các tên
thử nghiệm) để làm cho chúng dễ đọc hơn.
Mã này cho kết quả đúng là các số hoàn hảo nhưng nó chạy rất chậm với thử nghiệm phủ định do
phải kiểm tra quá nhiều số. Các vấn đề hiệu suất có thể xuất hiện từ các thử nghiệm đơn vị đã đưa
tôi quay về với mã lệnh để xem xem tôi có thể thực hiện một số cải tiến không. Hiện tại, tôi duyệt
qua suốt vòng lặp cho đến khi gặp chính số đó để thu được các thừa số. Nhưng tôi có cần phải đi
xa như thế không? Không, nếu như tôi có thể thu hoạch các thừa số theo từng cặp. Tất cả các thừa
số đều có cặp (ví dụ, nếu số đích là số 28, khi tôi tìm thấy thừa số 2, tôi cũng có thể lấy luôn thừa số
14). Tôi chỉ cần đi tiếp lên tới căn bậc 2 của số đích là tôi có thể thu được các thừa số theo cặp. Vì
mục đích này, tôi cải tiến các thuật toán và cấu trúc lại mã lệnh cho Listing 3:
Listing 3. Phiên bản thuật toán đã cải tiến
public class PerfectNumberFinder2 {
public static boolean isPerfect(int number) {
// get factors
List<Integer> factors = new ArrayList<Integer>();
factors.add(1);
factors.add(number);
for (int i = 2; i <= sqrt(number); i++)
if (number % i == 0) {
factors.add(i);
factors.add(number / i);
}
// sum factors
int sum = 0;
for (int n : factors)
sum += n;
// decide if it's perfect
return sum - number == number;
}
}
Mã này chạy trong một thời gian khá dài nhưng một số kết quả thử nghiệm không thành công. Té
ra là khi bạn thu thập các thừa số theo các cặp, bạn vô tình lấy ra các số hai lần khi đạt đến căn bậc
hai của số đích. Ví dụ, đối với số 16, có căn bậc hai là 4, vô tình được thêm vào danh sách hai lần.
Điều này rất dễ dàng sửa chữa bằng cách tạo một điều kiện canh giữ trường hợp này, như được
hiển thị trong Listing 4:
Listing 4. Thuật toán cải tiến đã sửa lỗi
for (int i = 2; i <= sqrt(number); i++)
if (number % i == 0) {
factors.add(i);
if (number / i != i)
factors.add(number / i);
}
Bây giờ tôi có một phiên bản kiểm tra sau của trình tìm số hoàn hảo. Nó làm việc được nhưng có
một số vấn đề về thiết kế kéo theo. Trước tiên, tôi đã sử dụng các dòng chú thích phân tách các
phần của mã lệnh. Đây luôn luôn là hương vị của mã lệnh: nó là một tiếng kêu cứu để cấu trúc lại
thành các phương thức riêng. Cái mới mà tôi vừa thêm vào có lẽ cần một chú thích để giải thích
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 5 của 12
developerWorks®
ibm.com/developerWorks/vn/
những gì mà điều kiện canh giữ bé nhỏ ấy sẽ làm, nhưng bây giờ tôi sẽ để mặc thế đã. Vấn đề lớn
nhất nằm ở độ dài của nó. Nguyên tắc ngón tay cái của tôi đối với các dự án Java nói rằng không
nên có phương thức nào dài hơn 10 dòng mã. Nếu một phương thức vượt quá con số này, nó gần
như chắc chắn là làm nhiều hơn một điều mà nó không nên làm. Phương thức này rõ ràng vi phạm
nguyên tắc ấy, do đó tôi sẽ thử một cách khác, lần này sẽ sử dụng TDD.
Thiết kế nổi dần thông qua TDD
Câu thần chú Ấn độ dành cho viết mã TDD là: "Cái điều đơn giản nhất để tôi có thể viết một thử
nghiệm cho nó là gì ?". Trong trường hợp này, đó có phải là "là một số hoàn hảo hay là không?".
Không — câu trả lời là điều này quá rộng. Tôi phải phân rã bài toán và suy nghĩ "số hoàn hảo" có
nghĩa là gì. Tôi có thể dễ dàng đi đến kết quả là một số bước cần thiết để khám phá ra một số hoàn
hảo:
• Tôi cần các thừa số của số đang xét.
• Tôi cần phải xác định xem một số có phải là thừa số không.
• Tôi cần phải lấy tổng các thừa số.
Hướng theo ý tưởng tìm điều đơn giản nhất ấy, mục nào trong số các mục trong danh sách trên có
vẻ là mục đơn giản nhất ? Tôi nghĩ rằng đó là mục xác định xem một số có phải là thừa số của một
số khác không. Vậy đây là phép thử nghiệm đầu tiên của tôi, nó có trong Listing 5:
Listing 5. Kiểm tra xem "một số có phải là thừa số không?"
public class Classifier1Test {
@Test public void is_1_a_factor_of_10() {
assertTrue(Classifier1.isFactor(1, 10));
}
}
Phép kiểm tra đơn giản này là tầm thường đến mức ngớ ngẩn và nó chính là cái tôi muốn. Để thực
hiện thử nghiệm này, bạn phải có một lớp có tên là Classifier1, với một phương thức isFactor().
Vì vậy, tôi phải tạo ra một khung sườn cấu trúc của lớp này trước khi tôi thậm chí có thể nhận được
một thanh màu đỏ. Việc viết các thử nghiệm đơn vị tầm thường quá đỗi này cho phép bạn dựng lên
một kết cấu trước khi bạn cần bắt đầu suy nghĩ về lĩnh vực của bài toán theo một cách có ý nghĩa.
Tôi muốn suy nghĩ về chỉ một điều ở một thời điểm và điều này cho phép tôi tiếp tục làm việc trên
khung sườn cấu trúc mà không phải lo về các sắc thái của bài toán mà tôi đang giải quyết. Sau khi
biên dịch những thứ trên và thanh màu đỏ xuất hiện, tôi ở tư thế sẵn sàng để viết mã, hiển thị trong
Listing 6:
Listing 6. Lần đầu tiên thông qua thử nghiệm với phương thức thừa số
public class Classifier1 {
public static boolean isFactor(int factor, int number) {
return number % factor == 0;
}
}
Tốt rồi, thật đẹp và đơn giản, và nó làm được việc. Bây giờ tôi có thể chuyển sang nhiệm vụ đơn
giản nhất tiếp theo: nhận một danh sách các thừa số của một số. Thử nghiệm xuất hiện trong 7:
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 6 của 12
ibm.com/developerWorks/vn/
developerWorks®
Listing 7. Thử nghiệm tiếp theo: Các thừa số của một số đã cho
@Test public void factors_for() {
int[] expected = new int[] {1};
assertThat(Classifier1.factorsFor(1), is(expected));
}
Listing 7 chứa thử nghiệm đơn giản nhất mà tôi phải cố gắng làm để nhận được các thừa số, vì thế
bây giờ tôi có thể viết mã lệnh đơn giản nhất để thông qua được thử nghiệm này (và cấu trúc lại nó
sau này để làm cho nó tinh tế hơn). Phương thức tiếp theo xuất hiện trong Listing 8:
Listing 8. Phương thức đơn giản factorsFor()
public static int[] factorsFor(int number) {
return new int[] {number};
}
Mặc dù phương thức này làm việc đúng, nó giữ tôi tạm dừng trên đường đi. Có vẻ như để cho
isFactor() thành phương thức tĩnh (static) là một ý tưởng tốt, bởi vì nó chỉ trả về kết quả dựa trên
đầu vào của nó. Tuy nhiên, bây giờ tôi cũng đã để cho factorsFor() là phương thức tĩnh, có nghĩa
là tôi phải chuyển một tham số được gọi là number cho cả hai phương thức. Mã lệnh này trở thành
quá thủ tục, đó là hậu quả phụ của việc lạm dụng phương thức tĩnh. Để sửa chữa điều này, tôi sẽ cấu
trúc lại hai phương thức mà tôi đã có, đây là việc đơn giản là vì cho đến nay mới chỉ có một ít mã
như vậy. Lớp Classifier đã cấu trúc lại xuất hiện trong Listing 9:
Listing 9. Lớp Classifier đã cải tiến
public class Classifier2 {
private int _number;
public Classifier2(int number) {
_number = number;
}
public boolean isFactor(int factor) {
return _number % factor == 0;
}
}
Tôi đã làm cho number thành một biến thành viên trong lớp Classifier2, điều này cho phép tôi
tránh được việc chuyển đi chuyển lại nó như một tham số tới một bó các phương thức tĩnh.
Mục tiếp theo trong danh sách phân rã ở trên nói rằng tôi cần phải tìm ra các thừa số của một số. Vì
vậy, thử nghiệm tiếp theo của tôi cần kiểm tra điều này (hiển thị trong Listing 10):
Listing 10. Thử nghiệm tiếp theo: Các thừa số của một số
@Test public void factors_for_6() {
int[] expected = new int[] {1, 2, 3, 6};
Classifier2 c = new Classifier2(6);
assertThat(c.getFactors(), is(expected));
}
Bây giờ, tôi sẽ thử triển khai thực hiện phương thức trả về một mảng các thừa số của một tham số
đã cho, hiển thị trong 11:
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 7 của 12
developerWorks®
ibm.com/developerWorks/vn/
Listing 11. Lần đầu tiên thông qua thử nghiệm với phương thức getFactors()
public int[] getFactors() {
List<Integer> factors = new ArrayList<Integer>();
factors.add(1);
factors.add(_number);
for (int i = 2; i < _number; i++) {
if (isFactor(i))
factors.add(i);
}
int[] intListOfFactors = new int[factors.size()];
int i = 0;
for (Integer f : factors)
intListOfFactors[i++] = f.intValue();
return intListOfFactors;
}
Mã này cho phép vượt qua thử nghiệm, nhưng khi suy nghĩ lại, thật dễ sợ! Điều này đôi lúc xảy ra
khi bạn điều tra tỷ mỉ cách triển khai thực hiện mã lệnh bằng cách sử dụng các thử nghiệm. Có
gì khủng khiếp như vậy về các mã này? Trước hết, nó rất dài và phức tạp và nó cũng mắc nhược
điểm là vấn đề "làm nhiều hơn một thứ". Bản năng của tôi đã dẫn tôi trở lại với việc dùng một mảng
int[], nhưng nó sẽ tăng thêm khá nhiều sự phức tạp vào mã lệnh ở dưới cùng và không đạt được
bất cứ thứ gì cho tôi. Đó là một con đường dốc trơn trượt khó đi khi bắt đầu suy nghĩ quá nhiều
về việc làm cho mọi thứ thuận tiện hơn dành cho các phương thức tương lai mà có thể gọi phương
thức này. Bạn cần phải có một lý do có sức thuyết phục để thêm một cái gì đó phức tạp như thế vào
mối nối này và tôi còn chưa có một sự biện hộ nào cho việc này. Việc xem xét kỹ mã này gợi ý rằng
có lẽ các thừa sừ cũng nên tồn tại như một trạng thái bên trong của lớp, cho phép tôi tách riêng ra
phần chức năng của phương thức này.
Một trong những đặc điểm có ích mà các thử nghiệm làm nổi lên là các phương thức thực sự kết
dính. Kent Beck đã viết về điều này trong một cuốn sách có ảnh hưởng tên là Các mẫu thực tiễn
tốt nhất của Smalltalk (Smalltalk Best Practice Patterns ) (xem Tài nguyên). Trong cuốn sách đó,
Kent đã định nghĩa một mẫu được gọi là phương thức cấu thành (composed method). Mẫu phương
thức cấu thành định nghĩa ba khẳng định then chốt:
• Chia chương trình của bạn thành các phương thức thực hiện một công việc có thể nhận biết
được.
• Giữ cho tất cả các phép toán trong một phương thức có cùng một mức độ trừu tượng hóa.
• Điều này sẽ tự nhiên dẫn đến các chương trình với nhiều phương thức nhỏ, mỗi phương thức
có độ dài vài dòng.
Phương thức cấu thành là một trong những đặc điểm thiết kế có ích mà TDD khuyến khích và tôi
đã vi phạm rõ ràng mẫu này trong phương thức getFactors() ở Listing 11. Tôi có thể sửa chữa nó
bằng cách thực hiện các bước sau:
1. Nâng các thừa sừ lên thành trạng thái bên trong.
2. Di chuyển đoạn mã khởi tạo các thừa sừ vào hàm tạo.
3. Loại bỏ đoạn mã mạ vàng nhằm chuyển đổi kết quả thành mảng int[] và xử lý nó sau nếu
điều này là có ích.
4. Thêm một thử nghiệm khác cho addFactors().
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 8 của 12
ibm.com/developerWorks/vn/
developerWorks®
Bước thứ tư là khá tế nhị, nhưng quan trọng. Việc viết phiên bản mã có lỗi này đã để lộ ra rằng
vòng phân rã đầu tiên của tôi đã không đầy đủ. Dòng mã addFactors() giấu vào giữa phương thức
dài này là hành vi thử nghiệm được. Nó tầm thường đến mức mà tôi đã không nhận thấy điều này
khi lần đầu tiên xem xét bài toán, nhưng bây giờ tôi đã thấy rồi. Điều này thường xuyên xảy ra. Một
thử nghiệm có thể dẫn bạn đến phân rã tiếp tục bài toán thành các đoạn ngày càng nhỏ hơn, mỗi
đoạn đều có thể thử nghiệm.
Tôi sẽ tạm dừng bài toán lớn hơn về getFactors() vào lúc này và giải quyết bài toán mới nhỏ nhất
của tôi. Như vậy, thử nghiệm tiếp theo của tôi là addFactors(), được hiển thị trong Listing 12:
Listing 12. Thử nghiệm với addFactors()
@Test public void add_factors() {
Classifier3 c = new Classifier3(6);
c.addFactor(2);
c.addFactor(3);
assertThat(c.getFactors(), is(Arrays.asList(1, 2, 3, 6)));
}
Đoạn mã cần thử nghiệm, được hiển thị trong Listing 13, là rất đơn giản:
Listing 13. Mã lệnh đơn giản để cộng các thừa số
public void addFactor(int factor) {
_factors.add(factor);
}
Tôi chạy thử nghiệm đơn vị của tôi, hoàn toàn tin tưởng rằng tôi sẽ thấy một thanh màu xanh lục,
nhưng nó thất bại! Làm thế nào mà một thử nghiệm đơn giản đến như vậy lại thất bại? Nguyên
nhân gốc rễ xuất hiện trong Hình 2:
Hình 2. Nguyên nhân gốc rễ của thử nghiệm không thành công
Danh sách mà tôi mong đợi có các giá trị 1, 2, 3, 6 nhưng thực tế trả về là 1, 6, 2, 3. Ôi, đó là
vì tôi đã thay đổi mã để thêm 1 và chính số đích vào hàm tạo. Một giải pháp cho vấn đề này sẽ là
luôn viết như tôi mong muốn, giả sử rằng số 1 và chính số đích luôn luôn được viết trước hết. Nhưng
đó có phải là giải pháp đúng không? Không. Vấn đề ở chỗ căn bản hơn nhiều. Các thừa số có phải
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 9 của 12
developerWorks®
ibm.com/developerWorks/vn/
là một danh sách các số không? Không, chúng là một tập hợp các số. Giả thiết đầu tiên (không
đúng) của tôi dẫn đến việc sử dụng một danh sách các số nguyên dành cho các thừa số, nhưng đó
là một phép trừu tượng hóa tồi. Bằng việc cấu trúc lại mã lệnh, bây giờ tôi sử dụng các tập hợp thay
vì các danh sách, tôi không chỉ khắc phục được vấn đề này mà còn làm cho giải pháp tổng thể trở
nên tốt hơn vì bây giờ tôi đang sử dụng phép trừu tượng hóa chính xác hơn.
Đây đúng là một kiểu suy nghĩ thiếu sót khi cho rằng các thử nghiệm có thể phơi bày ra, có phải
bạn viết các thử nghiệm trước khi bạn viết mã để che giấu việc phán xét bạn. Bây giờ, nhờ thử
nghiệm đơn giản này, toàn bộ thiết kế mã lệnh của tôi thành tốt hơn vì tôi đã phát hiện một cách
trừu tượng hóa thích hợp hơn.
Kết luận
Cho đến nay, tôi đã thảo luận về thiết kế nổi dần trong bối cảnh của bài toán số hoàn hảo. Nói riêng,
lưu ý rằng phiên bản đầu tiên của giải pháp (phiên bản kiểm tra sau) đã phạm cùng một giả thiết sai
lầm về các kiểu dữ liệu. "Thử nghiệm sau" kiểm tra các chức năng của mã của bạn ở mức chi tiết
thô, chứ không phải ở mức các phần riêng biệt. TDD kiểm tra các khối nền tảng làm nên chức năng
ở mức chi tiết thô ấy, phơi bày ra nhiều thông tin hơn trong quá trình làm việc.
Trong bài viết tiếp theo, tôi sẽ tiếp tục bài toán số hoàn hảo, minh họa nhiều ví dụ về các loại thiết
kế có thể xuất hiện nếu bạn để lộ ra cách thức của các thử nghiệm của bạn. Khi tôi có phiên bản
TDD đầy đủ, tôi sẽ so sánh một vài số liệu thống kê giữa hai cơ sở mã. Tôi cũng sẽ xử lý một số câu
hỏi thiết kế khó khăn khác về TDD, ví dụ như có hay không và khi nào thì thử nghiệm các phương
thức riêng.
Kiến trúc tiến hóa và thiết kế nổi dần: Thiết kế dựa theo thử
nghiệm, Phần 1
Trang 10 của 12
ibm.com/developerWorks/vn/
developerWorks®
Tài nguyên
Học tập
• Hamcrest matchers: Một thư viện các trình phối hợp đối tượng cho phép bạn định nghĩa thế
nào là "khớp" để sử dụng trong các khung công tác khác.
• Test-Driven Development (Kent Beck, Addison-Wesley, 2003): Beck, nhà sáng lập Lập trình
đỉnh cao, sử dụng các ví dụ dựa trên tiền tệ để giải thích TDD.
• Smalltalk Best Practice Patterns (Kent Beck, Prentice Hall, 1996): Tìm hiểu thêm về mẫu
phương thức cấu thành.
• The Productive Programmer (Neal Ford, O'Reilly Media, 2008): Một phiên bản dài hơn của ví
dụ trong bài viết này xuất hiện trong chương "Phát triển dựa vào thử nghiệm” trong cuốn sách
gần đây nhất của Neal Ford.
• "Emergent Optimization in Test Driven Design" (Michael Feathers): Thử nghiệm giúp tránh
được sự tối ưu hóa quá sớm như thế nào.
• Duyệt qua technology bookstore để tìm các sách về chủ đề kỹ thuật này và các chủ đề kỹ
thuật khác.
• developerWorks Java technology zone: 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ệ
• JUnit: Tải về JUnit.
Thảo luận
• Xem 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ế dựa theo thử
nghiệm, Phần 1
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ế dựa theo thử
nghiệm, Phần 1
Trang 12 của 12