Chủ Nhật, 5 tháng 2, 2012

OOP - Bài 7: Doanh thu trong tuần



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
  1. 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
  2. Tính doanh thu bình quân ngày (average daily sales)
    • Input: daily sales figures
    • Output: average daily sales
  3. Tính doanh thu ngày cao nhất (highest daily sales)
    • Input: daily sales figures
    • Output: highest daily sales
  4. Tính doanh thu ngày thấp nhất (lowest daily sales)
    • Input: daily sales figures
    • Output: lowest daily sales

1. Tính total weekly sales

1.1. Thiết kế sơ bộ

Xác định tên methods và parameters

Hình 2

Thử chuyển parameter thành field

Hình 3

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.

Hình 4

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

Hình 5

Do đã xác định được tên class, giờ đây ta có thể đơn giản hóa tên method và field.

Hình 6


1.2. Test cases


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. Tính average daily sales

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.

Hình 7


2.2. Test cases


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()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. Tính highest daily sales

3.1. Thiết kế sơ bộ

Hình 8


3.2. Test cases


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.


4. Tính lowest daily sales

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