Thứ Bảy, 17 tháng 3, 2012

OOP - Bài 10: Thao tác trên tập tin văn bản (Text Files)



Center of Excellence

    Bài 10
    Thao tác trên tập tin văn bản (Text Files)


Kiểm thử phải trở thành yếu tố bắt buộc trong quá trình phát triển,
và cuộc hôn phối giữa phát triển và kiểm thử là điểm hội tụ chất lượng
-- James A. Whittaker

I. Bài toán
Hãy phát triển chương trình tính tổng tất cả các giá trị được lưu trong một tập tin văn bản (text file), trong đó các giá trị cách nhau bởi một hay nhiều khoảng trắng (white space). White space là một trong 5 characters sau: '\t' (tab, horizontal tabulation), '\n' (xuống dòng, new line), '\f' (nạp giấy, form feed), '\r' (trở về đầu dòng), và ' ' hay ' ' (dấu cách, space). Sau đây là ví dụ về một text file:
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 tổng tất cả các giá trị trong một text file.
  • Input: tên text file (file name)
  • Output: tổng (sum)


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

Theo kinh nghiệm thiết kế, khi phải làm việc trên dữ liệu lưu trữ, file hoặc cơ sở dữ liệu (database), nhà thiết kế sẽ tạo ra một tầng chuyên quản lý dữ liệu (data layer hoặc data tier), bao gồm bốn thao tác cơ bản, gọi tắt là CRUD (Create – Tạo hoặc Thêm, Read – Đọc, Update – Sửa, Delete – Xóa), cùng một số tính toán đơn giản như đếm (count), tính tổng (sum), và tính trung bình (average). Data tier có một hay nhiều bộ phận chuyên trách làm đại diện để giao tiếp với bên ngoài, gọi là facade.


Với bài toán trên, ta cần làm việc với text file để tính tổng. Do tính tổng là một trong những thao tác cơ bản của data tier, ta tạo một class trong data tier đại diện cho text file này.

Hình 2


Bước 3. Xây dựng test cases

Ta có thể chia test cases thành hai trường hợp: file thực sự có trong đĩa cứng (hard disk) và file không tồn tại do sai tên file hoặc sai đường dẫn. Khi file có trong hard disk, các trường hợp sau có thể xảy ra: file hoàn toàn trống tức không có data; file chứa một hay nhiều số; hoặc file chứa data không phải là số.


sum()
No fileName sum?
1
manyNumbers.txt (8.7 7.9 3.0 9.2 12.6)
41.4
2
oneNumber.txt (8.7)
8.7
3
empty.txt (file trống, không có data)
Không có
4
invalid.txt (8.7 abc 7.9 3.0 9.2 12.6)
Không có
5
File không tồn tại hoặc sai đường dẫn (nonexistence.txt)
Không có


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

  • Project: TextFileOfNumbers
  • Package: data

Ta từng bước giải quyết từng test case. Trước hết là trường hợp file chứa data hợp lệ.

4.1. Trường hợp file chứa một hay nhiều số

Ta tạo hai text files lấy tên là manyNumbers.txtoneNumber.txt, đầu tiên là manyNumbers.txt. Right-click project, chọn New, rồi chọn File để chuyển sang New File window.

Hình 3

Nhập tên file vào ô File name:, rồi click Finish.

Hình 4

Nhập các giá trị vào file, rồi nhấn Ctrl+S để lưu. Ta làm tương tự các bước trên cho file oneNumber.txt.

Đến đây, ta phát triển test method cho trường hợp này.

package data;

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

public class NumberFacadeTest {
    @Test
    public void sum() {
        assertEquals(41.4, new NumberFacade("manyNumbers.txt").sum(), 0.0);
        assertEquals(8.7, new NumberFacade("oneNumber.txt").sum(), 0.0);
    }
}

Khi phát triển sum(), ta sử dụng câu lệnh try-with-resources, trong đó có Scanner object để mở file.

package data;

import java.io.File;
import java.util.Scanner;

public class NumberFacade {
    private final String fileName;

    public NumberFacade(String fileName) {
        this.fileName = fileName;
    }
    
    public double sum() {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            return ???;
        }
    }
}

Tuy nhiên, Java buộc ta phải xử lý ngoại lệ FileNotFoundException (không tìm thấy file) khi thực hiện việc mở file. Vì vậy ta bổ sung đoạn throws FileNotFoundException vào sau dòng khai báo của sum() method, để cho biết rằng khi không tìm thấy file thì việc xử lý exception này sẽ được đùn đẩy cho nơi phát ra lệnh gọi sum().

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class NumberFacade {
    private final String fileName;

    public NumberFacade(String fileName) {
        this.fileName = fileName;
    }
    
    public double sum() throws FileNotFoundException {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            return ???;
        }
    }
}

Một khi đã mở được file, ta dùng bộ tích lũy acc (viết tắt của accumulator) để lưu trữ kết quả tính toán. Ban đầu acc chưa có gì nên bằng 0.

    public double sum() throws FileNotFoundException {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            double acc = 0.0;
            return ???;
        }
    }

Sau đó ta dùng vòng lặp while kiểm tra xem trong file có chứa data không (hasNext).

    public double sum() throws FileNotFoundException {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            double acc = 0.0;
            while (scanner.hasNext())
                ???
            return acc;
        }
    }

Nếu có thì đọc giá trị đó ra khỏi file (nextDouble), cộng dồn vào bộ tích lũy acc, rồi lặp lại việc kiểm tra và cộng dồn cho đến khi không còn data nào trong file mà chưa được đọc. Lưu ý rằng ở đây ta đã sử dụng toán tử += để cộng dồn một số thực double vào bộ tích lũy.

    public double sum() throws FileNotFoundException {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            double acc = 0.0;
            while (scanner.hasNext())
                acc += scanner.nextDouble();
            return acc;
        }
    }

Trước khi thi hành test class để kiểm thử kết quả, ta cần bổ sung đoạn throws FileNotFoundException vào sau dòng khai báo của test method.

public class NumberFacadeTest {
    @Test
    public void sum() throws FileNotFoundException {
        assertEquals(41.4, new NumberFacade("manyNumbers.txt").sum(), 0.0);
        assertEquals(8.7, new NumberFacade("oneNumber.txt").sum(), 0.0);
    }
}


4.2. Trường hợp file không tồn tại hoặc sai đường dẫn

Khi phát triển test method cho trường hợp này, ta cần nghĩ ra một tên file rất đặc biệt sao cho file này khó có khả năng tồn tại trong hard disk, chẳng hạn nonexistence.txt.

public class NumberFacadeTest {
    @Test public void sum() throws FileNotFoundException {
        assertEquals(41.4, new NumberFacade("manyNumbers.txt").sum(), 0.0);
        assertEquals(8.7, new NumberFacade("oneNumber.txt").sum(), 0.0);
    }
    
    @Test(expected=NoSuchElementException.class)
    public void sumEmptyFile() throws FileNotFoundException {
        new NumberFacade("empty.txt").sum();
    }
    
    @Test(expected=FileNotFoundException.class)
    public void sumNonxistentFile() throws FileNotFoundException {
        new NumberFacade("nonexistence.txt").sum();
    }
}


4.3. Trường hợp file trống

Trước tiên ta tạo text file empty.txt trong thư mục gốc (root folder) của project. (Tất nhiên, ta có thể đặt file vào bất kỳ vị trí nào trong hard disk, và khi đó cần cung cấp đường dẫn chính xác đến file.) Sau khi tạo xong text file, ta tiếp tục phát triển test class. Dĩ nhiên là ta không thể tính tổng trên một file trống, do đó ta dự kiến sẽ nhận được NoSuchElementException (nghĩa là không thể có giá trị như thế).

public class NumberFacadeTest {
    @Test public void sum() throws FileNotFoundException {
        assertEquals(41.4, new NumberFacade("manyNumbers.txt").sum(), 0.0);
        assertEquals(8.7, new NumberFacade("oneNumber.txt").sum(), 0.0);
    }
    
    @Test(expected=NoSuchElementException.class)
    public void sumEmptyFile() throws FileNotFoundException {
        new NumberFacade("empty.txt").sum();
    }
}

Khi đó, trong NumberFacade, ta bổ sung đoạn code kiểm tra trường hợp file trống.

    public double sum() throws FileNotFoundException {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            if (scanner.hasNext() == false)
                throw new NoSuchElementException(this.fileName + " is empty");
            double acc = 0.0;
            while (scanner.hasNext())
                acc += scanner.nextDouble();
            return acc;
        }
    }


4.4. Trường hợp file chứa data không phải là số

Để kiểm tra việc thi hành chương trình đối với trường hợp file chứa data không phải là số, ta tạo thêm text file invalid.txt và chèn vào đó chuỗi ký tự abc.

public class NumberFacadeTest {
    @Test public void sum() throws FileNotFoundException {
        assertEquals(41.4, new NumberFacade("manyNumbers.txt").sum(), 0.0);
        assertEquals(8.7, new NumberFacade("oneNumber.txt").sum(), 0.0);
    }
    
    @Test(expected=NoSuchElementException.class)
    public void sumEmptyFile() throws FileNotFoundException {
        new NumberFacade("empty.txt").sum();
    }
    
    @Test(expected=FileNotFoundException.class)
    public void sumNonxistentFile() throws FileNotFoundException {
        new NumberFacade("nonexistence.txt").sum();
    }
    
    @Test public void sumInvalidFile() throws FileNotFoundException {
        new NumberFacade("invalid.txt").sum();
    }
}

Khi thi hành test class, ta nhận được tín hiệu đỏ và InputMismatchException (dữ liệu nhập không khớp với yêu cầu). Lý do là ở vòng lặp while trong sum() thuộc NumberFacade class, ta đã kiểm tra xem file có chứa data không bằng cách gọi hasNext(). Vì file chứa chuỗi “abc” nên dòng lệnh bên trong while được thực hiện, tức đọc ra một giá trị qua lệnh nextDouble() rồi cộng dồn vào bộ tích lũy. Nhưng giá trị đọc ra không phải là số nên nextDouble() buộc phải tung exception.

Để có được tín hiệu xanh khi thi hành test class, ta bổ sung expected attribute vào test method.

    @Test(expected=InputMismatchException.class)
    public void sumInvalidFile() throws FileNotFoundException {
        new NumberFacade("invalid.txt").sum();
    }


IV. Tổng kết


Bài viết đã trình bày việc thiết kế data tier chịu trách nhiệm tương tác với dữ liệu lưu trữ, đồng thời sử dụng cấu trúc
try-with-resources kết hợp với vòng lặp while để đọc data trong text file.


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

Không có nhận xét nào:

Đăng nhận xét