Lớp Trừu Tượng trong Java: Tối Ưu Hóa Kế Thừa và Thiết Kế Hệ Thống

Giới thiệu về Kế thừa trong Java

Trước khi khám phá sâu về các lớp trừu tượng (abstract classes), chúng ta hãy cùng nhìn lại khái niệm kế thừa trong Java. Kế thừa là một trong những trụ cột cơ bản của lập trình hướng đối tượng (OOP), mang lại nhiều lợi ích quan trọng cho việc phát triển phần mềm:

  • Tái sử dụng mã: Một lớp con có thể kế thừa và tái sử dụng các thuộc tính và phương thức đã được định nghĩa trong lớp cha. Điều này giúp giảm đáng kể lượng mã trùng lặp, từ đó nâng cao hiệu quả bảo trì và tính dễ đọc của mã nguồn.
  • Mở rộng chức năng: Lớp con có khả năng mở rộng chức năng của lớp cha bằng cách thêm các thuộc tính và phương thức mới, hoặc cung cấp các triển khai chuyên biệt hơn cho các phương thức hiện có thông qua việc ghi đè (override).
  • Xây dựng cấu trúc phân cấp: Kế thừa cho phép chúng ta tạo ra một hệ thống các lớp có tổ chức theo cấp bậc, giúp quản lý và sắp xếp mã nguồn một cách có logic. Ví dụ, một lớp cơ sở Animal có thể có các lớp con như Dog, Cat, mỗi lớp kế thừa các đặc tính chung nhưng cũng sở hữu những hành vi riêng biệt.
  • Hỗ trợ đa hình: Kế thừa là nền tảng của đa hình, cho phép sử dụng tham chiếu của kiểu lớp cha để thao tác với các đối tượng của lớp con. Điều này làm cho mã linh hoạt, dễ mở rộng và dễ dàng thích nghi với các tình huống khác nhau.

Để minh họa, hãy xem xét ví dụ về quản lý các loại điều hòa không khí. Ban đầu, chúng ta có thể tạo các lớp riêng biệt cho từng loại điều hòa:


// Lớp điều hòa nhập khẩu (ví dụ: thị trường Mỹ)
public class ImportedAirConditioner {
    private boolean operatingState = false;

    public void powerOn() {
        operatingState = true;
    }

    public void powerOff() {
        operatingState = false;
    }

    public void displayStatus() {
        System.out.println("Air Conditioner Status: " + (operatingState ? "ON" : "OFF"));
    }
}

// Lớp điều hòa nội địa (ví dụ: thị trường Việt Nam)
public class DomesticAirConditioner {
    private boolean operatingState = false;

    public void powerOn() {
        operatingState = true;
    }

    public void powerOff() {
        operatingState = false;
    }

    public void displayStatus() {
        System.out.println("Trạng thái Điều hòa: " + (operatingState ? "ĐANG BẬT" : "ĐANG TẮT"));
    }
}

Và cách sử dụng trong chương trình chính:


public class AirConditionerApplication {
    public static void main(String[] args) {
        System.out.println("--- Kiểm tra Điều hòa Nội địa ---");
        DomesticAirConditioner domesticAC = new DomesticAirConditioner();
        domesticAC.powerOn();
        domesticAC.displayStatus();
        domesticAC.powerOff();
        domesticAC.displayStatus();

        System.out.println("\n--- Kiểm tra Điều hòa Nhập khẩu ---");
        ImportedAirConditioner importedAC = new ImportedAirConditioner();
        importedAC.powerOn();
        importedAC.displayStatus();
        importedAC.powerOff();
        importedAC.displayStatus();
    }
}

Có thể thấy, các lớp ImportedAirConditionerDomesticAirConditioner đều có các thuộc tính (operatingState) và phương thức (powerOn(), powerOff()) giống hệt nhau. Sự trùng lặp này sẽ trở nên khó quản lý nếu chúng ta cần thêm nhiều loại điều hòa khác như điều hòa Nhật Bản, điều hòa châu Âu, v.v.

Để giải quyết vấn đề này, chúng ta có thể áp dụng kế thừa bằng cách tạo một lớp cơ sở chung:


public class BaseAirConditioner {
    protected boolean operatingState = false; // protected cho phép lớp con truy cập

    public void powerOn() {
        operatingState = true;
    }

    public void powerOff() {
        operatingState = false;
    }
}

Với lớp cơ sở BaseAirConditioner, các lớp con giờ đây chỉ cần tập trung vào các đặc điểm riêng biệt của mình:


public class ImportedAirConditioner extends BaseAirConditioner {
    public void displayStatus() {
        System.out.println("Air Conditioner Status: " + (operatingState ? "ON" : "OFF"));
    }
}

public class DomesticAirConditioner extends BaseAirConditioner {
    public void displayStatus() {
        System.out.println("Trạng thái Điều hòa: " + (operatingState ? "ĐANG BẬT" : "ĐANG TẮT"));
    }
}

Từ khóa extends cho biết ImportedAirConditionerDomesticAirConditioner kế thừa từ BaseAirConditioner. Thuộc tính operatingState được khai báo là protected, cho phép các lớp con truy cập trực tiếp nhưng vẫn giới hạn truy cập từ bên ngoài gói, giữ vững nguyên tắc đóng gói.

Lớp Trừu Tượng: Định nghĩa hành vi bắt buộc

Mặc dù kế thừa đã giúp tái sử dụng mã, nhưng phương thức displayStatus() vẫn phải được triển khai riêng biệt trong từng lớp con vì nội dung thông báo khác nhau. Tuy nhiên, hành động "hiển thị trạng thái" là một yêu cầu chung cho mọi điều hòa. Đây chính là lúc khái niệm lớp trừu tượng trở nên hữu ích.

Một lớp trừu tượng là một lớp không thể được khởi tạo trực tiếp (tức là bạn không thể tạo một đối tượng từ nó bằng new). Mục đích chính của nó là cung cấp một khuôn mẫu thiết kế và định nghĩa các hành vi mà các lớp con phải triển khai.

Sự khác biệt giữa Lớp Trừu Tượng và Kế thừa thông thường

  1. Mục đích chính:
    • Lớp trừu tượng: Định nghĩa một khung sườn hoặc giao diện chung, bắt buộc các lớp con phải cung cấp triển khai cho các phương thức trừu tượng, đồng thời có thể cung cấp các triển khai mặc định cho các phương thức khác.
    • Kế thừa thông thường: Chủ yếu tập trung vào việc tái sử dụng mã và thiết lập mối quan hệ "là một" (is-a) giữa các lớp.
  2. Trọng tâm:
    • Lớp trừu tượng: Nhấn mạnh vào "cái gì" một lớp con nên làm (các hành vi mà nó phải triển khai).
    • Kế thừa thông thường: Nhấn mạnh vào "cách thức" tái sử dụng các hành vi đã có và cấu trúc lớp.
  3. Tính linh hoạt:
    • Lớp trừu tượng: Cho phép định nghĩa các phương thức trừu tượng mà không có phần thân triển khai. Các phương thức này bắt buộc phải được triển khai bởi các lớp con không trừu tượng, tạo ra một ràng buộc mạnh mẽ.
    • Kế thừa thông thường: Lớp con có thể tùy chọn ghi đè hoặc không ghi đè các phương thức của lớp cha, hoặc thêm các phương thức mới mà không có ràng buộc bắt buộc.

Triển khai Lớp Trừu Tượng

Để biến BaseAirConditioner thành một lớp trừu tượng, chúng ta thêm từ khóa abstract vào khai báo lớp và khai báo phương thức displayStatus() là một phương thức trừu tượng:


public abstract class BaseAirConditioner {
    protected boolean operatingState = false;

    public void powerOn() {
        operatingState = true;
    }

    public void powerOff() {
        operatingState = false;
    }

    // Phương thức trừu tượng không có phần thân
    public abstract void displayStatus();
}

Lưu ý rằng một phương thức trừu tượng không có dấu ngoặc nhọn {} và phần thân triển khai; nó chỉ có khai báo phương thức và kết thúc bằng dấu chấm phẩy ;.

Khi một lớp (không trừu tượng) kế thừa từ một lớp trừu tượng, nó bắt buộc phải triển khai tất cả các phương thức trừu tượng của lớp cha. Do đó, các lớp ImportedAirConditionerDomesticAirConditioner sẽ được viết lại như sau:


public class ImportedAirConditioner extends BaseAirConditioner {
    @Override // Sử dụng @Override để kiểm tra tại thời điểm biên dịch
    public void displayStatus() {
        System.out.println("Air Conditioner Status: " + (operatingState ? "ON" : "OFF"));
    }
}

public class DomesticAirConditioner extends BaseAirConditioner {
    @Override
    public void displayStatus() {
        System.out.println("Trạng thái Điều hòa: " + (operatingState ? "ĐANG BẬT" : "ĐANG TẮT"));
    }
}

Bây giờ, nếu bạn quên triển khai phương thức displayStatus() trong một trong các lớp con này, trình biên dịch Java sẽ báo lỗi. Điều này đảm bảo rằng mọi loại điều hòa đều sẽ có một cách để hiển thị trạng thái của mình, mặc dù cách hiển thị có thể khác nhau (ví dụ: ngôn ngữ thông báo). Điều này tương tự như việc các loài động vật khác nhau (chó, mèo) đều có hành động "phát ra âm thanh", nhưng âm thanh cụ thể của chúng là khác nhau ("gâu gâu", "meo meo").

Với sự kết hợp của đa hình và lớp trừu tượng, chúng ta có thể cải thiện chương trình chính để xử lý mọi loại điều hòa một cách thống nhất và linh hoạt:


public class AirConditionerApplication {

    // Phương thức tiện ích để kiểm tra bất kỳ đối tượng điều hòa nào
    public static void performTest(BaseAirConditioner acUnit) {
        System.out.println("--- Bắt đầu kiểm tra đơn vị điều hòa ---");
        acUnit.powerOn();
        acUnit.displayStatus();
        acUnit.powerOff();
        acUnit.displayStatus();
        System.out.println("--- Kết thúc kiểm tra đơn vị điều hòa ---\n");
    }

    public static void main(String[] args) {
        System.out.println("--- Kiểm tra Điều hòa Nội địa ---");
        performTest(new DomesticAirConditioner());

        System.out.println("--- Kiểm tra Điều hòa Nhập khẩu ---");
        performTest(new ImportedAirConditioner());

        // Khi có một loại điều hòa mới, chỉ cần tạo đối tượng và gọi hàm test:
        // performTest(new NewBrandAirConditioner());
    }
}

Phương thức performTest nhận một đối tượng kiểu BaseAirConditioner. Nhờ đa hình, nó có thể chấp nhận bất kỳ đối tượng lớp con nào của BaseAirConditioner (như DomesticAirConditioner hoặc ImportedAirConditioner). Phương thức này gọi powerOn(), powerOff() (được kế thừa từ lớp cha) và displayStatus() (được triển khai riêng bởi mỗi lớp con). Điều này giúp mã nguồn trở nên gọn gàng, dễ đọc, dễ bảo trì và rất dễ mở rộng khi có thêm các loại điều hòa mới.

Thẻ: Java Lớp Trừu Tượng Kế thừa Đa hình Lập trình hướng đối tượng

Đăng vào ngày 30 tháng 6 lúc 19:53