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

OOP - Bài 9: Doanh thu toàn công ty



Center of Excellence


Bài 9
Doanh thu toàn công ty


Hãy gắng để lại thế giới này tốt hơn một tí
so với thời điểm bạn phát hiện ra nó
-- Robert Stephenson Smyth Baden-Powell

I. Bài toán

Hãy phát triển chương trình tính tổng doanh thu toàn năm của một công ty thương mại. Sau đây là ví dụ về doanh thu 4 quí trong năm của 3 chi nhánh thuộc công ty



Quí 1
Quí 2
Quí 3
Quí 4
Chi nhánh 1
$35,698.77
$36,148.63
$31,258.95
$30,864.12
Chi nhánh 2
$41,289.64
$43,278.52
$40,928.18
$42,818.98
Chi nhánh 3
$28914.56
$27631.52
$30596.64
$29834.21



II. Giải pháp
Bước 1. Phát biểu lại yêu cầu bài toán

Tính tổng doanh thu toàn năm (total sales)
  • Input: doanh thu hàng quí trong năm của 3 chi nhánh (quarterly sales)
  • Output: total sales


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

Xác định tên method và parameter

Hình 1

Thử chuyển parameter thành field

Hình 2

Xác định data type cho field

Do quarterlySales là tập hợp doanh thu của 3 chi nhánh, mỗi chi nhánh cung cấp doanh thu của 4 quí, ta có thể biểu diễn quarterlySales ở dạng ma trận (matrix) hay mảng hai chiều (two-dimensional array) của những số double. Khác với mảng một chiều (one-dimensional array, gọi tắt là array), ta cần đặt hai cặp ngoặc vuông vào sau kiểu double khi khai báo data type cho quarterlySales.

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 doanh thu hàng quí trong năm, rồi từ đó tính được tổng doanh thu? Đó chính là doanh thu cả năm (yearly sales).

Hình 4

Một khi xác định được tên class, ta có thể đơn giản hóa tên method như sau

Hình 5


Bước 3. Xây dựng test cases
  • Input: số liệu đã cho trong bài
  • Output: 419,262.72


Bước 4. Phát triển test class

  • Project: TotalSales
  • Package: totalsales


4.1. Phát triển YearlySalesTest

package totalsales;

import org.junit.Test;
import static org.junit.Assert.*;

public class YearlySalesTest {
    @Test public void sum() {
        assertEquals(419_262.72, new YearlySales(???).sum(), 0.0);
    }
}

Ta thay thế cụm ??? bằng một two-dimensional array với tên là quarterlySales.

public class YearlySalesTest {
    @Test public void sum() {
        double[][] quarterlySales = ???;
        assertEquals(419_262.72, new YearlySales(quarterlySales).sum(), 0.0);
    }
}

Một two-dimensional array có thể được xem là một one-dimensional array, trong đó mỗi phần tử của one-dimensional array lại là một one-dimensional array. Ta triển khai tiếp

public class YearlySalesTest {
    @Test public void sum() {
        double[][] quarterlySales = { ???, ???, ??? };
        assertEquals(419_262.72, new YearlySales(quarterlySales).sum(), 0.0);
    }
}

Mỗi một cụm ??? ở trên là một one-dimensional array doanh thu của một chi nhánh, ta cần đặt chúng vào trong một cặp ngoặc móc khác.

public class YearlySalesTest {
    @Test public void sum() {
        double[][] quarterlySales = {
            { ??? },
            { ??? },
            { ??? } };
        assertEquals(419_262.72, new YearlySales(quarterlySales).sum(), 0.0);
    }
}

Đến đây ta tiến hành thay thế ??? bằng những giá trị cụ thể.

public class YearlySalesTest {
    @Test public void sum() {
        double[][] quarterlySales = {
            { 35_698.77, 36_148.63, 31_258.95, 30_864.12 },
            { 41_289.64, 43_278.52, 40_928.18, 42_818.98 },
            { 28_914.56, 27_631.52, 30_596.64, 29_834.21 } };
        assertEquals(419_262.72, new YearlySales(quarterlySales).sum(), 0.0);
    }
}


4.2. Phát triển YearlySales

Từ YearlySalesTest, ta tạo YearlylySales class.

package totalsales;

public class YearlySales {
    private final double[][] quarterlySales;

    public YearlySales(double[][] quarterlySales) {
        this.quarterlySales = quarterlySales;
    }
    
    public double sum() {
        return ???;
    }
}

Như thường lệ, khi tính tổng của nhiều giá trị, ta sử dụng một accumulator lấy tên là acc.

public class YearlySales {
    private final double[][] quarterlySales;

    public YearlySales(double[][] quarterlySales) {
        this.quarterlySales = quarterlySales;
    }
    
    public double sum() {
        double acc = 0.0;
        ???
        return acc;
    }
}

Kế đến, ta dùng vòng lặp for-each để lấy ra từng phần tử (dòng) trong quarterlySales, tính tổng trên dòng đó, rồi tích lũy vào acc. Lưu ý rằng mỗi dòng là một one-dimensional array (double[]) gồm doanh thu 4 quí của một chi nhánh.

public class YearlySales {
    private final double[][] quarterlySales;

    public YearlySales(double[][] quarterlySales) {
        this.quarterlySales = quarterlySales;
    }
    
    public double sum() {
        double acc = 0.0;
        for (double[] divisionalSales : quarterlySales)
            acc += this.sum(divisionalSales);
        return acc;
    }
    
    private double sum(double[] divisionalSales) {
        return ???;
    }
}
Để ý rằng lúc này ta có hai methods sum trùng tên, song với số parameters khác nhau: method đầu không có, method sau có một parameter. Kỹ thuật cho phép một class có nhiều methods trùng tên, nhưng với parameters khác nhau, gọi là overloading. Overloading được dịch sang tiếng Việt là nạp chồng hoặc quá tải, có lẽ quá tải sát nghĩa hơn, nhưng ta cứ nên gọi là overloading và hiểu ý nghĩa của thuật ngữ này là được.

Việc tính tổng một one-dimensional array thì tương tự như Bài Doanh thu trong tuần.

public class YearlySales {
    private final double[][] quarterlySales;

    public YearlySales(double[][] quarterlySales) {
        this.quarterlySales = quarterlySales;
    }
    
    public double sum() {
        double acc = 0.0;
        for (double[] divisionalSales : quarterlySales)
            acc += this.sum(divisionalSales);
        return acc;
    }
    
    private double sum(double[] divisionalSales) {
        double acc = 0.0;
        for (double each : divisionalSales)
            acc += each;
        return acc;
    }
}

Nếu thi hành YearlySalesTest class, ta gặp tín hiệu đỏ với thông báo lỗi: expected:<419262.72> but was:<419262.72000000003>. Ta điều chỉnh độ lệch cho phép giữa hai giá trị về 0.00000000006, hoặc 6 x 10-11. Trong Java, ta viết 6E-11.


4.3. Loại bỏ code duplication

Như đã trình bày ở phần trên, thao tác tính tổng trên một one-dimensional array rất giống với bài Doanh thu trong tuần. Đây là dạng code duplication diễn ra ở hai projects khác nhau. Để loại bỏ, ta khai báo sum() là một static method và đặt vào một class mới, lấy tên là ArrayUtils. Nhiệm vụ của ArrayUtils là nơi chứa những thao tác nho nhỏ (utilities) xử lý trên arrays. Cách tạo ra class chỉ chứa các static methods đã được trình bày trong bài Tổ chức giải bóng đá.

package util;

public class ArrayUtils {
    private ArrayUtils() {
        throw new RuntimeException(this.getClass() +
                " is a noninstantiable utility class");
    }
    
    public static double sum(double[] values) {
        double acc = 0.0;
        for (double each : values)
            acc += each;
        return acc;
    }
}

Sau khi đã tạo sum(double[]) bên trong ArrayUtils, ta tiến hành loại bỏ method này ra khỏi YearlySales class và điều chỉnh lệnh gọi đến method này cho phù hợp với tình hình mới.

package totalsales;

import util.ArrayUtils;

public class YearlySales {
    private final double[][] quarterlySales;

    public YearlySales(double[][] quarterlySales) {
        this.quarterlySales = quarterlySales;
    }
    
    public double sum() {
        double acc = 0.0;
        for (double[] divisionalSales : quarterlySales)
            acc += ArrayUtils.sum(divisionalSales);
        return acc;
    }
}


III. Tổng kết

Bài viết đã trình bày kỹ thuật lập trình trên cấu trúc dữ liệu two-dimensional arrays cùng với khái niệm overloading áp dụng trong trường hợp một class chứa nhiều methods có cùng tên, nhưng khác parameters. Vấn đề code duplication diễn tra ở hai projects khác nhau đã được xử lý bằng cách chuyển phần code bị trùng lặp (duplicated code) vào trong một utility class riêng biệt.


IV. Bài tập

Bài 1

Hãy hoàn thành ArrayUtils class như được mô tả dưới đây. Nhớ kèm theo test class.

package util;

public class ArrayUtils {
    private ArrayUtils() {
        throw new RuntimeException(this.getClass() +
                " is a noninstantiable utility class");
    }
    
    public static double sum(double[] values) {
        double acc = 0.0;
        for (double each : values)
            acc += each;
        return acc;
    }
    
    public static double sum(double[][] values) {
        return ???;
    }
    
    public static double average(double[] values) {
        return ???;
    }
    
    public static double min(double[] values) {
        return ???;
    }
    
    public static double max(double[] values) {
        return ???;
    }
}

Đáp án

ArrayUtilsTest.java

package util;

import org.junit.Test;
import static org.junit.Assert.*;

public class ArrayUtilsTest {
    private double[] values = {
        1_000.0, 500.0, 3_000.0, 4_000.0, 9_000.0, 6_000.0, 7_000.0 };
    
    @Test public void sum1DArray() {
        assertEquals(30_500.0, ArrayUtils.sum(this.values), 0.0);
    }
    
    @Test public void sum2DArray() {
        double[][] values = {
            { 35_698.77, 36_148.63, 31_258.95, 30_864.12 },
            { 41_289.64, 43_278.52, 40_928.18, 42_818.98 },
            { 28_914.56, 27_631.52, 30_596.64, 29_834.21 } };
        assertEquals(419262.72, ArrayUtils.sum(values), 6E-11);
    }
    
    @Test public void average() {
        assertEquals(4357.14, ArrayUtils.average(this.values), 0.009);
    }
    
    @Test public void max() {
        assertEquals(9000.0, ArrayUtils.max(this.values), 0.0);
    }
        
    @Test public void min() {
        assertEquals(500.0, ArrayUtils.min(this.values), 0.0);
    }
}


ArrayUtils.java

package util;

public class ArrayUtils {
    private ArrayUtils() {
        throw new RuntimeException(this.getClass() +
                " is a noninstantiable utility class");
    }
    
    public static double sum(double[] values) {
        double acc = 0.0;
        for (double each : values)
            acc += each;
        return acc;
    }
    
    public static double sum(double[][] values) {
        double acc = 0.0;
        for (double[] row : values)
            acc += sum(row);
        return acc;
    }
    
    public static double average(double[] values) {
        return sum(values) / values.length;
    }
    
    public static double min(double[] values) {
        double currentMin = Double.POSITIVE_INFINITY;
        for (double value : values) {
            if (value < currentMin)
                currentMin = value;
        }
        return currentMin;
    }
    
    public static double max(double[] values) {
        double currentMax = Double.NEGATIVE_INFINITY;
        for (double value : values) {
            if (value > currentMax)
                currentMax = value;
        }
        return currentMax;
    }
}

Bài 2

Thường thì chúng ta không muốn lật lại vấn đề đã diễn ra trong quá khứ để cải thiện. Hãy chế ngự tâm thức lười biếng này bằng cách sửa đổi code trong Bài Doanh thu trong tuần nhằm tận dụng ArrayUtils class.

Đáp án

package weeklysales;

import util.ArrayUtils;

public class WeeklySales {
    private double[] figures;

    public WeeklySales(double... figures) {
        this.figures = figures; 
    }

    public double sum() {
        return ArrayUtils.sum(this.figures);
    }
    
    public double average() {
        return this.sum() / this.figures.length;
    }
    
    public double max() {
        return ArrayUtils.max(this.figures);
    }
    
    public double min() {
        return ArrayUtils.min(this.figures);
    }
}


V. Tài liệu tham khảo
  1. Tony Gaddis (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