Center
of Excellence
Bài
4
Thức
ăn cho vật nuôi (Pet Food)
Bạn
cần hiểu biết đối tượng qua tên riêng của nó,
để
triệt tiêu hiểm họa huyền bí của đối tượng đó.
--
Elias Canetti
I.
Bài toán
Một
cửa hàng bày bán các loại thức ăn cho vật nuôi như
sau:
Loại
(grade)
|
Giá
(cents/lb.)
|
A
|
30
|
B
|
20
|
C
|
10
|
Hãy
phát triển chương trình xác định giá của một loại
thức ăn cho trước. 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, bạn có thể
tham khảo một UI mẫu dưới đây.
II.
Giải pháp
Bước
1. Phát biểu lại yêu cầu bài toán
Xác
định giá của một loại thức ăn
- Input: Loại thức ăn (grade)
- Output: Giá (price)
Bước
2. Thiết kế sơ bộ
- Xác định tên method và tên parameter
Hình
2
- Thử chuyển parameter thành field của class
Hình
3
- Xác định tên class dựa vào field và methodTa trả lời câu hỏi: Đối tượng gì có loại thức ăn cho vật nuôi và có thể cho biết giá của nó? Đó chính là thực phẩm (pet food).
Hình
4
Bước
3. Xây dựng test cases
Nhận
xét rằng input chỉ có thể là một trong ba trường hợp:
A, B, hoặc C, vì vậy ta chỉ cần ba test cases là đủ.
getPrice()
|
||
No
|
grade
|
price?
|
1
|
A
|
30
|
2
|
B
|
20
|
3
|
C
|
10
|
Tuy
nhiên, người khó tính có thể cho rằng ba trường hợp
trên là chưa đủ, lỡ người dùng cố tình hay vô ý nhập
vào một input khác, chẳng hạn là D thì sao? Trong trường
hợp đó, getPrice() sẽ không biết phải về giá trị
nào cho phù hợp, vì dữ liệu mà bài toán này cung cấp
không hề có loại thực phẩm nào như thế. Ta nói đây
là trường hợp ngoại lệ (exception) và sẽ xử lý sau.
Bây giờ ta bổ sung data types vào thiết kế.
Hình
5
Bước
4: Phát triển test class
- Project: PetFood
- Package: petfood
- Test class: PetFoodTest
package petfood;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class PetFoodTest {
@Test
public void getPrice() {
assertEquals(30, new PetFood('A').getPrice());
assertEquals(20, new PetFood('B').getPrice());
assertEquals(10, new PetFood('C').getPrice());
}
}
Từ
PetFoodTest, ta tiến hành xây dựng PetFood và
getPrice().
package petfood;
public class PetFood {
private char grade;
public PetFood(char grade) {
this.grade = grade;
}
public int getPrice() {
return ???;
}
}
Đến
đây ta tập trung suy luận trên getPrice(). Biết rằng
phụ thuộc vào loại thức ăn, getPrice() chỉ có thể
trả về một trong ba giá trị là 30, 20, hay 10, ta áp dụng
cấu trúc điều kiện IF
public int getPrice() {
if (this.grade == 'A')
return 30;
else if (this.grade == 'B')
return 20;
else
return 10;
}
Xử
lý ngoại lệ bằng NoSuchElementException
Như
đã đề cập ở trên, getPrice() sẽ gặp trường
hợp ngoại lệ khi grade không phải là 'A', 'B', hoặc
'C'. Với getPrice() hiện tại, nếu grade là 'D'
thì kết quả nhận được sẽ là 10, và tất nhiên đây
là kết quả không chính xác. Ta bắt đầu xử lý
exception bằng việc bổ sung trường hợp này vào test
class.
package petfood;
import java.util.NoSuchElementException;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class PetFoodTest {
@Test
public void getPrice() {
assertEquals(30, new PetFood('A').getPrice());
assertEquals(20, new PetFood('B').getPrice());
assertEquals(10, new PetFood('C').getPrice());
}
@Test(expected = NoSuchElementException.class)
public void getPriceWithIllegalGrade() {
new PetFood('D').getPrice();
}
}
Chú
ý rằng ta đã tách riêng trường hợp exception bằng cách
đặt nó vào một test method khác và báo rằng ta đã dự
kiến (expected) sẽ gặp NoSuchElementException
(không có giá trị nào như thế).
Bây
giờ nếu thi hành PetFoodTest, ta sẽ gặp tín hiệu
đỏ vì getPrice() chưa có khả năng xử lý exception
này. Để được tín hiệu xanh, ta điều chỉnh lại
getPrice() như sau
public int getPrice() {
if (this.grade == 'A')
return 30;
else if (this.grade == 'B')
return 20;
else if (this.grade == 'C')
return 10;
else
throw new NoSuchElementException();
}
Sử dụng kiểu
enum cho grade
Theo thiết kế ở
Hình 5, ta đã áp dụng kiểu char cho grade
field. Java dùng một số nguyên có kích thước 2 bytes (đơn
vị bộ nhớ trong computer). Điều này có nghĩa là giá trị
kiểu char có thể là một trong 65536 giá trị khác
nhau (từ 0 đến 65535). Đây là một số lượng khổng lồ
so với số loại thức ăn được đề cập trong bài toán.
Nhằm ngăn chặn không cho PetFood tiếp nhận một giá
trị grade không thực sự tồn tại, ta có thể thay
kiểu char bằng một kiểu dữ liệu liệt kê
(enumerated data type).
Để tạo một
kiểu enum, trong File menu ta chọn New, rồi
chọn Enum.
Hình
6
Nhập Name là
Payment, rồi chọn Finish
Hình
7
Bây giờ bên
trong Grade, ta có thể liệt kê tất cả các loại
thức ăn hợp lệ.
package petfood;
public enum Grade {
A, B, C
}
Do đã thay char
bằng Grade, ta điều chỉnh PetFood và
PetFoodTest như sau
PetFood.java
package petfood;
import java.util.NoSuchElementException;
public class PetFood {
private Grade grade;
public PetFood(Grade grade) {
this.grade = grade;
}
public int getPrice() {
if (this.grade == Grade.A)
return 30;
else if (this.grade == Grade.B)
return 20;
else if (this.grade == Grade.C)
return 10;
else
throw new NoSuchElementException("Unknown price for grade " + grade);
}
}
PetFoodTest.java
package petfood;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class PetFoodTest {
@Test
public void getPrice() {
assertEquals(30, new PetFood(Grade.A).getPrice());
assertEquals(20, new PetFood(Grade.B).getPrice());
assertEquals(10, new PetFood(Grade.C).getPrice());
}
}
III.
Một thiết kế khác
Hãy
quay lại Hình 2. Giả sử rằng ta không muốn chuyển
grade parameter trở thành field của class. Để xác
định tên class trong tình huống này, ta cần trả lời câu
hỏi: Đối tượng gì có thể cho biết giá nếu nó
nhận được thông tin về loại thức ăn? Đó chính là
bảng giá (price list). Ta có thiết kế sau
Hình
8
Với
thiết kế trên, ta phát triển lại test class.
package petfood;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class PetFoodTest {
@Test
public void getPrice() {
assertEquals(30, new PetFoodPriceList().getPrice(Grade.A));
assertEquals(20, new PetFoodPriceList().getPrice(Grade.B));
assertEquals(10, new PetFoodPriceList().getPrice(Grade.C));
}
}
Để
ý rằng trong getPrice() method của PetFoodPriceListTest
class, ta đã ba lần tạo ra đối tượng PetFoodPriceList
thông qua việc sử dụng toán tử new. Điều này là
dư thừa vì chỉ cần một price list là đủ. Ta tiến hành
loại bỏ tình trạng trùng lặp dữ liệu (data duplication)
này bằng cách khai báo price list là một biến cục bộ
(local variable) bên trong getPrice().
public class PetFoodTest {
@Test
public void getPrice() {
PetFoodPriceList petFoodPriceList = new PetFoodPriceList();
assertEquals(30, petFoodPriceList.getPrice(Grade.A));
assertEquals(20, petFoodPriceList.getPrice(Grade.B));
assertEquals(10, petFoodPriceList.getPrice(Grade.C));
}
}
Với
test class như trên, ta phát triển PetFoodPriceList
class như sau
package petfood;
import java.util.NoSuchElementException;
public class PetFoodPriceList {
public int getPrice(Grade grade) {
if (grade == Grade.A)
return 30;
else if (grade == Grade.B)
return 20;
else if (grade == Grade.C)
return 10;
else
throw new NoSuchElementException("Unknown price for grade " + grade);
}
}
Dùng phương án
null thay cho exception
Ngoài
việc sử dụng những exceptions như NoSuchElementException
để xử lý ngoại lệ, ta còn có thể sử dụng một
kiệu đặc biệt là null, nghĩa là chưa thể xác
định được giá trị cụ thể. Khi đó, ta viết lại
PetFoodPriceListTest và PetFoodPriceList như sau
PetFoodPriceListTest.java
package petfood;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class PetFoodTest {
@Test
public void getPrice() {
PetFoodPriceList petFoodPriceList = new PetFoodPriceList();
assertEquals(30, (int)petFoodPriceList.getPrice(Grade.A));
assertEquals(20, (int)petFoodPriceList.getPrice(Grade.B));
assertEquals(10, (int)petFoodPriceList.getPrice(Grade.C));
}
}
PetFoodPriceList.java
package petfood;
public class PetFoodPriceList {
public Integer getPrice(Grade grade) {
if (grade == Grade.A)
return 30;
else if (grade == Grade.B)
return 20;
else if (grade == Grade.C)
return 10;
else
return null;
}
}
Có hai điểm cần
chú ý trong getPrice(). Thứ nhất, thay vì tung ngoại
lệ bằng việc gọi thow new NoSuchElementException(), ta
đã dùng return null, tức getPrice()
không thể trả về một giá trị cụ thể. Thứ hai, kiểu
dữ liệu trả về đã được thay đổi từ kiểu nguyên
thủy (primitive data type) là int sang kiểu đối tượng
(object data type) tương ứng là Integer. Lý do của sự
thay đổi này là vì trong Java, kiểu int không bao gồm
giá trị null, trong khi kiểu đối tượng tương ứng
Integer lại có thể chấp nhận giá trị null.
Mọi primitive data
type đều có object data type tương ứng. Chẳng hạn,
boolean ứng với Boolean, char ứng với
Character, int ứng với Integer, double
ứng với Double, … Sự khác nhau giữa primitive type
và object type tương ứng là không nhiều, nên object type
còn được gọi là wrapper type, tức chẳng qua là primitive
type được gói lại trong một vỏ bọc (wrapper). Tuy nhiên,
ở một số tình huống, ta buộc phải sử dụng object
type thay cho primitive type, như trường hợp null trong
getPrice() ở trên.
IV.
Tổng kết
Bài
viết đã trình bày hai thiết kế khác nhau nhằm giải
quyết cùng một vấn đề là xác định giá cả của một
loại thức ăn. Thiết kế thứ nhất đã xem loại thực
phẩm (grade) là một field bên trong PetFood class,
trong khi thiết kế thứ hai lại xem grade là một
parameter của getPrice() method bên trong PetFoodPriceList
class. Điều này cho thấy việc đặt tên class phụ thuộc
rất nhiều vào fields và methods của class đó.
Ngoài
ra, bài còn giới thiệu vấn đề xử lý ngoại lệ và
làm thế nào phát triển một test method cho nó. Kiểu dữ
liệu liệt kê (enumerated data type) và kiểu dữ liệu với
vỏ bọc (wrapper data type) cũng đã được thảo luận.
V.
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 3.
VI.
Bài tập
Hãy
dùng enum để viết lại
giải pháp cho vấn đề đặt ra ở Bài 2.
VII.
Thuật ngữ tiếng Anh
byte
|
một
đơn vị bộ nhớ trong computer
|
enumerated
data type
|
kiểu
dữ liệu liệt kê
|
exception
|
ngoại
lệ
|
object
data type
|
kiểu
dữ liệu đối tượng
|
primitive
data type
|
kiểu
dữ liệu nguyên thủy
|
wrapper
data type
|
kiểu
dữ liệu với vỏ bọc
|
Nhận xét này đã bị quản trị viên blog xóa.
Trả lờiXóaNhận xét này đã bị quản trị viên blog xóa.
Trả lờiXóa