Thứ Sáu, 27 tháng 1, 2012

OOP - Bài 4: Thức ăn cho vật nuôi (Pet Food)



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.

Hình 1



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à method
    Ta 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 PetFoodgetPrice().

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 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 PetFoodPetFoodTest 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 PetFoodPriceListTestPetFoodPriceList 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
  1. 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

2 nhận xét: