Center of Excellence
Bài
7
Doanh
thu trong tuần
Vạn vật được
tạo tác từ không
Everything is from
nothing
-- Jessie Fairweather
I. Bài toán
Hãy phát triển chương
trình tính tổng doanh thu trong tuần, doanh thu bình quân
ngày, doanh thu ngày cao nhất, và doanh thu ngày thấp nhất.
Sau đây là ví dụ về doanh thu của một tuần:
1000.00, 500.00, 3000.00,
4000.00, 9000.00, 6000.00, 7000.00
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
Ta cần giải quyết
các yêu cầu sau
- Tính tổng doanh thu tuần (total weekly sales)
- Input: doanh thu hàng ngày trong tuần (daily sales figures)
- Output: total weekly sales
- Tính doanh thu bình quân ngày (average daily sales)
- Input: daily sales figures
- Output: average daily sales
- Tính doanh thu ngày cao nhất (highest daily sales)
- Input: daily sales figures
- Output: highest daily sales
- Tính doanh thu ngày thấp nhất (lowest daily sales)
- Input: daily sales figures
- Output: lowest daily sales
1.1. Thiết kế sơ
bộ
Xác định tên
methods và parameters
Thử chuyển
parameter thành field
Xác định data
type cho field
Do
dailySalesFigures là
một tập hợp doanh thu của bảy ngày trong tuần, ta không
thể sử dụng double
cho field này, mà phải là một mảng (array)
gồm nhiều double.
Để chỉ định một array, ta bổ sung cặp ngoặc vuông.
Xác định tên
class dựa vào field và method
Ta
cần trả lời câu hỏi: Đối tượng nào chứa doanh
thu của bảy ngày trong tuần, rồi từ đó tính được
doanh thu tổng? Đó chính là doanh thu của một tuần
(weekly sales).
Do đã
xác định được tên class, giờ đây ta có thể đơn giản
hóa tên method và field.
sum()
|
||
No
|
figures
|
sum?
|
1
|
1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0,
7_000.0
|
30_500.0
|
1.3.
Phát triển method
Project: WeeklySales
package weeklysales;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class WeeklySalesTest {
@Test public void sum() {
assertEquals(30_500.0, new WeeklySales(???).sum(), 0.0);
}
}
Để
thay thế cụm ??? bằng giá trị cụ thể, ta đặt doanh
thu của bảy ngày trong tuần vào một array. Lưu ý là tất
cả các giá trị này đều nằm trong cặp ngoặc nhọn, và
chúng cách nhau bằng dấu phẩy.
public class WeeklySalesTest {
@Test public void sum() {
double[] figures = { 1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
assertEquals(30_500.0, new WeeklySales(figures).sum(), 0.0);
}
}
Từ
WeeklySalesTest, ta tạo ra WeeklySales class.
package weeklysales;
public class WeeklySales {
private double[] figures;
public WeeklySales(double[] figures) {
this.figures = figures;
}
public double sum() {
return ???;
}
}
Để
tính tổng, ta sử dụng một bộ tích lũy (accumulator) lấy
tên là acc. Ban đầu acc chưa tích lũy được
gì nên bằng 0.0.
public class WeeklySales {
private double[] figures;
public WeeklySales(double[] figures) {
this.figures = figures;
}
public double sum() {
double acc = 0.0;
???
return acc;
}
}
Sau
đó ta dùng vòng lặp for-each, lấy ra từng giá trị
doanh thu (figure) bên trong figures,
rồi tích lũy figure vào acc thông qua toán tử
cộng dồn +=. (Ngoài +=, ta còn có -=, *=, /=, %=)
public class WeeklySales {
private double[] figures;
public WeeklySales(double[] figures) {
this.figures = figures;
}
public double sum() {
double acc = 0.0;
for (double figure : this.figures)
acc += figure;
return acc;
}
}
2.1.
Thiết kế sơ bộ
Do
yêu cầu này dùng chung input với yêu cầu trước nên ta
đặt chúng vào cùng một class.
average()
|
||
No
|
figures
|
average?
|
1
|
1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0,
7_000.0
|
4_357.14
(làm tròn về hai chữ số sau dấu thập phân từ kết
quả 4_357.1428...)
|
2.3.
Phát triển method
public class WeeklySalesTest {
@Test public void sum() {
double[] figures = { 1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
assertEquals(30_500.0, new WeeklySales(figures).sum(), 0.0);
}
@Test public void average() {
double[] figures = { 1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
assertEquals(4_357.14, new WeeklySales(figures).average(), 0.0);
}
}
Xét
thấy có tình trạng code duplication trong hai test methods
sum() và average(),
nhưng ta cần hoàn thành nhiệm vụ trước mắt, đó là
phát triển average(). Doanh
thu bình quân ngày sẽ bằng tổng doanh thu chia cho tổng số
ngày, ta viết
package weeklysales;
public class WeeklySales {
private double[] figures;
public WeeklySales(double[] figures) {
this.figures = figures;
}
public double sum() {
double acc = 0.0;
for (double figure : this.figures)
acc += figure;
return acc;
}
public double average() {
return this.sum() / this.figures.length;
}
}
Ở
trên, bên trong average(),
ta đã tận dụng method tính tổng sum()
đã được phát triển từ trước. Thêm vào đó, ta còn
dùng length field của
figures array để
xác định tổng số ngày. Dĩ nhiên có thể biết trước
tổng số ngày là 7. Tuy vậy, việc sử dụng length
sẽ làm giải pháp trở nên tổng quát hơn. Chẳng hạn,
nếu cửa hàng đóng cửa ngày Chủ nhật, tức doanh số
trong tuần chỉ gồm 6 ngày, thì code của ta không cần
phải thay đổi.
Đến
đây nếu thi hành WeeklySalesTest
class, ta gặp tín hiệu đỏ với thông báo lỗi:
expected:<4357.14> but was:<4357.14857142857>,
tức kết quả của chương trình là 4357.14857142857,
chứ không phải giá trị 4357.14
mà ta đã kỳ vọng. Lý do là ta đã chủ động làm tròn
kết quả về hai chữ số sau dấu chấm thập phân. Để
được tín hiệu xanh, ta cần xác định lại độ lệch
cho phép giữa hai giá trị bên trong lệnh assertEquals().
Trước tiên, ta lấy giá trị lớn hơn trừ giá trị nhỏ
hơn, 4357.14857142857
- 4357.14
= 0.00857142857.
Từ đó độ lệch được xác định bằng cách giữ lại
giá trị chỉ chứa chữ số khác 0
đầu tiên tính từ bên trái (tức 0.008), rồi tăng 1
vào chữ số đó, tức 0.009.
Trường hợp vẫn gặp tín hiệu đỏ thì ta lại tiếp
tục tăng 1
vào chữ số khác 0
cho đến khi gặp tín hiệu xanh.
@Test public void average() {
double[] figures = { 1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
assertEquals(4_357.14, new WeeklySales(figures).average(), 0.009);
}
Loại
bỏ code duplication
Ta
tiến hành loại bỏ tình trạng code duplication bằng cách
khai báo figures là
một field.
public class WeeklySalesTest {
private double[] figures = { 1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
@Test public void sum() {
assertEquals(30_500.0, new WeeklySales(this.figures).sum(), 0.0);
}
@Test public void average() {
assertEquals(4_357.14, new WeeklySales(this.figures).average(), 0.009);
}
}
Hơn
nữa, cả hai methods đều gọi new
WeeklySales(this.figures), nên
ta tiếp tục chuyển object này thành field và đặt tên là
weeklySales.
public class WeeklySalesTest {
private double[] figures = { 1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
private WeeklySales weeklySales = new WeeklySales(this.figures);
@Test public void sum() {
assertEquals(30_500.0, this.weeklySales.sum(), 0.0);
}
@Test public void average() {
assertEquals(4_357.14, this.weeklySales.average(), 0.009);
}
}
Ta
còn có thể cải tiến code thêm một chút thông qua việc
vận dụng khái niệm varargs,
tức variable-length argument list (danh sách đối số có chiều
dài biến động). Trong đoạn code trên, thay vì phải định
nghĩa một array lấy tên là figures,
rồi truyền array đó vào lệnh new
WeeklySales(figures), ta có thể
viết trực tiếp như sau
public class WeeklySalesTest {
private WeeklySales weeklySales = new WeeklySales(
1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0);
@Test public void sum() {
assertEquals(30_500.0, this.weeklySales.sum(), 0.0);
}
@Test public void average() {
assertEquals(4_357.14, this.weeklySales.average(), 0.009);
}
}
Khi đó WeeklySales class cần phải được viết dưới đây, ở đó bên trong parameter của constructor, ta đã thay cặp ngoặc vuông bằng ba dấu chấm.
public class WeeklySales {
private double[] figures;
public WeeklySales(double... figures) {
this.figures = figures;
}
public double sum() {
double acc = 0.0;
for (double figure : this.figures)
acc += figure;
return acc;
}
public double average() {
return this.sum() / this.figures.length;
}
}
3.1.
Thiết kế sơ bộ
max()
|
||
No
|
figures
|
max?
|
1
|
1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0,
7_000.0
|
9_000.0
|
3.3.
Phát triển method
WeeklySalesTest.java
public class WeeklySalesTest {
private WeeklySales weeklySales = new WeeklySales(
1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0);
@Test public void sum() {
assertEquals(30_500.0, this.weeklySales.sum(), 0.0);
}
@Test public void average() {
assertEquals(4_357.14, this.weeklySales.average(), 0.009);
}
@Test public void max() {
assertEquals(9_000.0, this.weeklySales.max(), 0.0);
}
}
WeeklySales.java
public class WeeklySales {
private double[] figures;
public WeeklySales(double... figures) {
this.figures = figures;
}
public double sum() {
double acc = 0.0;
for (double figure : this.figures)
acc += figure;
return acc;
}
public double average() {
return this.sum() / this.figures.length;
}
public double max() {
return ???;
}
}
Để
tìm được doanh thu cao nhất, ta áp dụng thuật toán
(algorithm) sau: đầu tiên giả định rằng doanh thu cao nhất
hiện thời (current max) là hằng số âm vô cực
(negative infinity).
public double getMax() {
double currentMax = Double.NEGATIVE_INFINITY;
???
return currentMax;
}
Sau
đó ta dùng vòng lặp for-each để lấy ra từng giá
trị (figure) bên trong figures array,
public double max() {
double currentMax = Double.NEGATIVE_INFINITY;
for (double figure : this.figures)
???
return currentMax;
}
Mỗi
lần lấy được figure nào ra, ta so sánh nó với
currentMax, nếu lớn hơn currentMax, thì
figure đó sẽ trở thành currentMax.
public double max() {
double currentMax = Double.NEGATIVE_INFINITY;
for (double figure : this.figures)
if (figure > currentMax)
currentMax = figure;
return currentMax;
}
Sau
khi đã so sánh currentMax với toàn bộ sales
trong dailySales, currentMax chính là doanh số
cao nhất.
Phần
này dành làm bài tập. Lưu ý hằng số dương vô cực sẽ
là Double.POSITIVE_INFINITY.
III.
Tổng kết
Bài viết đã vận dụng cấu trúc dữ liệu array, vòng
lặp for-each, bộ tích lũy (accumulator), và toán tử
cộng dồn +=, để thực hiện những tính toán trên tập
hợp các số double.
Khái niệm về danh sách đối số có chiều dài biến động
(varargs) cũng đã được đề cập. Ngoài ra, bài còn trình
bày một phương pháp xác định độ lệch cho phép giữa
hai giá trị double
trong quá trình kiểm thử.
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 8.
Không có nhận xét nào:
Đăng nhận xét