Center
of Excellence
Bài
8
Điểm
trung bình toàn môn học
Vẻ
đẹp của phong cách, của nét hài hòa
của tính thanh lịch và
nhịp độ,
đều
tùy thuộc vào sự giản dị
--
Plato
I. Bài toán
Một giảng viên cho
học viên làm nhiều bài kiểm tra trong suốt một học kỳ.
Đến cuối kỳ, khi tính điểm trung bình cho một học
viên, giảng viên đó sẽ loại ra điểm thấp nhất. Chẳng
hạn, nếu tập các điểm của một học viên là 85, 70,
40, và 95, thì khi tính trung bình, điểm thấp nhất 40 sẽ
được loại ra, và lúc đó điểm trung bình môn sẽ là
(85 + 70 + 95) / 3 = 83,3. Hãy phát triển chương trình giúp
giảng viên thực hiện thao tác tính trung bình cho một học
viên.
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
Tính điểm trung
bình (average
score),
nhưng không kể điểm thấp nhất (min).
- Input: tất cả các điểm của một học viên (scores)
- Output: average score
Xác định tên
method và parameter
Hình 2
Thử chuyển
parameter thành field
Hình 3
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 toàn bộ
điểm của một học viên, rồi từ đó tính được điểm
trung bình mà không kể điểm thấp nhất? Đó chính là
tập các điểm kiểm tra của một học viên (score
collection).
Hình 4
Bước
3. Xây dựng test case
- Input: 85, 70, 40, 95
- Output: 83.3 (làm tròn đến một chữ số sau dấu thập phân)
Bước
4. Phát triển test class
- Project: ScoreCollection
- Package: scorecollection
package scorecollection;
import org.junit.Test;
import static org.junit.Assert.*;
public class ScoreCollectionTest {
@Test
public void averageWithoutMin() {
ScoreCollection scoreCollection = new ScoreCollection(85, 70, 40, 95);
assertEquals(83.3, scoreCollection.averageWithoutMin(), 0.0);
}
}
Sau
khi hoàn thành test class, ta xây dựng class và method chính
của chương trình.
package scorecollection;
public class ScoreCollection {
private final int[] scores;
public ScoreCollection(int... scores) {
this.scores = scores;
}
public double averageWithoutMin() {
return ???;
}
}
Điểm
trung bình sẽ bằng tổng số điểm mà không tính điểm
thấp nhất, chia cho số lượng điểm trừ 1. Ta triển
khai
public class ScoreCollection {
private final int[] scores;
public ScoreCollection(int... scores) {
this.scores = scores;
}
public double averageWithoutMin() {
return this.sumWithoutMin() / (this.scores.length - 1);
}
private int sumWithoutMin() {
return ???;
}
}
Đến
đây ta tiếp tục phát triển sumWithoutMin(). Kết
quả trả về sẽ là tổng số điểm trừ đi điểm thấp
nhất.
public class ScoreCollection {
private final int[] scores;
public ScoreCollection(int... scores) {
this.scores = scores;
}
public double averageWithoutMin() {
return this.sumlWithoutMin() / (this.scores.length - 1);
}
private int sumWithoutMin() {
return this.sum() - this.min();
}
}
Việc
tính tổng và xác định điểm thấp nhất sẽ tương tự
với Bài Doanh thu trong tuần. Ta được kết quả
sau.
public class ScoreCollection {
private final int[] scores;
public ScoreCollection(int... scores) {
this.scores = scores;
}
public double averageWithoutMin() {
return this.sumWithoutMin() / (this.scores.length - 1);
}
private int sumWithoutMin() {
return this.sum() - this.min();
}
private int sum() {
int acc = 0;
for (int score : this.scores)
acc += score;
return acc;
}
private int min() {
int currentMin = Integer.MAX_VALUE;
for (int score : this.scores)
if (score < currentMin)
currentMin = score;
return currentMin;
}
}
Xử
lý lỗi do phép chia nguyên
Khi
thi hành test class, ta nhận được tín hiệu đỏ với
thông báo lỗi: expected: <83.3> but was: <83.0>.
Ở đây nếu điều chỉnh sai số cho phép từ 0.0 lên 0.4
thì ta sẽ nhận được tín hiệu xanh. Tuy nhiên, hành động
này không thể giải quyết một lỗi nghiêm trọng đang ẩn
trong chương trình. Lỗi này phát sinh từ phép chia nguyên.
Hãy
đọc lại averageWithoutMin(). Kết quả của method này
là phép chia giữa tử số sumWithoutMin() và mẫu số
(scores.length – 1). Mà cả tử và mẫu đều là hai
số nguyên, nên Java mặc định thực hiện phép chia
nguyên, tức kết quả là 83, thay vì 83.333... Ta có thể
hỏi: thế thì tại sao kết quả là giá trị thực 83.0,
thay vì giá trị nguyên 83? Trả lời rằng: do
averageWithoutMin() qui định phải trả về một kết
quả double, Java một lần nữa thực hiện việc
chuyển đổi kiểu ngầm định từ int sang double,
tức ngầm chuyển 83 về 83.0.
Để
giải quyết được lỗi do phép chia nguyên ngầm định,
ta cần buộc Java thực hiện phép chia số thực trong
averageWithoutMin(). Muốn được như vậy, ta phải ép
kiểu một trong hai, hoặc cả hai, tử số và mẫu số về
double. Sau đây là code ép kiểu tử số về double.
public double averageWithoutMin() {
return (double)this.sumWithoutMin() / (this.scores.length - 1);
}
Đến
đây nếu thi hành test class với sai số cho phép là 0.0, ta
nhận được một thông báo lỗi khác: expected: <83.3>
but was: <83.33333333333333>. Để được tính hiệu
xanh, ta điều chỉnh sai số so phép về 0.04 như cách làm
của bài Doanh thu trong tuần.
III.
Tổng kết
Bài viết minh họa tiến trình tư duy nhằm xác định được
giá trị trung bình có ràng buộc trên một tập số.
Tại mỗi bước trong quá trình lý luận, ta có thể tạo
thêm một hay nhiều methods mới, để hỗ trợ cho việc
giải quyết vấn đề đang đặt ra. Cần cẩn trọng khi
đặt tên cho các methods, nhằm thể hiện tính dễ đọc
của code, tạo thuận lợi cho việc bảo trì software về
sau.
Ngoài
ra, bài viết còn minh họa một sai sót có thể xảy ra khi
thực hiện phép chia số thực giữa hai giá trị nguyên.
Khi đó ta cần thay đổi hành vi tính toán ngầm định của
Java bằng động tác ép kiểu (type casting) tường minh.
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