Khóa OOP người mới: Bài 2 — Cây ATM

Chào mừng bạn tiếp tục với hành trình học OOP! Chúng ta cùng bước sang BÀI 2: LỚP CÂY ATM (thuộc Lab 02).

🏧 BÀI 2: LỚP CÂY ATM - QUẢN LÝ TÀI KHOẢN VÀ THAO TÁC LIÊN KẾT ĐỐI TƯỢNG

1. Trực quan hóa tư duy OOP: Tương tác giữa các Object

Trong bài Phân số, ta đã thấy một phân số này thực hiện phép tính với một phân số khác. Ở bài ATM này, tư duy OOP thể hiện rất rõ ở hành động Chuyển tiền:

Bạn mang tài khoản của mình (TK A) đến cây ATM.

Bạn nhập số tài khoản người nhận (TK B) và số tiền cần chuyển.

Cây ATM sẽ thực hiện: Trừ tiền ở TK A và Cộng tiền vào TK B. Đây chính là sự tương tác trực tiếp giữa 2 đối tượng (Object) thuộc cùng một lớp ATM.

2. Đọc hiểu sơ đồ UML của lớp ATM

Dựa trên đề bài Lab 02, sơ đồ lớp ATM như sau:

Thuộc tính / Phương thức Cú pháp trong Java Ý nghĩa
soTK: String private String soTK; Số tài khoản ngân hàng (dạng chuỗi chữ/số)
soDu: double private double soDu; Số dư tiền hiện có trong tài khoản
+ ATM(stk, sd) public ATM(String stk, double sd) Hàm khởi tạo tài khoản với số TK và số dư ban đầu
+ laySTK(): String public String laySTK() Getter lấy ra số tài khoản
+ laySoDu(): double public double laySoDu() Getter lấy ra số dư hiện tại
+ toString(): String public String toString() Chuyển thông tin tài khoản thành chuỗi để in
+ napTien(st): void public void napTien(double st) Nạp thêm số tiền st vào tài khoản
+ rutTien(st): boolean public boolean rutTien(double st) Rút số tiền st. Trả về true nếu thành công, false nếu thất bại
+ chuyenTien(ATM tk, double st): boolean public boolean chuyenTien(ATM tk, double st) Chuyển tiền sang đối tượng tk khác

3. Phân tích & Cài đặt Mã nguồn Java Chi Tiết

Bước 3.1: Tạo lớp ATM.java

public class ATM {
    // 1. Thuộc tính đóng gói
    private String soTK;
    private double soDu;

    // 2. Phương thức khởi tạo (Constructor)
    public ATM(String soTK, double soDu) {
        this.soTK = soTK;
        // Kiểm tra điều kiện số dư không được âm khi khởi tạo
        if (soDu < 0) {
            System.out.println("Lỗi: Số dư ban đầu không được âm! Đặt mặc định = 0.");
            this.soDu = 0;
        } else {
            this.soDu = soDu;
        }
    }

    // 3. Các hàm Getter
    public String laySTK() {
        return this.soTK;
    }

    public double laySoDu() {
        return this.soDu;
    }

    // 4. Hàm nạp tiền
    public void napTien(double st) {
        if (st <= 0) {
            System.out.println("Lỗi: Số tiền nạp phải lớn hơn 0!");
            return;
        }
        this.soDu += st; // Cộng trực tiếp vào số dư hiện tại
        System.out.println("Nạp thành công " + st + " vào TK " + this.soTK);
    }

    // 5. Hàm rút tiền
    public boolean rutTien(double st) {
        if (st <= 0) {
            System.out.println("Lỗi: Số tiền rút phải lớn hơn 0!");
            return false;
        }
        // Kiểm tra điều kiện đủ tiền để rút
        if (st > this.soDu) {
            System.out.println("Rút tiền thất bại: Số dư TK " + this.soTK + " không đủ!");
            return false;
        }
        this.soDu -= st; // Trừ tiền khỏi số dư
        System.out.println("Rút thành công " + st + " từ TK " + this.soTK);
        return true;
    }

    // 6. Hàm chuyển tiền sang tài khoản khác (Đối tượng ATM khác)
    public boolean chuyenTien(ATM tkNhan, double st) {
        if (tkNhan == null) {
            System.out.println("Lỗi: Tài khoản nhận không tồn tại!");
            return false;
        }
        // Gọi hàm rutTien từ chính đối tượng hiện tại (this)
        // Nếu rút tiền thành công thì mới nạp tiền vào tài khoản nhận
        if (this.rutTien(st)) {
            tkNhan.napTien(st);
            System.out.println("Chuyển tiền thành công từ TK " + this.soTK + " sang TK " + tkNhan.laySTK());
            return true;
        }
        System.out.println("Chuyển tiền thất bại!");
        return false;
    }

    // 7. Hàm chuyển đối tượng thành dạng String biểu diễn thông tin
    @Override
    public String toString() {
        return "STK: " + this.soTK + " | Số dư: " + this.soDu;
    }
}

Bước 3.2: Viết chương trình chính MainATM.java (Thực thi các yêu cầu Lab 02)

public class MainATM {
    public static void main(String[] args) {
        // 1. Tạo danh sách 5 tài khoản, mỗi tài khoản có sẵn số tiền tối thiểu là 100
        ATM[] dsTK = new ATM[] {
            new ATM("1111111", 100),
            new ATM("2222222", 100),
            new ATM("3333333", 100),
            new ATM("4444444", 100),
            new ATM("5555555", 100)
        };

        System.out.println("=== DANH SÁCH TÀI KHOẢN BAN ĐẦU ===");
        inDanhSach(dsTK);

        System.out.println("\n=== THỰC HIỆN CÁC THAO TÁC BÀI TẬP ===");

        // 2. Tài khoản 1111111 nạp tiền vào tài khoản 1000
        dsTK[0].napTien(1000);

        // 3. Tài khoản 1111111 chuyển cho tài khoản 2222222 số tiền 500
        dsTK[0].chuyenTien(dsTK[1], 500);

        // 4. Tài khoản 2222222 rút số tiền 200
        dsTK[1].rutTien(200);

        // 5. In số tiền hiện có của các tài khoản
        System.out.println("\n=== SỐ TIỀN HIỆN CÓ CỦA CÁC TÀI KHOẢN ===");
        inDanhSach(dsTK);

        // 6. In tổng số tiền của các tài khoản
        double tongTien = 0;
        for (ATM tk : dsTK) {
            tongTien += tk.laySoDu();
        }
        System.out.println("\n=> TỔNG SỐ TIỀN CỦA TOÀN BỘ HỆ THỐNG: " + tongTien);
    }

    // Hàm phụ trợ để in danh sách tài khoản cho ngắn gọn
    public static void inDanhSach(ATM[] ds) {
        for (ATM tk : ds) {
            System.out.println(tk);
        }
    }
}

⚠️ Lỗi sai phổ biến người mới bắt đầu cần tránh!

Lỗi viết lặp lại code (Redundancy):

Mắc phải: Khi viết hàm chuyenTien(ATM tk, double st), người mới hay tự viết lại logic trừ tiền của tài khoản gửi và cộng tiền cho tài khoản nhận.

Lời khuyên OOP: Hãy tái sử dụng các phương thức đã có (rutTien và napTien). Giúp code gọn gàng và nếu sau này logic rút tiền thay đổi (ví dụ: mất thêm phí giao dịch 1100đ), bạn chỉ cần sửa ở 1 nơi.

Lỗi NullPointer khi chuyển tiền:

Mắc phải: Truyền một đối tượng null vào tham số chuyenTien(tk, st) mà không kiểm tra if (tk == null), dẫn đến ứng dụng bị crash.

📝 BÀI TẬP TỰ RÈN LỰC

Đề bài: Bổ sung phương thức public boolean rutTienCoPhi(double st, double phi) vào lớp ATM.

Hàm thực hiện rút số tiền st kèm theo khoản phí phi.

Gợi ý: Điều kiện rút tiền công khai lúc này phải là (st + phi) <= soDu.

public boolean rutTienCoPhi(double st, double phi) {
    double tongTienPhaiTra = st + phi;
    if (tongTienPhaiTra <= 0) {
        System.out.println("Lỗi: Số tiền rút không hợp lệ!");
        return false;
    }
    if (tongTienPhaiTra > this.soDu) {
        System.out.println("Không đủ số dư để rút " + st + " cùng với phí " + phi);
        return false;
    }
    this.soDu -= tongTienPhaiTra;
    System.out.println("Rút thành công " + st + " (Phí: " + phi + "). Số dư còn lại: " + this.soDu);
    return true;
}

Bổ sung: hiểu sâu từng dòng (Bài 2)

Từ mới trong bài này (giải từ số 0):

Mổ xẻ hàm chuyển tiền (dòng chảy 4 bước):

public boolean chuyenTien(ATM tkNhan, double st) {
    if (tkNhan == null) { ... return false; }  // bước 0: chặn crash
    if (this.rutTien(st)) {   // bước 1: trừ tiền mình; rutTien đã kiểm tra st > 0 và đủ số dư
        tkNhan.napTien(st);   // bước 2: chỉ khi trừ thành công mới cộng cho người nhận
        ... return true;      // bước 3: báo thành công
    }
    ... return false;  // trừ thất bại thì không cộng, tiền không tự sinh ra hay mất đi
}

Điểm tinh tế: nếu rutTien thất bại (thiếu tiền), napTien không bao giờ chạy — tổng tiền hệ thống được bảo toàn. Đó là lý do output mẫu: 1111111 còn 600, 2222222 thành 400, tổng vẫn 1300.

Trace tay: 5 tài khoản, mỗi cái 100. Nạp 1000 vào 1111111 → 1100. Chuyển 500 sang 2222222 → 600 và 600. Rút 200 ở 2222222 → 400. Tổng = 600 + 400 + 100 + 100 + 100 = 1300. khớp output.

Tự kiểm tra:

  1. Vì sao chuyenTien không tự viết this.soDu -= st; tkNhan.soDu += st;?
Đáp ánVì soDu là private của object khác — Java cho phép cùng lớp truy cập nhau, nhưng viết thế sẽ bỏ qua mọi kiểm tra (số âm, thiếu tiền, null). Gọi lại rutTien/napTien thì được kiểm tra miễn phí.
  1. rutTien(0) hoặc rutTien(-50) trả về gì?
Đáp ánfalse, kèm thông báo lỗi — vì điều kiện st <= 0 chặn ngay từ đầu, số dư không đổi.
  1. Quên kiểm tra tkNhan == null thì sao?
Đáp ánDòng tkNhan.napTien(st) sẽ ném NullPointerException và cả chương trình crash.