Thiết kế chương trình hướng đối tượng
Center
of Excellence
Bài
11
Doanh
thu trong tháng
Cách nhanh nhất để
làm được nhiều việc
là chỉ làm đúng một
việc tại một thời điểm
-- Richard Cech
I. Bài toán
Doanh thu hàng ngày
trong tháng của công ty được lưu trữ trong một tập tin
văn bản (text file). Hãy phát triển chương trình tính tổng
doanh thu tháng và doanh thu bình quân ngày. Sau đây là ví
dụ về doanh thu tháng:
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
(1) Tính tổng doanh thu tháng (total sales)- Input: tên text file (file name)
- Output: total sales
- Input: file name
- Output: average daily sales
Bước 2. Thiết kế sơ bộ
Tương tự như bài
trước, ở đây ta phải làm việc với dữ liệu lưu trữ,
nên ta tạo một class trong data
tier, làm đại diện cho text file này.
Hình 2
Bước 3. Xây dựng test cases
No
|
fileName
|
sum?
|
averageDailySales?
|
1
|
April2012.txt
theo Hình 1 (file hợp lệ, gồm nhiều dòng)
|
56437.42
(có thể sử dụng phần mền bảng tính để tính ra
kết quả này)
|
1881.25 (làm tròn về hai chữ số
sau dấu chấm thập phân từ kết quả 1881.247333...)
|
2
|
oneLine.txt
(1
1458.32)
(file
hợp lệ, gồm một dòng)
|
1458.32
|
1458.32
|
3
|
File không tồn
tại hoặc sai đường dẫn (nonexistence.txt)
|
Không có
|
Không có
|
4
|
empty.txt
(file trống)
|
Không có
|
Không có
|
5
|
invalid1.txt
(file không hợp lệ do chứa dòng trống)
|
Không có
|
Không có
|
6
|
invalid2.txt
(file không hợp lệ do có dòng chứa không đủ hai giá
trị)
|
Không có
|
Không có
|
7
|
invalid3.txt
(file không hợp lệ do có dòng chứa giá trị không
phải là số)
|
Không có
|
Không có
|
Bước 4. Phát triển test class
- Project: MonthlySales
- Package: data
4.1. Phát triển test method cho sum()
4.1.1. Trường hợp file hợp lệ, chứa một hay nhiều dòng
package data;
import java.io.FileNotFoundException;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class MonthlySalesFacadeTest {
@Test
public void sum() throws FileNotFoundException {
assertEquals(56437.42, new MonthlySalesFacade("April2012.txt").sum(), 0.0);
assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
}
}
Sau
khi đã có test method, ta tiến hành phát triển method chính.
Ban đầu, cách làm tương tự như bài trước.
package data;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class MonthlySalesFacade {
private final String fileName;
public MonthlySalesFacade(String fileName) {
this.fileName = fileName;
}
public double sum() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
double acc = 0.0;
while (scanner.hasNext())
???
return acc;
}
}
}
Tuy
nhiên, cấu trúc file ở bài này khác bài trước ở chỗ:
mỗi dòng (line) chứa hai giá trị, một số nguyên và một
số thực. Vì vậy trước tiên ta cần đọc ra nguyên một
dòng.
public double sum() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
double acc = 0.0;
while (scanner.hasNext()) {
String line = scanner.nextLine();
???
}
return acc;
}
}
Sau đó ta dùng thêm một Scanner object để thực hiện
việc trích data từ line đó. Khi trích giá trị nguyên bằng
nextInt(), ta không xử lý gì cả vì giá trị này
không phục vụ cho việc tính tổng.
public double sum() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
double acc = 0.0;
while (scanner.hasNext()) {
String line = scanner.nextLine();
Scanner lineScanner = new Scanner(line);
lineScanner.nextInt();
acc += lineScanner.nextDouble();
}
return acc;
}
}
Đến
đây nếu thi hành test method, ta gặp tín hiệu đỏ, do khả
năng biểu diễn và tính toán không thật chính xác trên
số thực của computer. Ta điều chỉnh lại sai số cho
phép như sau
@Test
public void sum() throws FileNotFoundException {
assertEquals(56437.42, new MonthlySalesFacade("April2012.txt").sum(), 2E-11);
assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
}
4.1.2. Trường hợp file không tồn tại hoặc sai đường dẫn
Ta
làm tương tự như bài trước.
@Test(expected=FileNotFoundException.class)
public void sumNonexistentFile() throws FileNotFoundException {
new MonthlySalesFacade("nonExistence.txt").sum();
}
4.1.3. Trường hợp file trống
Cách
làm tương tự như bài trước.
public double sum() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
if (scanner.hasNext() == false)
throw new NoSuchElementException(this.fileName + " is empty");
double acc = 0.0;
while (scanner.hasNext()) {
String line = scanner.nextLine();
Scanner lineScanner = new Scanner(line);
lineScanner.nextInt();
acc += lineScanner.nextDouble();
}
return acc;
}
}
4.1.4. Trường hợp file chứa dòng trống hoặc chứa dòng không đủ hai giá trị
Tương
tự như trường hợp file trống, kết quả dự kiến sẽ
là ngoại lệ NoSuchElementExceoption.
@Test(expected=NoSuchElementException.class)
public void sumEmptyFile() throws FileNotFoundException {
new MonthlySalesFacade("empty.txt").sum();
}
@Test(expected=NoSuchElementException.class)
public void sumFileWithBlankLine() throws FileNotFoundException {
new MonthlySalesFacade("invalid1.txt").sum();
}
@Test(expected=NoSuchElementException.class)
public void sumFileWithInsufficientData() throws FileNotFoundException {
new MonthlySalesFacade("invalid2.txt").sum();
}
4.1.5.
Trường hợp file chứa giá trị không phải là số
Kết
quả dự kiến sẽ là ngoại lệ InputMismatchExceoption.
@Test(expected=InputMismatchException.class)
public void sumFileWithNonNumberData() throws FileNotFoundException {
new MonthlySalesFacade("invalid3.txt").sum();
}
4.2. Phát triển test method cho averageDailySales()
public class MonthlySalesFacadeTest {
@Test
public void averageDailySales() {
assertEquals(1881.25,
new MonthlySalesFacade("April2012.txt").averageDailySales(), 0.003);
}
@Test
public void sum() throws FileNotFoundException {
assertEquals(56437.42, new MonthlySalesFacade("April2012.txt").sum(), 2E-11);
assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
}
...
}
Nhận
thấy sự xuất hiện của code duplication trong đoạn code
trên, ta tiến hành loại bỏ
bằng cách đưa phần giống nhau ra ngoài để có thể dùng
chung.
public class MonthlySalesFacadeTest {
private MonthlySalesFacade aprilSales = new MonthlySalesFacade("April2012.txt");
@Test
public void averageDailySales() throws FileNotFoundException {
assertEquals(1881.25, this.aprilSales.averageDailySales(), 0.003);
}
@Test
public void sum() throws FileNotFoundException {
assertEquals(56437.42, this.aprilSales.sum(), 2E-11);
assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
}
...
}
Đến
đây ta bắt đầu phát triển averageDailySales()
cho MonthlySalesFacade
class. Doanh thu bình quân ngày sẽ bằng tổng doanh thu tháng
chia cho tổng số ngày, ta viết
public class MonthlySalesFacade {
private final String fileName;
public MonthlySalesFacade(String fileName) {
this.fileName = fileName;
}
public double sum() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
if (scanner.hasNext() == false)
throw new NoSuchElementException(this.fileName + " is empty");
double acc = 0.0;
while (scanner.hasNext()) {
String line = scanner.nextLine();
Scanner lineScanner = new Scanner(line);
lineScanner.nextInt();
acc += lineScanner.nextDouble();
}
return acc;
}
}
public double averageDailySales() {
return this.sum() / this.count();
}
}
Rồi
ta phát triển tiếp count()
để đếm tổng số ngày, tức tổng số dòng chứa trong
file. Tuy nhiên, mặc dù averageDailySales()
ở trên chỉ chứa một dòng ngắn ngủi nhưng đó lại
là method không hiệu quả về tốc độ thi hành. Đọc kỹ
ta thấy method này đã phải thao tác trên file đến hai
lần, một lần trong lệnh gọi sum()
và một lần khác trong lệnh gọi count().
Biết rằng khi làm việc với bộ nhớ ngoài (external
memory), chẳng hạn với files hoặc databases, tốc độ thi
hành chương trình sẽ bị ảnh hưởng đáng kể. Vì vậy,
ta cần hạn chế số lần truy cập external memory. Ta làm
theo cách sau: khi đọc data từ file để tính trung bình, ta
đồng thời làm hai việc: vừa tính tổng và vừa đếm
số dòng. Ta đếm số dòng bằng cách ban đầu đặt count
về 0, tức ban đầu chưa đếm được dòng nào trong file.
Sau đó bên trong vòng lặp while,
mỗi lần đọc được một dòng thì tăng count
thêm 1 bằng toán tử ++.
public class MonthlySalesFacade {
private final String fileName;
public MonthlySalesFacade(String fileName) {
this.fileName = fileName;
}
public double sum() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
if (scanner.hasNext() == false)
throw new NoSuchElementException(this.fileName + " is empty");
double acc = 0.0;
while (scanner.hasNext()) {
String line = scanner.nextLine();
Scanner lineScanner = new Scanner(line);
lineScanner.nextInt();
acc += lineScanner.nextDouble();
}
return acc;
}
}
public double averageDailySales() throws FileNotFoundException {
try (Scanner scanner = new Scanner(new File(this.fileName))) {
if (scanner.hasNext() == false)
throw new NoSuchElementException(this.fileName + " is empty");
double acc = 0.0;
int count = 0;
while (scanner.hasNext()) {
String line = scanner.nextLine();
Scanner lineScanner = new Scanner(line);
lineScanner.nextInt();
acc += lineScanner.nextDouble();
count++;
}
return acc / count;
}
}
}
Cách
làm trên khiến averageDailySales()
trở nên khó hiểu, do nó phải giải quyết đồng thời
hai việc. Bù lại, tốc độ thi hành được cải tiến.
Nói chung, nếu không phải làm việc với external memory, ta
nên tuân thủ nguyên tắc đơn thể (modularization) hay đơn
nhiệm (single responsibility): Mỗi method chỉ nên làm một
việc.
III. Tổng kết
Bài viết trình bày kỹ thuật xử lý dữ liệu có cấu
trúc được lưu trong text files.
Để cải thiện hiệu suất thi hành khi làm việc với
external memory, chương trình đã vi phạm nguyên tắc đơn
nhiệm.
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 5.
Không có nhận xét nào:
Đăng nhận xét