Thứ Sáu, 2 tháng 3, 2012

OOP - Bài 8: Điểm trung bình toàn môn học



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.

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

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


Bước 2. Thiết kế sơ bộ

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