Phát triển ứng dụng máy tính thực hiện các phép toán cơ bản cho học sinh tiểu học

Giới thiệu

Dự án MathExam, phiên bản V2.0.0, được bổ sung chức năng tạo bài tập toán về các phép tính cộng, trừ, nhân, chia cho học sinh lớp 3.

Phân tích và Lập kế hoạch

Bảng dưới đây trình bày ước tính thời gian ban đầu và thời gian thực tế cho từng giai đoạn phát triển phần mềm theo quy trình PSP 2.1:

Giai đoạn Mô tả Thời gian ước tính (phút) Thời gian thực tế (phút)
Planning Lập kế hoạch - -
    Estimate Ước tính thời gian 5 3
Development Phát triển - -
    Analysis Phân tích yêu cầu (bao gồm học công nghệ mới) 90 720
    Design Spec Tạo đặc tả thiết kế 20 20
    Design Review Rà soát thiết kế 10 30
    Coding Standard Thiết lập quy tắc code 30 10
    Design Thiết kế chi tiết 30 20
    Coding Viết mã 360 600
    Code Review Rà soát mã 20 300
    Test Kiểm thử (tự kiểm thử, sửa lỗi, nộp) 50 20
Reporting Báo cáo - -
    Test Report Báo cáo kiểm thử 30 20
    Size Measurement Đo lường khối lượng công việc 30 20
    Postmortem & Process Improvement Plan Rút kinh nghiệm và kế hoạch cải tiến quy trình 30 30
Tổng cộng 1793 1793

Phân tích Yêu cầu

  • Đặc điểm 1: Mỗi biểu thức chứa từ 2 đến 4 toán tử.
  • Đặc điểm 2: Kết quả của phép trừ không được là số âm.
  • Đặc điểm 3: Phép chia không được có số dư và số chia không được bằng 0.
  • Đặc điểm 4: Hỗ trợ dấu ngoặc đơn.

Dựa trên các yêu cầu trên, chương trình cần đáp ứng các tiêu chí sau:

  • Tạo ra các biểu thức toán học với nhiều toán tử.
  • Phân tích và xác định toán tử để tạo số ngẫu nhiên.
  • Sử dụng thuật toán Shunting-yard và biểu thức hậu tố (Reverse Polish Notation - RPN) để xử lý phép tính.

Thiết kế

1. Ý tưởng Thiết kế

  1. Bước 1: Phát sinh các bài toán theo yêu cầu.
  2. Bước 2: Lưu trữ biểu thức vào tệp văn bản.
  3. Bước 3: Tiếp nhận tham số từ dòng lệnh để điều khiển chương trình.
  4. Bước 4: Chuyển đổi biểu thức trung tố sang biểu thức hậu tố.
  5. Bước 5: Tính toán giá trị của biểu thức hậu tố.
  6. Bước 6: Sửa lỗi, chuẩn hóa tên biến/hàm và cải thiện chất lượng mã nguồn.

2. Giải pháp Thực hiện

  • Chuẩn bị: Tạo kho lưu trữ trên GitHub, clone về máy và tạo thư mục "Pair".
  • Sử dụng IDE MyEclipse để tạo lớp chứa phương thức main và các hàm hỗ trợ.
  • Các điểm kỹ thuật quan trọng:
    • Xử lý linh hoạt các trường hợp nhập tham số dòng lệnh như -n 10 -grade 3 hoặc -grade 3 -n 10.
    • Ưu tiên tính toán các biểu thức trong ngoặc đơn.
    • Thêm dấu ngoặc đơn vào biểu thức một cách hợp lý.
    • Triển khai thuật toán biểu thức hậu tố.
    • Chức năng ghi lại các câu hỏi sai (chưa được triển khai trong phiên bản này).

Lập trình

Lưu ý: Các chức năng sau chưa được hoàn thiện trong phiên bản này:

  • Xử lý lỗi chia cho 0.
  • Xử lý lỗi kết quả phép trừ là số âm.
  • Phạm vi số trong các phép tính là từ 0 đến 9 (không bao gồm số có 2 chữ số).

1. Nhật ký Gỡ lỗi

  • Nhật ký 1:

    Lỗi nhận diện toán tử: Ký tự 'x' (dấu nhân viết thường) bị nhận diện nhầm thành ký tự hợp lệ khi đưa vào ngăn xếp.

    Mã nguồn lỗi:

    private static boolean isOperator(String operator){
        if (operator.equals("+")||operator.equals("-")||operator.equals("×")||operator.equals("÷")||operator.equals("(")||operator.equals(")")) {
            return true;
        }
        return false;
    }
                

    Giải pháp: Chuẩn hóa ký tự nhân thành '×'.

  • Nhật ký 2:

    Lỗi ngoại lệ (null pointer exception) khi tạo bài tập lớp 3 do sử dụng sai chỉ số vòng lặp `i`.

    Mã nguồn lỗi:

    for (int j = 0; j < count; j++) {
        .... (mã bị lược bỏ)
        str_ArithmeticProblem[i] = "( " +n1 + " " + cs[c1] + " " + n2 +" ) " + " " + cs[c2] + " " + n3;
        .... (mã bị lược bỏ)
    }
                

    Giải pháp: Sửa lỗi chỉ số vòng lặp thành `j`.

    for (int j = 0; j < count; j++) {
        .... (mã bị lược bỏ)
        str_ArithmeticProblem[j] = "( " +n1 + " " + cs[c1] + " " + n2 +" ) " + " " + cs[c2] + " " + n3;
        .... (mã bị lược bỏ)
    }
                

2. Mã nguồn Quan trọng

  • Thuật toán Shunting-yard (Chuyển đổi Trung tố sang Hậu tố):
  • // Thuật toán Shunting-yard - Chuyển đổi biểu thức trung tố sang hậu tố
    private void toPostfixExpression(String str_mix){
        int len = str_mix.length();
        char c, nextChar;
        String sc;
        for (int i = 0 ; i <= len - 1 ; i++) {
            c = str_mix.charAt(i);
            sc = String.valueOf(c);
            if(isOperator(sc)) { // Kiểm tra nếu là toán tử
                if(operators.isEmpty()){ // Ngăn xếp rỗng thì đẩy vào
                    operators.push(sc);
                } else {
                    if(priority(operators.peek()) < priority(sc) && !sc.equals(")")){
                        // Toán tử trên đỉnh ngăn xếp có ưu tiên thấp hơn toán tử hiện tại và không phải là ')' thì đẩy vào
                        operators.push(sc);
                    } else if(priority(operators.peek()) >= priority(sc) && !sc.equals(")")){
                        // Nếu toán tử trên đỉnh ngăn xếp có ưu tiên cao hơn hoặc bằng toán tử hiện tại và không phải là ')'
                        while(!operators.empty() && !operators.peek().equals("(") // Ngăn xếp không rỗng và đỉnh ngăn xếp không phải '('
                                && priority(operators.peek()) >= priority(sc)){ // Ưu tiên của đỉnh ngăn xếp lớn hơn hoặc bằng toán tử hiện tại
                            operator_Add = operators.pop();
                            postfixExpression.append(operator_Add);
                            operand.push(operator_Add); // Giả định operand stack chỉ lưu trữ toán tử để xử lý sau
                        }
                        operators.push(sc); // Đẩy toán tử hiện tại vào ngăn xếp
                    } else if(sc.equals(")")){ // Nếu toán tử hiện tại là ')'
                        // Lấy các toán tử từ ngăn xếp cho đến khi gặp '('
                        do {
                            operator_Add = operators.pop();
                            postfixExpression.append(operator_Add);
                            operand.push(operator_Add);
                        } while (!operators.peek().equals("("));
                        operators.pop(); // Loại bỏ '(' khỏi ngăn xếp
                    }
                }
            } else { // Không phải là toán tử
                if(!sc.equals(" ")){ // Nếu không phải khoảng trắng
                    postfixExpression.append(sc);
                    operand.push(sc); // Giả định operand stack chỉ lưu trữ toán hạng để xử lý sau
                }
            }
        }
        // Đẩy các toán tử còn lại trong ngăn xếp ra
        while(!operators.empty()){
            operator_Add = operators.pop();
            postfixExpression.append(operator_Add);
            operand.push(operator_Add);
        }
    }
            
  • Hàm Tính toán Biểu thức Hậu tố (Reverse Polish Notation):
  • // Hàm tính toán biểu thức hậu tố
    private int reversePolish() {
        // TODO Auto-generated method stub
        char c;
        int len = postfixExpression.toString().length();
        for (int i = 0; i < len; i++) {
            c = postfixExpression.charAt(i);
            // Nếu không phải là toán tử thì chuyển sang số nguyên và đẩy vào ngăn xếp số
            if(!isOperator(String.valueOf(c))){
                postfixNumber.push(Integer.parseInt(String.valueOf(c)));
            } else{
                // Nếu là toán tử, lấy hai toán hạng từ ngăn xếp, thực hiện phép tính và đẩy kết quả trở lại
                int m = postfixNumber.pop(); // Toán hạng thứ hai
                int n = postfixNumber.pop(); // Toán hạng thứ nhất
                String operator = String.valueOf(c);
                postfixNumber.push(Calculation(n, m, operator)); // Thực hiện phép tính và đẩy kết quả
            }
        }
        // Kết quả cuối cùng nằm trên đỉnh ngăn xếp
        return postfixNumber.pop();
    }
            

3. Quy tắc Mã nguồn

  • Tên lớp theo quy tắc UpperCamelCase.
  • Tên phương thức, tham số, biến thành viên, biến cục bộ theo quy tắc lowerCamelCase (họ camel).
  • Quy tắc đặt tên biến và hằng số:
    • Biến private/protected/default: thêm tiền tố 'm' viết thường.
    • Biến static: thêm tiền tố 's' viết thường.
    • Các biến khác: bắt đầu bằng chữ cái viết thường.
    • Hằng số static final: viết hoa toàn bộ.
  • Khai báo mảng: đặt dấu ngoặc vuông ngay sau kiểu dữ liệu.
  • Quy ước sử dụng dấu ngoặc nhọn:
    • Nếu khối lệnh rỗng: viết gọn là {}, không xuống dòng.
    • Nếu khối lệnh không rỗng:
      • Dấu ngoặc nhọn mở { viết liền với dòng trước, không xuống dòng.
      • Dấu ngoặc nhọn mở { xuống dòng sau nó.
      • Dấu ngoặc nhọn đóng } xuống dòng trước nó.
      • Nếu sau dấu ngoặc nhọn đóng } có các mã lệnh khác như else: không xuống dòng.
      • Nếu dấu ngoặc nhọn đóng } kết thúc một khối lệnh: bắt buộc phải xuống dòng sau nó.
  • Không có khoảng trắng giữa dấu ngoặc đơn mở ( và ký tự theo sau, hoặc giữa ký tự đứng trước dấu ngoặc đơn đóng ) và dấu ngoặc đó.

4. Quy trình Lập trình Cặp (Pair Programming)

  • Phương pháp áp dụng: Ping-Pong.
  • Thảo luận ban đầu về quy trình lập trình, phương pháp thực hiện và xác định các điểm khó khăn.
  • Phân công nhiệm vụ cụ thể cho từng thành viên dựa trên chức năng cần triển khai.
  • Trao đổi ý kiến và cập nhật tiến độ thường xuyên trong quá trình lập trình.
  • Tích hợp mã nguồn và thực hiện rà soát mã.

Kiểm thử

Bảng dưới đây tóm tắt kết quả kiểm thử với các lệnh và đầu ra tương ứng:

STT Lệnh Kiểm thử Kết quả Dự kiến Kết quả Thực tế
1 java MathExam -n 10 -grade 1 Xuất 10 bài toán lớp 1 Khớp với kết quả dự kiến
2 java MathExam -n 10 -grade 2 Xuất 10 bài toán lớp 2 Khớp với kết quả dự kiến
3 java MathExam -n 10 -grade 3 Xuất 10 bài toán lớp 3 Khớp với kết quả dự kiến
4 java MathExam -grade 3 -n 10 Xuất 10 bài toán lớp 3 Khớp với kết quả dự kiến
5 java MathExam -grade 2 -n 10 Xuất 10 bài toán lớp 2 Khớp với kết quả dự kiến
6 java MathExam -grade 1 -n 10 Xuất 10 bài toán lớp 1 Khớp với kết quả dự kiến
7 java MathExam -g 3 -n 10 Sai quy tắc nhập tham số, chương trình kết thúc Khớp với kết quả dự kiến
8 java MathExam -grade 3 -c 10 Sai quy tắc nhập tham số, chương trình kết thúc Khớp với kết quả dự kiến
9 java MathExam 10 -grade 1 Sai quy tắc nhập tham số, chương trình kết thúc Khớp với kết quả dự kiến
10 java MathExam -g 10 -c 1 Sai quy tắc nhập tham số, chương trình kết thúc Khớp với kết quả dự kiến

Kết luận

  • Việc phân tích yêu cầu kỹ lưỡng ngay từ đầu dự án sẽ giúp xây dựng nền tảng vững chắc và giảm thiểu việc sửa đổi mã nguồn không cần thiết.
  • Khi độ phức tạp kỹ thuật tăng lên, việc bổ sung chú thích cho mã nguồn là vô cùng quan trọng.
  • Trong quá trình lập trình cặp, cần phân công nhiệm vụ rõ ràng, thống nhất về cách tiếp cận mã nguồn và duy trì nhịp độ làm việc đồng đều giữa các thành viên.
  • Nên thường xuyên đẩy mã nguồn lên GitHub để quản lý phiên bản. Điều này giúp dễ dàng khôi phục mã nguồn khi gặp sự cố, như trường hợp thay đổi mã nguồn và không thể hoàn tác.

Thẻ: Java Thuật toán Shunting-yard Biểu thức hậu tố Lập trình cặp Quản lý phiên bản

Đăng vào ngày 5 tháng 10 lúc 08:55