Center
of Excellence
Bài
5
Tổ
chức giải bóng đá
Thượng đế hiện diện trong
những tiểu tiết
-- Ludwig mies van der Rohe
I. Bài
Toán
Hãy phát triển code
nhận từ bàn phím số cầu thủ qui định cho từng đội
bóng và tổng số cầu thủ tham dự giải đấu, trong đó
số cầu thủ mỗi đội phải từ 9 đến 15 người, và
tổng số cầu thủ tham dự phải là dương. Code sẽ cho
biết kết quả về số đội bóng mà liên đoàn có thể
tổ chức giải đấu và số cầu thủ bị dôi ra.
Bạn
không phải phát triển phần UI, nhưng để hiểu rõ hơn
yêu cầu bài toán, có thể tham khảo một UI mẫu dưới
đây.
Hình 1
II. Giải
pháp
Theo yêu cầu bài
toán, ta cần phải thực hiện bốn nhiệm vụ, trong đó
có hai nhiệm vụ về nhập data và hai nhiệm vụ liên quan
đến tính toán.
- Nhập số cầu thủ mỗi đội (phải từ 9 đến 15)
- Input: từ bàn phím (keyboard)
- Output: số cầu thủ mỗi đội (number of players per team)
- Nhập số cầu thủ tham dự (phải là số nguyên dương)
- Input: từ keyboard
- Output: số cầu thủ tham dự (number of players)
- Tính số đội bóng (number of teams)
- Input: number of players per team, number of players
- Output: number of teams
- Tính số cầu thủ bị dôi ra (number of left over players)
- Input: number of players per team, number of players
- Output: number of left over players
Ta sẽ từng bước
giải quyết bốn nhiệm vụ này.
1.
Nhập number of players per team
1.1.
Thiết kế sơ bộ
Ta
thiết kế một class phục vụ cho việc nhập data, đặt
tên là Input.
Hình 2
Tuy
nhiên, theo kinh nghiệm lập trình, ta thường né tránh việc
sử dụng hằng số (constant) trong tên gọi nhằm làm cho
method linh hoạt hơn. Ở đây có hai constants là 9 và 15. Ta
sẽ thay chúng bằng hai parameters, một parameter gọi là giá
trị chặn dưới (lower bound), một gọi là chặn trên
(upper bound). Sau đó ta thử chuyển parameters thành fields.
Hình 3
Rõ
ràng là readPlayersPerTeams()
method linh hoạt hơn readPlayersPerTeamFrom9To15()
vì khi đó lowerBound
và upperBound
không nhất thiết
phải là 9 và
15 nữa, mà có thể là những giá trị khác, chẳng hạn
11 và 15.
Ngoài
ra, do Input class là bộ phận tương tác với người
dùng (user), ta đặt nó vào view package. View
(hay presentation) là từ thường dùng để biểu thị
bộ phận tương tác với user từ chương trình.
Hình 4
1.2.
Test cases
readPlayersPerTeam()
|
||
No
|
input từ keyboard
|
playersPerTeams?
|
1
|
8
|
không chấp nhận
|
2
|
9
|
9
|
3
|
10
|
10
|
4
|
15
|
15
|
5
|
16
|
không chấp nhận
|
1.3.
Phát triển test class
- Project: Soccer
package view;
import org.junit.Test;
import static org.junit.Assert.*;
public class InputTest {
@Test
public void readPlayersPerTeam() {
???
}
}
Biết rằng số nguyên mà người dùng nhập vào phải từ
9 đến 15, nhưng ta không biết chính xác là bao nhiêu, nên
ta chỉ có thể viết
public class InputTest {
@Test
public void readPlayersPerTeam() {
int playersPerTeam = new Input(9, 15).readNPlayersPerTeam();
assertTrue(9 <= playersPerTeam && playersPerTeam <= 15);
}
}
Lưu ý rằng đoạn code trên đã lặp lại các hằng số 9
và 15 hai lần. Tương tự như code duplication, vấn đề
trùng lặp dữ liệu (data duplication) là không tốt bởi vì
sau này nếu phải thay đối giá trị, chẳng hạn từ 9
sang 11, ta phải tìm khắp nơi trong code xem chỗ nào có số
9 thì phải sửa lại là 11. Nếu chỉ cần sót một chỗ
là chương trình sẽ bị lỗi. Để loại bỏ rủi ro của
data duplication, ta sẽ đặt tên cho các constants. Sau này,
nếu phải thay đổi giá trị của một constant nào đó,
ta chỉ cần sửa code ở một chỗ mà thôi.
public class InputTest {
@Test
public void readPlayersPerTeam() {
final int LOWER_BOUND = 9; // local constant – hằng cục bộ
final int UPPER_BOUND = 15;
int playersPerTeam = new Input(LOWER_BOUND, UPPER_BOUND).readPlayersPerTeam();
assertTrue(LOWER_BOUND <= playersPerTeam && playersPerTeam <= UPPER_BOUND);
}
}
Từ InputTest, ta tiến hành xây dựng Input và
readPlayersPerTeams().
package view;
public class Input {
private int lowerBound;
private int upperBound;
public Input(int lowerBound, int upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public int readPlayersPerTeam() {
???
}
}
Bây giờ ta tập trung phát triển readPlayersPerTeam().
Đầu tiên ta phát ra một lời nhắc (prompt) để người
dùng biết phải nhập data vào, sau đó tiến hành nhập
một số nguyên, rồi kiểm tra số nguyên đó, nếu đạt
yêu cầu thì trả về kết quả, ngược lại thì yêu cầu
người dùng nhập lại.
public int readPlayersPerTeam() {
// Nhắc người dùng nhập data
// Nhập vào một số nguyên
// Nếu số nguyên thỏa yêu cầu thì
// trả về kết quả
// Ngược lại thì
// yêu cầu người dùng nhập lại
}
Ta từng bước thay các dòng chú thích (comment) viết bằng
ngôn ngữ tự nhiên ở trên bằng ngôn ngữ Java. Trước
hết là đưa ra lời nhắc.
public int readPlayersPerTeam() {
System.out.print("Number of players per team [" + this.lowerBound +
", " + this.upperBound + "]: ");
// Nhập vào một số nguyên
// Nếu số nguyên thỏa yêu cầu thì
// trả về kết quả
// Ngược lại thì
// yêu cầu người dùng nhập lại
}
Để nhập vào một số nguyên, ta cần tạo Scanner
object rồi gọi nextInt().
package view;
import java.util.Scanner;
public class Input {
private int lowerBound;
private int upperBound;
public Input(int lowerBound, int upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public int readPlayersPerTeam() {
System.out.print("Number of players per team [" + this.lowerBound +
", " + this.upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
// Nếu số nguyên thỏa yêu cầu thì
// trả về kết quả
// Ngược lại thì
// yêu cầu người dùng nhập lại
}
}
Kế đến là kiểm tra số nguyên vừa được nhập vào có
thỏa yêu cầu hay không.
public int readPlayersPerTeam() {
System.out.print("Number of players per team [" + this.lowerBound +
", " + this.upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
if (lowerBound <= n && n <= upperBound)
return n;
else
// yêu cầu người dùng nhập lại
}
Nếu user nhập vào một số nguyên không đạt, ta sẽ yêu
cầu nhập lại. Nói cách khác, ta thi hành lại từ đầu
readPlayersPerTeam(). Ta viết
public int readPlayersPerTeam() {
System.out.print("Number of players per team [" + this.lowerBound +
", " + this.upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
if (lowerBound <= n && n <= upperBound)
return n;
else
return this.readPlayersPerTeam();
}
Để ý ở đoạn code trên, bên trong readPlayersPerTeam()
method, dòng cuối, ta đã gọi thi hành bản thân
readPlayersPerTeam(). Kỹ thuật này gọi là đệ
qui (recursion).
Bây
giờ nếu thi hành InputTest rồi nhập vào số nguyên,
ta sẽ được tín hiệu màu xanh. Song nếu cắc cớ, thay
vì nhập vào một số nguyên, ta lại nhập vào một số
thực hay một chuỗi ký tự (String), chương trình sẽ
tung InputMismatchException và cho tín hiệu đỏ. Để
giải quyết tình huống này, ta buộc phải xử lý
exception bằng khối try-catch như sau
import java.util.InputMismatchException;
import java.util.Scanner;
public class Input {
private int lowerBound;
private int upperBound;
public Input(int lowerBound, int upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public int readPlayersPerTeam() {
System.out.print("Number of players per team [" + this.lowerBound +
", " + this.upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
try {
int n = scanner.nextInt();
if (this.lowerBound <= n && n <= this.upperBound)
return n;
} catch (InputMismatchException e) {}
return this.readPlayersPerTeam();
}
}
ReadPlayersPerTeam() method ở trên đã yêu cầu người
dùng nhập số liệu từ bàn phím thông qua cái gọi là
console. Console chẳng qua là một môi trường đơn giản
tương tác với người dùng. Ngoài console, còn có một môi
trường tương tác thân thiện hơn gọi là Giao diện Người
dùng Đồ họa (Graphical User Interface), viết tắt là GUI.
Dưới đây là một phương án dùng GUI.
Hình 5
package view;
import java.util.InputMismatchException;
import java.util.Scanner;
import javax.swing.JOptionPane;
public class Input {
private int lowerBound;
private int upperBound;
public Input(int lowerBound, int upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public int readPlayersPerTeamGui() {
try {
String s = JOptionPane.showInputDialog(
"Enter the number of players per team in [" +
this.lowerBound + ", " + this.upperBound + "]: ");
int n = Integer.parseInt(s);
if (this.lowerBound <= n && n <= this.upperBound)
return n;
} catch (NumberFormatException e) {}
return this.readPlayersPerTeamGui();
}
public int readPlayersPerTeam() {
System.out.print("Number of players per team [" + this.lowerBound +
", " + this.upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
try {
int n = scanner.nextInt();
if (this.lowerBound <= n && n <= this.upperBound)
return n;
} catch (InputMismatchException e) {}
return this.readPlayersPerTeam();
}
}
Trong readPlayersPerTeamGui(), ta đã gọi thi hành
showInputDialog() của JOptionPane class. Khi đó
Java sẽ hiển thị một hộp thoại (dialog) để user nhập
data. Lưu ý rằng cho dù user nhập vào một số, kết quả
trả về luôn là một String. Để chuyển một số
thuộc kiểu String về kiểu nguyên int, ta dùng
phương thức tĩnh (static method) tên là parseInt() của
Integer class. Nếu quá trình chuyển đổi thất bại,
tức String đó thật sự không phải là một số, ta
sẽ nhận được NumberFormatException.
2.
Nhập number of players
2.1.
Thiết kế sơ bộ
Do
đây cũng là thao tác nhập nên ta thử đặt method này vào
Input class.
Hình 6
Tuy
nhiên thao tác đặt để đơn thuần như vậy là không phù
hợp vì readPlayersGui() hoàn toàn không dùng đến
fields. Khi quyết định khai báo một field, ta cần đảm
bảo rằng field đó có ý nghĩa nhất định đối với tất
cả các methods bên trong class.. Để giải quyết tình huống
này, ta chuyển fields về lại parameters cho
readPlayersPerTeam() và readPlayersPerTeamGui().
Hình 7
Đến
đây ta thấy rằng Input
class không còn field nào cả, vì vậy ta có thể biến tất
cả methods thành static methods.Static
method khác với method bình thường ở chỗ: khi thi hành
một static method, ta không cần tạo object, mà dùng trực
tiếp tên class. Chẳng hạn, để thi hành readPlayersGui()
static method, ta chỉ cần viết Input.readPlayersGui(),
thay vì phải viết new Input().readPlayersGui().
Static method còn được gọi là class method, tức method
thuộc class; và method bình thường còn được gọi là
object method, tức method thuộc object. Trong OOP, ta chỉ sử
dụng static methods trong những tình huống thích hợp, để
code thật sự có tính OO.
Hình 8
2.2.
Test cases
readPlayers()
|
||
No
|
input từ keyboard
|
players?
|
1
|
100
|
100
|
2
|
0
|
không chấp nhận
|
3
|
-1
|
không chấp nhận
|
2.3.
Phát triển methods
InputTest.java
package view;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class InputTest {
private static final int LOWER_BOUND = 9; // class constant – hằng thuộc lớp
private static final int UPPER_BOUND = 15;
@Test
public void readPlayersPerTeam() {
int playersPerTeam = Input.readPlayersPerTeam(LOWER_BOUND, UPPER_BOUND);
assertTrue(LOWER_BOUND <= playersPerTeam && playersPerTeam <= UPPER_BOUND);
}
@Test
public void readPlayersPerTeamGui() {
int playersPerTeam = Input.readPlayersPerTeamGui(LOWER_BOUND, UPPER_BOUND);
assertTrue(LOWER_BOUND <= playersPerTeam && playersPerTeam <= UPPER_BOUND);
}
@Test
public void readPlayersGui() {
assertTrue(Input.readPlayersGui() > 0);
}
}
Input.java
package view;
import java.util.InputMismatchException;
import java.util.Scanner;
import javax.swing.JOptionPane;
public class Input {
public static int readPlayersPerTeamGui(int lowerBound, int upperBound) {
try {
String s = JOptionPane.showInputDialog("Number of players per team [" +
lowerBound + ", " + upperBound + "]: ");
int n = Integer.parseInt(s);
if (lowerBound <= n && n <= upperBound)
return n;
} catch (NumberFormatException e) {}
return readPlayersPerTeamGui(lowerBound, upperBound);
}
public static int readPlayersPerTeam(int lowerBound, int upperBound) {
System.out.print("Number of players per team [" + lowerBound +
", " + upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
try {
int n = scanner.nextInt();
if (lowerBound <= n && n <= upperBound)
return n;
} catch (InputMismatchException e) {}
return readPlayersPerTeam(lowerBound, upperBound);
}
public static int readPlayersGui() {
try {
String s = JOptionPane.showInputDialog("Total number of players: ");
int n = Integer.parseInt(s);
if (n > 0)
return n;
} catch (NumberFormatException e) {}
return readPlayersGui();
}
}
3.
Tính number of teams
3.1.
Thiết kế sơ bộ
Do
đây là thao tác tính toán, ta đặt class vào model
package. Model (hay business logic) là từ thường
dùng để biểu thị bộ phận tính toán của chương
trình.
Hình 9
Để
xác định tên class ta cần trả lời câu hỏi: Đối
tượng nào có số cầu thủ mỗi đội và số cầu thủ
tham dự, rồi từ đó tính được số đội bóng?
Câu trả lời có thể là: ban tổ chức (organizer).
Hình 10
3.2.
Test cases
Ta có hai input (fields)
là playersPerTeam và
players, nên có thể
xây dựng 3 test cases, trong đó playersPerTeam
nhỏ hơn, bằng, hoặc lớn hơn players.
computeTeams()
|
|||
No
|
playersPerTeam
|
players
|
teams?
|
1
|
11
|
10
|
0
|
2
|
11
|
11
|
1
|
3
|
11
|
100
|
9
|
Lưu ý là khi xây
dựng test cases cho computeTeams(), ta đã không tính đến
các trường hợp bất hợp lệ của playersPerTeam
và players. Điều này dựa trên cơ sở là phần tính
toán chỉ phải lo tính toán, không nên lo đến việc kiểm
tra tính hợp lệ của data.
3.3.
Phát triển method
OrganizerTest.java
package model;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class OrganizerTest {
@Test
public void computeTeams() {
assertEquals(0, new Organizer(11, 10).computeTeams());
assertEquals(1, new Organizer(11, 11).computeTeams());
assertEquals(9, new Organizer(11, 100).computeTeams());
}
}
Organizer.java
package model;
public class Organizer {
private int playersPerTeam;
private int players;
public Organizer(int playersPerTeam, int players) {
this.playersPerTeam = playersPerTeam;
this.players = players;
}
public int computeTeams() {
return this.players / this.playersPerTeam;
}
}
Lưu ý là phép chia bên trong computeTeams() là một
phép chia nguyên, không quan tâm đến phần dư.
4.
Tính number of left over players
4.1.
Thiết kế sơ bộ
Hình
11
4.2.
Test cases
computeLeftOverPlayers()
|
|||
No
|
playersPerTeam
|
players
|
leftOverPlayers?
|
1
|
11
|
10
|
10
|
2
|
11
|
11
|
0
|
3
|
11
|
100
|
1
|
4.3.
Phát triển method
OrganizerTest.java
public class OrganizerTest {
@Test
public void computeTeams() {
assertEquals(0, new Organizer(11, 10).computeTeams());
assertEquals(1, new Organizer(11, 11).computeTeams());
assertEquals(9, new Organizer(11, 100).computeTeams());
}
@Test public void computeLeftOverPlayers() {
assertEquals(10, new Organizer(11, 10).computeLeftOverPlayers());
assertEquals(0, new Organizer(11, 11).computeLeftOverPlayers());
assertEquals(1, new Organizer(11, 100).computeLeftOverPlayers());
}
}
Để ý rằng đoạn code trên có hiện tượng code
duplication ở hai methods khác nhau. Để loại bỏ hiện
tượng này, ta đặt tên cho phần duplication và khai báo
chúng là fields trong class.
public class OrganizerTest {
private Organizer organizer1 = new Organizer(11, 10);
private Organizer organizer2 = new Organizer(11, 11);
private Organizer organizer3 = new Organizer(11, 100);
@Test
public void computeTeams() {
assertEquals(0, this.organizer1.computeTeams());
assertEquals(1, this.organizer2.computeTeams());
assertEquals(9, this.organizer3.computeTeams());
}
@Test public void computeLeftOverPlayers() {
assertEquals(10, this.organizer1.computeLeftOverPlayers());
assertEquals(0, this.organizer2.computeLeftOverPlayers());
assertEquals(1, this.organizer3.computeLeftOverPlayers());
}
}
Tiếp theo, ta phát triển computeLeftOverPlayers(),
trong đó ta sử dụng toán tử modulo %
để xác định phần dư của phép chia nguyên.
Organizer.java
public class Organizer {
private int playersPerTeam;
private int players;
public Organizer(int playersPerTeam, int players) {
this.playersPerTeam = playersPerTeam;
this.players = players;
}
public int computeTeams() {
return this.players / this.playersPerTeam;
}
public int computeLeftOverPlayers() {
return this.players % this.playersPerTeam;
}
}
5.
Tích hợp
Như vậy là ta đã
hoàn tất việc phát triển bốn nhiệm vụ mà bài toán đã
đặt ra. Tuy nhiên, ta cần tích hợp bốn nhiệm vụ này
lại để hình thành một chương trình hoàn chỉnh. Để
thực hiện việc này, ta tạo thêm Main
class và main()
method đặc biệt của class đó.
Hình 12
Các
mũi tên đứt nét đi từ Main
đến Input và
Orgernizer có nghĩa
là để Main có thể
hoàn thành nhiệm vụ, nó cần dùng đến Input
và Orgernizer. Nói
cách khác, quan hệ giữa Main
và Input, cũng như
quan hệ giữa Main
và Orgernizer là
quan hệ “sử dụng”
(“uses”
relationship) hay cộng
tác
(collaboration
relationship).
package controller;
import javax.swing.JOptionPane;
import model.Organizer;
import view.Input;
public class Main {
public static void main(String[] args) {
int playersPerTeams = Input.readPlayersPerTeamGui(9, 15);
int players = Input.readPlayersGui();
Organizer organizer = new Organizer(playersPerTeams, players);
JOptionPane.showMessageDialog(null,
"There will be " + organizer.computeTeams() + " team(s) with " +
organizer.computeLeftOverPlayers() + " player(s) left over");
}
}
Sau đây là một ví dụ khi thi hành Main.
(Trong Eclispe, vào Main
class, click Run menu,
rồi chọn Run)
Hình 13
Hình 14
Hình 15
6.
Phân định rõ class chỉ chứa static method(s)
Như
đã được thảo luận ở trên, khi gọi thi hành một
static method, ta không cần tạo object, mà dùng trực tiếp
tên class. Nói chung, nên tránh lạm dụng static methods để
giải pháp của ta mang tính tư duy hướng đối tượng
hơn. Trong trường hợp cần thiết, chẳng hạn ở bài
này, ta có lời khuyên là nên ngăn trở những toan tính
tạo object từ các classes đặc biệt đó. Để làm được
điều này, ta tạo thêm constructor phi tham số (no-arg
constructor) cho class, đồng thời khai báo constructor đó là
private (riêng tư), tức bên ngoài class không thể gọi new
trên class đó được nữa. Nếu bên trong class gọi new,
ngoại lệ RuntimeException
sẽ được tung ra.
Input.java
package view;
import java.util.InputMismatchException;
import java.util.Scanner;
import javax.swing.JOptionPane;
public class Input {
private Input() {
throw new RuntimeException(this.getClass() +
" is a noninstantiable utility class");
}
public static int readPlayersPerTeamGui(int lowerBound, int upperBound) {
try {
String s = JOptionPane.showInputDialog("Number of players per team [" +
lowerBound + ", " + upperBound + "]: ");
int n = Integer.parseInt(s);
if (lowerBound <= n && n <= upperBound)
return n;
} catch (NumberFormatException e) {}
return readPlayersPerTeamGui(lowerBound, upperBound);
}
public static int readPlayersPerTeam(int lowerBound, int upperBound) {
System.out.print("Number of players per team [" + lowerBound +
", " + upperBound + "]: ");
Scanner scanner = new Scanner(System.in);
try {
int n = scanner.nextInt();
if (lowerBound <= n && n <= upperBound)
return n;
} catch (InputMismatchException e) {}
return readPlayersPerTeam(lowerBound, upperBound);
}
public static int readPlayersGui() {
try {
String s = JOptionPane.showInputDialog("Total number of players: ");
int n = Integer.parseInt(s);
if (n > 0)
return n;
} catch (NumberFormatException e) {}
return readPlayersGui();
}
}
Main.java
package controller;
import javax.swing.JOptionPane;
import model.Organizer;
import view.Input;
public class Main {
private Main() {
throw new RuntimeException(this.getClass() +
" is a noninstantiable utility class");
}
public static void main(String[] args) {
int playersPerTeams = Input.readPlayersPerTeamGui(9, 15);
int players = Input.readPlayersGui();
Organizer organizer = new Organizer(playersPerTeams, players);
JOptionPane.showMessageDialog(null,
"There will be " + organizer.computeTeams() + " team(s) with " +
organizer.computeLeftOverPlayers() + " player(s) left over");
}
}
III. Tổng
kết
Bài
viết đã trình bày một thiết kế tách biệt khối nhập
data (thể hiện qua bộ phận view) ra khỏi khối tính
toán (thể hiện qua bộ phận model),
rồi sử dụng bộ phận controller
để tích hợp tất cả lại với nhau, thông qua “uses”
hay collaboration relationship,
để tạo ra một ứng dụng hoàn chỉnh chạy trên môi
trường console hoặc GUI.
Ngoài ra, bài viết còn đề cập đến các khái
niệm và kỹ thuật sau
- Recursion
- Xử lý exception bằng try-catch block
- Static/class methods
- Local và class constants
IV. Tài
liệu tham khảo
- Gaddis T. (2010) Starting Out with Java From Control Structures Through Objects (4th edition), Pearson Education, Boston. Chương 4.
- Bloch, J. (2008) Effective Java, Second Edition, Addison-Wesley, Upper Saddle River, NJ. Item 4, p. 19.
V. Bài
tập
Hãy
dùng console để bổ sung readPlayers() method vào Input
class.
Không có nhận xét nào:
Đăng nhận xét