Diễn Đàn Tin Học » Tutorial Room » Lập trình » Software Engineering » Craftsman-Truyện dài nhiều tập Craftsman 7 - Socket Service2 Alphonse và Jerry thử truy cập nhiều lần Socket Service. Hãy cùng xem để biết kết quả có được như ý của họ không nhé ...
Chào các SE fans, xin gởi đến các fans phần kế tiếp của bộ "truyện dài nhiều
tập" craftsman7.
SocketService2
Robert C. Martin
Ngày 15 tháng 10 2002
Lần trước Alphonse và Jerry khởi đầu trên một framework java đơn giản hỗ trợ
dịch vụ socket. Test case thứ nhất của họ vạch ra trường hợp dồn đuổi (race
condition) mà họ đã giải quyết ổn thoả. Chuỗi unit test hiện tại được dẫn ở
mã dẫn 1 và mã nguồn chính ở mã dẫn 2.
Mã dẫn 1
| |
import
junit.framework.TestCase;
import junit.swingui.TestRunner;
import java.io.IOException;
import java.net.Socket;
public class TestSocketServer extends TestCase {
public static void main(String[]
args) {
TestRunner.main(new String[]{"TestSocketServer"});
}
public TestSocketServer(String name) {
super(name);
}
public void testOneConnection() throws Exception {
SocketService ss = new SocketService();
ss.serve(999);
connect(999);
ss.close();
assertEquals(1,
ss.connections());
}
private void connect(int
port) {
try {
Socket s =
new Socket("localhost", port);
try {
Thread.sleep(100);
} catch
(InterruptedException e) {
}
s.close();
} catch (IOException e) {
fail("could not
connect");
}
}
} |
|
Mã dẫn 2
| |
import
java.io.IOException;
import java.net.*;
public class SocketService {
private ServerSocket serverSocket =
null;
private int connections
= 0;
private Thread serverThread = null;
public void serve(int
port) throws Exception {
serverSocket = new ServerSocket(port);
serverThread = new Thread(
new Runnable() {
public void run() {
try {
Socket s = serverSocket.accept();
s.close();
connections++;
} catch
(IOException e) {
}
}
}
);
serverThread.start();
}
public void close() throws Exception {
serverSocket.close();
}
public int connections() {
return connections;
}
} |
|
Sau giờ giải lao, chúng tôi trở lại và sẵn sàng tiếp túc với SocketService.
"Chúng ta đã chứng minh được mình có thể truy cập một lần. Vậy hãy thử truy cập
nhiều lần xem sao." Jerry nói.
"Nghe được lắm." Tôi trả lời. Sau đó tôi viết cái test case như sau:
| |
public void testManyConnections()
throws Exception {
SocketService ss = new SocketService();
ss.serve(999);
for (int i = 0; i < 10; i++)
connect(999);
ss.close();
assertEquals(10, ss.connections());
} |
|
"OK, cái test này hỏng." Tôi nói.
"Y như ước đoán". Jerry đáp. "Cái SocketService chỉ gọi method accept một
lần. Chúng ta cần đặt bước gọi đó vào một vòng lặp."
"Khi nào vòng lặp đó chấm dứt?" Tôi hỏi.
Jerry nghĩ ngợi 1 tí và nói: "Khi chúng ta gọi method close của
SocketService."
"Như thế này chăng?" Và tôi hiệu đính như sau:
| |
public class SocketService {
private ServerSocket serverSocket =
null;
private int connections =
0;
private Thread serverThread = null;
private boolean running = false;
public void serve(int
port) throws Exception {
serverSocket = new ServerSocket(port);
serverThread = new Thread(
new Runnable() {
public void
run() {
running = true;
while (running) {
try {
Socket s = serverSocket.accept();
s.close();
connections++;
} catch (IOException e) {
}
}
}
}
);
serverThread.start();
}
public void close() throws Exception {
running = false;
serverSocket.close();
}
} |
|
Tôi chạy cái test và cả hai đều đạt.
"Tốt." Tôi nói. "Bây giờ chúng ta có thể truy cập bao nhiêu tùy thích. Không may
cái SocketService chẳng làm gì nhiều khi mình truy cập đến nó. Nó chỉ
đóng lại mà thôi."
"Ừa, đổi nó đi." Jerry nói. "Mình hãy buộc SocketService gởi thông điệp
"Hello" mỗi khi chúng ta truy cập đến nó."
Tôi không cần chuyện này cho lắm. Tôi nói: "Tại sao mình làm bẩn cái
SocketService bằng thông điệp "Hello" chỉ để thoả mãn cái test của mình?
SocketService có thể gởi thông điệp thì tốt nhưng mình không muốn thông điệp
này là một phần của mã nguồn SocketService!"
"Ðúng thế!" Jerry đồng ý. "Mình muốn thông điệp được chỉ định và xác thực do cái
test."
"Mình làm sao đây?" Tôi hỏi.
Jerry mỉm cười đáp: "Chúng ta dùng cái Mock Object pattern. Nói một cách
ngắn gọn, mình tạo ra một cái interface từ đó SocketService sẽ thao tác
sau khi nhận một truy cập. Chúng ta sẽ có cái test ứng dụng cái interface đó
dùng để gởi thông điệp "Hello". Sau đó, mình sẽ có cái test dùng để đọc thông
điệp từ socket của client và xác thực thông tin được gởi đi một cách đúng đắn."
Tôi chẳng biết Mock Object pattern là gì cả và thành phần
interface của gã làm tôi bối rối. "Ông chỉ cho tôi được không?" Tôi hỏi.
Thế rồi Jerry vớ lấy bàn đánh và bắt đầu gõ.
"Ðầu tiên chúng ta viết cái test."
| |
public void testSendMessage() throws
Exception {
SocketService ss = new SocketService();
ss.serve(999, new HelloServer());
Socket s = new Socket("localhost", 999);
InputStream is = s.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String answer = br.readLine();
s.close();
assertEquals("Hello", answer);
} |
|
Tôi kiểm tra đoạn mã này cẩn thận. "OK, ông tạo ra cái gọi là HelloServer và
đưa nó vào trong method serve. Cái này sẽ làm hỏng hết các cái test
khác!"
"Hay lắm!" Jerry thốt lên. "Ðiều đó có nghĩa là chúng ta cần refactor những test
khác trước khi tiếp tục."
"Nhưng các dịch vụ trong hai cái test kia chẳng làm gì hết." Tôi ý kiến.
"Tất nhiên là chúng làm việc - chúng đếm số truy cập! Mày có nhớ là mày ghét mấy
cái biến số truy cập đến thế nào không, và nó chỉ là phần phụ mà thôi? Bây giờ
mình sẽ dẹp chúng đi."
"Mình sắp sửa làm thế à?"
"Xem đây." Jerry cười rộ. "Ðầu tiên chúng ta đổi hai cái test và thêm biến số
connections vào test case."
| |
public void testOneConnection() throws Exception {
ss.serve(999,
connectionCounter);
connect(999);
assertEquals(1,
connections);
}
public void testManyConnections() throws Exception {
ss.serve(999,
connectionCounter);
for (int i=0; i<10; i++)
connect(999);
assertEquals(10,
connections);
} |
|
"Kế tiếp mình tạo cái interface."
| |
import java.net.Socket;
public interface SocketServer {
public void serve(Socket s);
} |
|
"Sau đó chúng ta tạo biến số connectionCounter và khởi động nó
trong constructor của TestSocketServer bằng một anonymous inner class để
nó tăng cấp biến số connections.
| |
public class TestSocketServer extends TestCase {
private int connections = 0;
private SocketServer connectionCounter;
public static void main(String[]
args) {
TestRunner.main(new String[]{"TestSocketServer"});
}
public TestSocketServer(String name) {
super(name);
connectionCounter = new
SocketServer() {
public void serve(Socket s) {
connections++;
}
};
} |
|
...
"Cuối cùng, chúng ta cho nó biên dịch trọn bộ bằng cách thêm đối số phụ vào
method serve của SocketService và biến cái test mới thành phần chú
giải (để nó khỏi chạy)."
| |
public void serve(int
port, SocketServer server) throws
Exception {
...
} |
|
"OK, tôi biết ý ông rồi." Tôi nói. "Hai cái test cũ lúc này hẳn phải hỏng bởi lẽ
SocketService không bao giờ gây ra method serve từ đối số
SocketServer của nó."
Tất nhiên các cái test đã hỏng vì chính lý do ấy.
Tôi biết phải làm gì kế tiếp. Tôi vớ lấy bàn đánh và thay đổi như sau:
| |
public class SocketService {
private ServerSocket serverSocket =
null;
private int connections =
0;
private Thread serverThread = null;
private boolean running = false;
private SocketServer itsServer;
public void serve(int
port, SocketServer server) throws Exception {
itsServer = server;
serverSocket = new ServerSocket(port);
serverThread = new Thread(
new Runnable() {
public void run() {
running =
true;
while
(running) {
try {
Socket s = serverSocket.accept();
itsServer.serve(s);
s.close();
connections++;
} catch (IOException e) {
}
}
}
}
);
serverThread.start();
}
...
|
|
Ðoạn mã này làm các test chạy được.
"Hay lắm!" Jerry nói. "Bây giờ chúng ta phải làm cho cái test mới chạy."
Thế nên tôi tháo bỏ phần chú giải cho đoạn test và biên dịch nó. Nó "la làng"
trong phần HelloServer.
"Ô, đúng rồi. Mình phải thực thi cái HelloServer. Nó sẽ phun ra chữ
"hello" từ socket, phải không?"
"Ðúng thế." Jerry xác nhận.
Thế rồi tôi viết cái class mới trong hồ sơ TestSocketServer.java như sau
| |
class HelloServer
implements SocketServer {
public void serve(Socket s) {
try {
PrintStream ps = new
PrintStream(s.getOutputStream());
ps.println("Hello");
} catch (IOException e) {
}
}
} |
|
Các test đều ổn.
"Cũng dễ thôi." Jerry nói.
"Ừa. Phần pattern Mock Oject khá hữu dụng. Nó cho phép ta duy trì các mã
dùng để test trong kế hoạch test. SocketService không biết gì cả."
"Còn hữu dụng hơn thế." Jerry trả lời. "Các servers thật cũng sẽ ứng dụng
interface SocketServer."
"Tôi biết." Tôi trả lời. "Thật lý thú khi thấy từ nhu cầu tạo ra một unit test
đưa mình đến chỗ tạo ra một đồ hình hữu dụng một cách tổng quát."
"Ðiều này thường xảy ra mà." Jerry nói. "Tests là người dùng đó. Nhu cầu dùng
tests thường trùng hợp với nhu cầu của người dùng thật sự."
"Nhưng tại sao lại gọi nó là Mock Object?"
"Hãy nghĩ trên phương diện thế này. HelloServer dùng để thay thế cho,
hoặc là một bản nháp, của một server thật. Cái pattern này cho phép chúng ta
thay thế bản nháp của chuyện test vào mã nguồn ứng dụng thật sự."
"À ra vậy." Tôi đáp. "Thôi thì bây giờ mình nên dọn dẹp phần mã này và xoá bỏ
cái biến số truy cập vô dụng kia vậy."
"Ðồng ý."
Thế rồi chúng tôi dọn dẹp thêm một chút nữa và nghỉ giải lao. Kết quả của
SocketService như sau:
| |
import java.io.IOException;
import java.net.*;
public class SocketService {
private ServerSocket serverSocket =
null;
private Thread serverThread = null;
private boolean running = false;
private SocketServer itsServer;
public void serve(int
port, SocketServer server) throws Exception {
itsServer = server;
serverSocket = new ServerSocket(port);
serverThread =
makeServerThread();
serverThread.start();
}
private Thread makeServerThread() {
return new Thread (
new Runnable() {
public void run()
{
running =
true;
while
(running) {
acceptAndServeConnection();
}
}
}
);
}
private void
acceptAndServeConnection() {
try {
Socket s =
serverSocket.accept();
itsServer.serve(s);
s.close();
} catch (IOException e) {
}
}
public void close() throws Exception {
running = false;
serverSocket.close();
}
} |
|
<đón xem phần kế tiếp>
|