Hiển thị các bài đăng có nhãn OOP. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn OOP. Hiển thị tất cả bài đăng

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

Chỉ có mã nguồn mới nói lên sự thật


Chỉ có mã nguồn mới nói lên sự thật


Peter Sommerlad


Tính đích thực của một chương trình được thể hiện qua mã (code) thi hành. Nếu chỉ ở dạng nhị phân (binary), code sẽ rất khó đọc! Tuy nhiên, mã nguồn (source code) cần sẵn có nếu đây là chương trình của bạn, là quá trình phát triển một phần mềm thương mại, là một dự án nguồn mở (open source), hay code trong một ngôn ngữ thông dịch động (dynamic interpreted language). Khi bạn đọc source code, ý nghĩa của chương trình cần phải toát lên một cách hiển nhiên. Muốn biết chương trình làm gì, source code là thứ sau cùng bạn cần phải đọc. Thậm chí tài liệu thu thập yêu cầu chính xác nhất cũng không thể nói lên sự thật: nó không chi tiết những gì chương trình đang thật sự thi hành, đó chỉ là ý định mức cao của người phân tích yêu cầu. Tài liệu thiết kế có thể trình bày thiết kế đã được vạch sẵn, song lại thiếu chi tiết cần thiết của công đoạn hiện thực. Các tài liệu này có thể đã không còn đồng bộ với thực tế ... hoặc có lẽ chúng đã thất lạc. Hay chưa từng được viết ra. Source code có thể là thứ duy nhất còn sót lại.

Với hiểu biết như trên, bạn hãy tự vấn xem code của bạn phải sáng tỏ đến đâu để code có thể kể cho bạn hoặc cho một lập trình viên bất kỳ về điều mà code đang làm.

Bạn có thể bảo, "Ôi chào, chú thích trong code của tôi sẽ giải thích mọi điều bạn cần." Nhưng hãy nhớ cho rằng chú thích không là code. Chúng có thể sai sót như các dạng tài liệu khác. Truyền thống mặc nhiên cho rằng chú thích là hành vi tốt, nên một số lập trình viên mù quáng chú thích thật nhiều, thậm chí còn tường thuật, giảng giải những chi tiết vụn vặt và hiển nhiên trong code. Đây là con đường sai lầm trong nỗ lực khiến code của bạn trở nên trong sáng.

Nếu bạn cần chú thích cho code, hãy xét đến khả năng tái cấu trúc (refactoring) để không cần phải chú thích nữa. Chú thích dài dòng có thể gây rối màn hình và thậm chí còn bị IDE (môi trường phát triển tích hợp) của bạn tự động che khuất. Nếu cần diễn giải về một thay đổi, hãy gửi thông điệp vào hệ thống kiểm soát phiên bản (version control system) chứ đừng chú thích trong code.

Bạn có thể làm gì để thật sự khiến code của bạn nói lên sự thật càng sáng tỏ càng tốt? Hãy gắng đặt tên cho đúng. Cấu trúc code theo tính năng cố kết, nhờ đó còn tạo điều kiện dễ dàng cho việc đặt tên. Phân rã code nhằm đạt được tính trực giao. Hãy viết mã kiểm thử tự động (automated tests) hầu giải thích hành vi dự kiến và kiểm chứng giao diện. Không ngừng refactor sau khi đã biết cách đưa ra một giải pháp đơn giản hơn và hoàn thiện hơn. Hãy biến code của bạn trở nên càng dễ đọc và dễ hiểu chừng nào, càng tốt chừng ấy.

Hãy xem code của bạn là một công trình soạn thảo, tựa một bài thơ, bài luận, blog trước công chúng, hay một điện thư (email) quan trọng. Hãy cẩn thận trau chuốt hầu biểu đạt được mục đích và trực tiếp thể hiện điều mà code đang làm; sao cho code có thể tiếp tục diễn đạt được ý định của bạn khi bạn không còn ở đó. Nhớ rằng code nào ích lợi sẽ được sử dụng lâu dài hơn ý định ban đầu. Các lập trình viên bảo trì sẽ cảm ơn bạn. Và nếu bạn là một lập trình viên bảo trì, đồng thời code bạn đang bảo trì không dễ toát lên sự thật, hãy chủ động vận dụng những hướng dẫn vừa nêu. Hãy thiết lập một tiêu chuẩn lành mạnh cho code, và duy trì sự lành mạnh của chính bạn.


Nguồn

Sommerlad P. (2010) Only the Code Tells the Truth, 97 Things Every Programmer Should Know, Henney K. (ed), O'Reilly, Sebastopol, CA.

OOP - Bài 11: Doanh thu trong tháng


Thiết kế chương trình hướng đối tượng


    Center of Excellence


    Bài 11
    Doanh thu trong tháng



Cách nhanh nhất để làm được nhiều việc
là chỉ làm đúng một việc tại một thời điểm
-- Richard Cech


I. Bài toán


Doanh thu hàng ngày trong tháng của công ty được lưu trữ trong một tập tin văn bản (text file). Hãy phát triển chương trình tính tổng doanh thu tháng và doanh thu bình quân ngày. Sau đây là ví dụ về doanh thu tháng:

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

(1) Tính tổng doanh thu tháng (total sales)
  • Input: tên text file (file name)
  • Output: total sales
(2) Tính doanh thu bình quân ngày (average daily sales)
  • Input: file name
  • Output: average daily sales


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


Tương tự như bài trước, ở đây ta phải làm việc với dữ liệu lưu trữ, nên ta tạo một class trong data tier, làm đại diện cho text file này.

Hình 2


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



No
fileName
sum?
averageDailySales?
1
April2012.txt theo Hình 1 (file hợp lệ, gồm nhiều dòng)
56437.42 (có thể sử dụng phần mền bảng tính để tính ra kết quả này)
1881.25 (làm tròn về hai chữ số sau dấu chấm thập phân từ kết quả 1881.247333...)
2
oneLine.txt (1 1458.32) (file hợp lệ, gồm một dòng)
1458.32
1458.32
3
File không tồn tại hoặc sai đường dẫn (nonexistence.txt)
Không có
Không có
4
empty.txt (file trống)
Không có
Không có
5
invalid1.txt (file không hợp lệ do chứa dòng trống)
Không có
Không có
6
invalid2.txt (file không hợp lệ do có dòng chứa không đủ hai giá trị)
Không có
Không có
7
invalid3.txt (file không hợp lệ do có dòng chứa giá trị không phải là số)
Không có
Không có



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


  • Project: MonthlySales
  • Package: data


4.1. Phát triển test method cho sum()


4.1.1. Trường hợp file hợp lệ, chứa một hay nhiều dòng


package data;

import java.io.FileNotFoundException;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class MonthlySalesFacadeTest {
    @Test
    public void sum() throws FileNotFoundException {
        assertEquals(56437.42, new MonthlySalesFacade("April2012.txt").sum(), 0.0);
        assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
    }
}

Sau khi đã có test method, ta tiến hành phát triển method chính. Ban đầu, cách làm tương tự như bài trước.

package data;

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

public class MonthlySalesFacade {
    private final String fileName;

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

Tuy nhiên, cấu trúc file ở bài này khác bài trước ở chỗ: mỗi dòng (line) chứa hai giá trị, một số nguyên và một số thực. Vì vậy trước tiên ta cần đọc ra nguyên một dòng.

    public double sum() throws FileNotFoundException {
        try (Scanner scanner = new Scanner(new File(this.fileName))) {
            double acc = 0.0;
            while (scanner.hasNext()) {
                String line = scanner.nextLine();
                ???
            }
            return acc;
        }
    }
Sau đó ta dùng thêm một Scanner object để thực hiện việc trích data từ line đó. Khi trích giá trị nguyên bằng nextInt(), ta không xử lý gì cả vì giá trị này không phục vụ cho việc tính tổng.

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

Đến đây nếu thi hành test method, ta gặp tín hiệu đỏ, do khả năng biểu diễn và tính toán không thật chính xác trên số thực của computer. Ta điều chỉnh lại sai số cho phép như sau

    @Test
    public void sum() throws FileNotFoundException {
        assertEquals(56437.42, new MonthlySalesFacade("April2012.txt").sum(), 2E-11);
        assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
    }



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


Ta làm tương tự như bài trước.

    @Test(expected=FileNotFoundException.class)
    public void sumNonexistentFile() throws FileNotFoundException {
        new MonthlySalesFacade("nonExistence.txt").sum();
    }


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


Cách làm tương tự như bài trước.

    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()) {
                String line = scanner.nextLine();
                Scanner lineScanner = new Scanner(line);
                lineScanner.nextInt();
                acc += lineScanner.nextDouble();
            }
            return acc;
        }
    }


4.1.4. Trường hợp file chứa dòng trống hoặc chứa dòng không đủ hai giá trị


Tương tự như trường hợp file trống, kết quả dự kiến sẽ là ngoại lệ NoSuchElementExceoption.

    @Test(expected=NoSuchElementException.class)
    public void sumEmptyFile() throws FileNotFoundException {
        new MonthlySalesFacade("empty.txt").sum();
    }
    
    @Test(expected=NoSuchElementException.class)
    public void sumFileWithBlankLine() throws FileNotFoundException {
        new MonthlySalesFacade("invalid1.txt").sum();
    }
    
    @Test(expected=NoSuchElementException.class)
    public void sumFileWithInsufficientData() throws FileNotFoundException {
        new MonthlySalesFacade("invalid2.txt").sum();
    }


4.1.5. Trường hợp file chứa giá trị không phải là số

Kết quả dự kiến sẽ là ngoại lệ InputMismatchExceoption.

    @Test(expected=InputMismatchException.class)
    public void sumFileWithNonNumberData() throws FileNotFoundException {
        new MonthlySalesFacade("invalid3.txt").sum();
    }


4.2. Phát triển test method cho averageDailySales()


public class MonthlySalesFacadeTest {
    @Test
    public void averageDailySales() {
        assertEquals(1881.25,
            new MonthlySalesFacade("April2012.txt").averageDailySales(), 0.003);
    }
    
    @Test
    public void sum() throws FileNotFoundException {
        assertEquals(56437.42, new MonthlySalesFacade("April2012.txt").sum(), 2E-11);
        assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
    }
    
    ...
}

Nhận thấy sự xuất hiện của code duplication trong đoạn code trên, ta tiến hành loại bỏ bằng cách đưa phần giống nhau ra ngoài để có thể dùng chung.

public class MonthlySalesFacadeTest {
    private MonthlySalesFacade aprilSales = new MonthlySalesFacade("April2012.txt");
    
    @Test
    public void averageDailySales() throws FileNotFoundException {
        assertEquals(1881.25, this.aprilSales.averageDailySales(), 0.003);
    }
    
    @Test
    public void sum() throws FileNotFoundException {
        assertEquals(56437.42, this.aprilSales.sum(), 2E-11);
        assertEquals(1458.32, new MonthlySalesFacade("oneLine.txt").sum(), 0.0);
    }
    
    ...
}
Đến đây ta bắt đầu phát triển averageDailySales() cho MonthlySalesFacade class. Doanh thu bình quân ngày sẽ bằng tổng doanh thu tháng chia cho tổng số ngày, ta viết

public class MonthlySalesFacade {
    private final String fileName;

    public MonthlySalesFacade(String fileName) {
        this.fileName = fileName;
    }
    
    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()) {
                String line = scanner.nextLine();
                Scanner lineScanner = new Scanner(line);
                lineScanner.nextInt();
                acc += lineScanner.nextDouble();
            }
            return acc;
        }
    }
    
    public double averageDailySales() {
        return this.sum() / this.count();
    }
}

Rồi ta phát triển tiếp count() để đếm tổng số ngày, tức tổng số dòng chứa trong file. Tuy nhiên, mặc dù averageDailySales() ở trên chỉ chứa một dòng ngắn ngủi nhưng đó lại là method không hiệu quả về tốc độ thi hành. Đọc kỹ ta thấy method này đã phải thao tác trên file đến hai lần, một lần trong lệnh gọi sum() và một lần khác trong lệnh gọi count(). Biết rằng khi làm việc với bộ nhớ ngoài (external memory), chẳng hạn với files hoặc databases, tốc độ thi hành chương trình sẽ bị ảnh hưởng đáng kể. Vì vậy, ta cần hạn chế số lần truy cập external memory. Ta làm theo cách sau: khi đọc data từ file để tính trung bình, ta đồng thời làm hai việc: vừa tính tổng và vừa đếm số dòng. Ta đếm số dòng bằng cách ban đầu đặt count về 0, tức ban đầu chưa đếm được dòng nào trong file. Sau đó bên trong vòng lặp while, mỗi lần đọc được một dòng thì tăng count thêm 1 bằng toán tử ++.

public class MonthlySalesFacade {
    private final String fileName;

    public MonthlySalesFacade(String fileName) {
        this.fileName = fileName;
    }
    
    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()) {
                String line = scanner.nextLine();
                Scanner lineScanner = new Scanner(line);
                lineScanner.nextInt();
                acc += lineScanner.nextDouble();
            }
            return acc;
        }
    }
    
    public double averageDailySales() 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;
            int count = 0;
            while (scanner.hasNext()) {
                String line = scanner.nextLine();
                Scanner lineScanner = new Scanner(line);
                lineScanner.nextInt();
                acc += lineScanner.nextDouble();
                count++;
            }
            return acc / count;
        }
    }
}

Cách làm trên khiến averageDailySales() trở nên khó hiểu, do nó phải giải quyết đồng thời hai việc. Bù lại, tốc độ thi hành được cải tiến. Nói chung, nếu không phải làm việc với external memory, ta nên tuân thủ nguyên tắc đơn thể (modularization) hay đơn nhiệm (single responsibility): Mỗi method chỉ nên làm một việc.


III. Tổng kết


Bài viết trình bày kỹ thuật xử lý dữ liệu có cấu trúc được lưu trong text files. Để cải thiện hiệu suất thi hành khi làm việc với external memory, chương trình đã vi phạm nguyên tắc đơn nhiệm.


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

Thứ Hai, 19 tháng 3, 2012

Hãy chia sẻ kiến thức và kinh nghiệm


Hãy chia sẻ kiến thức và kinh nghiệm

Paul W. Homer


Ta học hỏi được rất nhiều từ cả kinh nghiệm thành công lẫn thất bại. Trong một nền công nghiệp non trẻ như phát triển phần mềm, phổ biến kinh nghiệm và tri thức đóng vai trò sống còn hầu hỗ trợ tiến trình phát triển vững bền. Bài học của một nhóm ở nơi khỉ ho cò gáy vẫn có thể tác động đến toàn thế giới.

Thật sự mà nói, kiến thức cơ bản của ta về phát triển phần mềm, tức những tri thức đúng tuyệt đối về mặt lý thuyết, còn nhỏ nhoi so với yêu cầu phát triển thành công dự án. Để bù đắp, chúng ta buộc phải phỏng đoán dựa trên đánh giá trực quan, hoặc thậm chí làm bừa. Bằng cách đó, các dự án phát triển quan trọng có thể cung cấp bằng chứng thực nghiệm về thành công cũng như thất bại. Ta từng bước thử nghiệm qua nhiều trường hợp, để rồi tái áp dụng vào nền công nghiệp trên qui mô tổng thể.

Ở mức độ cá thể, ai cũng gắng tiến bộ để học được cách xây dựng hệ thống ngày càng lớn. Con đường nghề nghiệp sẽ đưa ta đến những thử thách ngày càng cao, vì vậy mà ta muốn kinh nghiệm quá khứ có thể hướng dẫn chúng ta. Muốn là một chuyện, song để học được từ kinh nghiệm, ta thường phải hợp lý hóa nó. Cách hay nhất và dễ nhất là gắng diễn giải kinh nghiệm cho một người khác.

Thảo luận luôn giúp chỉ ra thiếu sót. Bạn chưa thật sự hiểu nếu bạn chưa giải thích được một cách lưu loát. Chính nhờ nỗ lực trình bày và thảo luận mà ta có thể biến kinh nghiệm thành tri thức.

Mặt khác, tuy có thể đã trải qua nhiều kinh nghiệm cụ thể, suy luận rút ra từ đó có thể chưa thật chính xác trong bối cảnh chung. Có thể ta chưa thành công hoặc tài trí như mong muốn. Tất nhiên việc thử nghiệm kiến thức của bạn trước bàn dân thiên hạ sẽ thật đáng sợ, đặc biệt là lúc bạn nhận ra điều mà bạn trân quý chỉ là hoang tưởng, sai lầm, hay chẳng bao giờ là chân lý; thật khó chấp nhận khi mình sai.

Suy cho cùng, không phải mọi điều trong tâm trí con người chúng ta đều đúng; không phải mọi thứ mình nghĩ cũng đều hợp lý. Chỉ khi nào biết thừa nhận sai lầm thì ta mới có cơ may tiến bộ. Ngạn ngữ "thất bại là mẹ thành công" là chân lý. Nếu ý tưởng và niềm tin của ta không trụ vững khi tranh luận, tốt hơn là biết được ngay, để sau này khỏi phải xây dựng trên đó.

Ta thật sự mong muốn chia sẻ tri thức và kinh nghiệm để ngành công nghiệp tiến bộ; đồng thời nhận thức rằng điều này sẽ giúp mình hiểu biết và sửa sai. Trước hiện trạng phát triển phần mềm, ta cần tận dụng mọi cơ hội hầu chia sẻ điều mình biết, điều ta nghĩ rằng ta biết, và những gì ta đã chứng kiến. Nếu hỗ trợ đồng nghiệp xung quanh tiến bộ, họ sẽ giúp ta phát huy được hết tiềm năng của mình.


Nguồn

Homer P.W. (2009) Share your knowledge and experiences, 97 Things Every Software Architect Should Know, Monson-Haefel R. (ed), O'Reilly, Sebastopol, CA.

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.