Java OO & Functional
5/38
Bài 5 / 38~12 phútKế thừa & Đa hìnhLộ trình · chặng 03/21Miễn phí… lượt xem

Abstract class — template buộc subclass hoàn thiện

Khi nào nên dùng abstract class, abstract method, template method pattern trong kiến trúc phần mềm, so sánh với interface và quy tắc quản lý state.

TL;DR: Abstract class là lớp đóng vai trò khung sườn kiến trúc, không thể khởi tạo trực tiếp và có thể chứa các abstract method chưa có thân hàm để bắt buộc các class con hoàn thiện. Khác với interface, abstract class sở hữu constructor, quản lý được trường dữ liệu instance (state chung) và là nền tảng cho Template Method Pattern: class cha nắm giữ thứ tự các bước của thuật toán trong một phương thức final, class con chỉ được phép điền logic vào các bước trừu tượng được chỉ định.

Ở bài trước, bạn đã thấy các class con override phương thức calculateLateFee để đa hình lúc chạy. Nhưng ở LibraryItem, việc viết sẵn một công thức tính tiền mặc định là không hợp lý, vì mỗi loại ấn phẩm bắt buộc phải có quy định riêng.

Trong hệ thống thư viện, bạn có nhiều loại ấn phẩm: sách in, tạp chí, đĩa DVD, tài liệu điện tử. Tất cả đều là ấn phẩm (LibraryItem), đều có id, title và ngày nhập kho. Tuy nhiên, nếu bạn cho phép ai đó gọi new LibraryItem("ITEM-01", "Chung chung"), đối tượng đó trở nên vô nghĩa: nó không có thời hạn mượn cụ thể, không biết lưu trữ ở kho vật lý hay server số.

Java cung cấp từ khoá abstract để giải quyết vấn đề này: nó ngăn chặn việc tạo ra các đối tượng dở dang, đồng thời thiết lập hợp đồng kỹ thuật buộc các class con phải hoàn thiện logic còn thiếu.

1. Analogy — Bản vẽ kết cấu và thi công công trình

Hình dung ban quản lý thư viện phát hành một bản vẽ kết cấu tiêu chuẩn cho mọi chi nhánh:

  1. Khung sườn cố định: Mọi chi nhánh đều phải có quầy thủ thư, hệ thống chiếu sáng và cửa thoát hiểm (concrete methods).
  2. Hạng mục để mở: Vị trí sắp xếp kệ sách và hệ thống máy chủ lưu trữ dữ liệu số được ghi chú "tuỳ thuộc vào loại hình chi nhánh" (abstract methods).
  3. Hiện thực: Không ai có thể "ở" trong một bản vẽ (không thể new AbstractHouse). Nhà thầu bắt buộc phải tạo ra một chi nhánh cụ thể (sách truyền thống hoặc thư viện điện tử) bằng cách thi công đầy đủ các hạng mục còn để mở.
Thực tế quản lýKhái niệm Java
Bản vẽ kết cấu tiêu chuẩnabstract class LibraryItem
Hạng mục đã hoàn thiện sẵnConcrete method có thân hàm (getId(), displayInfo())
Hạng mục bắt buộc chi nhánh tự điềnabstract method không có thân hàm (getMaxLoanDays())
Chi nhánh vật lý đi vào hoạt độngSubclass concrete (PrintedBook, DigitalEbook)
💡 Bản chất của Abstract Class

Abstract class đóng vai trò làm bộ khung chịu lực. Nó nắm giữ mã nguồn dùng chung và ràng buộc trách nhiệm để các class con không thể "quên" các nghiệp vụ cốt lõi.

Thử ngẫmNếu class cha không khai báo abstract mà chỉ viết một phương thức rỗng return 0;, điều gì sẽ xảy ra nếu một lập trình viên mới tạo thêm class AudioBook nhưng quên viết hàm tính thời hạn mượn?

2. Cú pháp khai báo Abstract Class và Abstract Method ra sao?

Để biến một class thành lớp trừu tượng, bạn đặt từ khoá abstract trước từ khoá class. Phương thức trừu tượng kết thúc bằng dấu chấm phẩy và không có cặp ngoặc nhọn {}:

Java
public abstract class LibraryItem {
    private final String id;
    private final String title;

    // Constructor van ton tai de phuc vu super(...) tu subclass
    protected LibraryItem(String id, String title) {
        this.id = id;
        this.title = title;
    }

    public String getId() { return id; }
    public String getTitle() { return title; }

    // Abstract method: khong co than ham, bat buoc con phai viet
    public abstract int getMaxLoanDays();
    public abstract long calculateLateFee(int daysOverdue);

    // Concrete method: chia se logic chung giua tat ca cac con
    public String describe() {
        return "[" + id + "] " + title + " (Toi da: " + getMaxLoanDays() + " ngay)";
    }
}

Các class con bắt buộc phải override toàn bộ các abstract method:

Java
public class PrintedBook extends LibraryItem {
    private final int pageCount;

    public PrintedBook(String id, String title, int pageCount) {
        super(id, title);
        this.pageCount = pageCount;
    }

    @Override
    public int getMaxLoanDays() {
        return 14; // Sach in cho muon 14 ngay
    }

    @Override
    public long calculateLateFee(int daysOverdue) {
        return daysOverdue * 5000L;
    }
}

public class DigitalEbook extends LibraryItem {
    private final String downloadUrl;

    public DigitalEbook(String id, String title, String downloadUrl) {
        super(id, title);
        this.downloadUrl = downloadUrl;
    }

    @Override
    public int getMaxLoanDays() {
        return 21; // Ebook cho muon 21 ngay
    }

    @Override
    public long calculateLateFee(int daysOverdue) {
        return 0L; // Tu dong thu hoi, khong phat tien
    }
}

Khi sử dụng:

Java
LibraryItem item1 = new PrintedBook("B-01", "Design Patterns", 400);
LibraryItem item2 = new DigitalEbook("E-02", "Refactoring", "https://books.io/refactor");

System.out.println(item1.describe()); // In ra: [B-01] Design Patterns (Toi da: 14 ngay)
System.out.println(item2.describe()); // In ra: [E-02] Refactoring (Toi da: 21 ngay)

// LibraryItem item3 = new LibraryItem("X", "Y"); // Loi bien dich: LibraryItem is abstract

3. Những quy tắc quan trọng của Abstract Class là gì?

  1. Không thể khởi tạo trực tiếp: Lệnh new LibraryItem(...) sẽ bị trình biên dịch từ chối ngay lập tức.
  2. Nếu class có chứa abstract method, class bắt buộc phải là abstract: Java không cho phép một class thông thường chứa phương thức chưa hoàn thiện.
  3. Class abstract không bắt buộc phải có abstract method: Bạn có thể đánh dấu một class là abstract ngay cả khi tất cả các phương thức của nó đều có thân hàm, nhằm mục đích duy nhất: ngăn cấm lập trình viên new trực tiếp class đó.
  4. Subclass không hiện thực đủ abstract method thì phải tiếp tục là abstract:
Java
public abstract class Periodical extends LibraryItem {
    // Khong override getMaxLoanDays -> Periodical bat buoc phai giu tu khoa abstract
    protected Periodical(String id, String title) {
        super(id, title);
    }
}

4. Ứng dụng thực chiến: Template Method Pattern

Sự kết hợp giữa phương thức cụ thể (final) và phương thức trừu tượng (abstract) tạo nên mẫu thiết kế kinh điển: Template Method Pattern.

Xét bài toán xuất dữ liệu danh mục ấn phẩm thư viện ra nhiều định dạng (CSV, JSON):

  • Mọi định dạng đều có chung quy trình: Mở file ➔ Ghi tiêu đề (Header) ➔ Duyệt từng ấn phẩm (Records) ➔ Ghi kết đoạn (Footer) ➔ Lưu ra đĩa.
  • Điểm khác biệt duy nhất là cấu trúc cú pháp của từng định dạng file.

Cột dọc bốn bước của export nằm ở lớp cha, hai mũi tên ngang rẽ xuống CsvExporter, bước writeFile cuối cùng là private

Java
public abstract class CatalogExporter {
    // Phuong thuc template: danh dau final de ngan subclass pha vo thu tu cac buoc
    public final void export(List<LibraryItem> items) {
        StringBuilder sb = new StringBuilder();
        sb.append(buildHeader()).append("\n");
        for (LibraryItem item : items) {
            sb.append(formatItem(item)).append("\n");
        }
        sb.append(buildFooter());
        writeFile(sb.toString());
    }

    // Cac buoc bat buoc con phai trien khai
    protected abstract String buildHeader();
    protected abstract String formatItem(LibraryItem item);

    // Hook method: co gia tri mac dinh, con co the tuy chon override hoac khong
    protected String buildFooter() {
        return "";
    }

    // Buoc co dinh cua he thong, private de dong goi hoan toan
    private void writeFile(String content) {
        System.out.println("Ghi du lieu xuong dia:\n" + content);
    }
}

Triển khai cho định dạng CSV và JSON:

Java
public class CsvCatalogExporter extends CatalogExporter {
    @Override
    protected String buildHeader() {
        return "id,title,max_days";
    }

    @Override
    protected String formatItem(LibraryItem item) {
        return item.getId() + "," + item.getTitle() + "," + item.getMaxLoanDays();
    }
}

public class JsonCatalogExporter extends CatalogExporter {
    @Override
    protected String buildHeader() {
        return "[\n";
    }

    @Override
    protected String formatItem(LibraryItem item) {
        return "  {\"id\": \"" + item.getId() + "\", \"title\": \"" + item.getTitle() + "\"}";
    }

    @Override
    protected String buildFooter() {
        return "]";
    }
}

Thứ tự xử lý được bảo đảm tuyệt đối ở class cha, trong khi các class con tự do tuỳ biến nội dung theo từng nhu cầu riêng biệt.

5. So sánh Abstract Class và Interface

Tiêu chíAbstract ClassInterface
Quản lý trạng thái (State)Có các biến thực thể (private String id)Không có instance field (chỉ có hằng số tĩnh static final)
ConstructorCó constructor (gọi qua super(...))Không có constructor
Kế thừaĐơn kế thừa (chỉ kế thừa tối đa 1 class)Đa kế thừa (implement được nhiều interface)
Mục đích thiết kếQuan hệ bản chất Is-a kèm chia sẻ trạng tháiQuan hệ năng lực Can-do (hợp đồng hành vi)
Phạm vi phương thứcpublic, protected, private tuỳ ýMặc định public (từ Java 9 có private)

Khi nào nên chọn Abstract Class thay vì Interface?

  1. Chọn Abstract Class khi:
    • Bạn cần chia sẻ các trường dữ liệu instance (id, title, borrowedDate) giữa các lớp con.
    • Bạn muốn áp dụng Template Method Pattern với một thuật toán khung cố định.
    • Các lớp con có mối quan hệ phụ thuộc chặt chẽ về mặt ngữ nghĩa (Sách in là một Ấn phẩm).
  2. Chọn Interface khi:
    • Bạn muốn định nghĩa một năng lực dùng chung cho các lớp hoàn toàn không liên quan (Searchable cho cả LibraryItem lẫn UserProfile).
    • Bạn cần tận dụng đa kế thừa hành vi để một đối tượng đảm nhận nhiều vai trò khác nhau.

6. Tại sao constructor của Abstract Class nên là protected?

Do không có bất kỳ đoạn mã bên ngoài nào có thể gọi new LibraryItem(...), việc để constructor ở mức public là thừa thãi. Đặt constructor ở mức protected thể hiện rõ ý đồ thiết kế: constructor này chỉ tồn tại để phục vụ các class con thông qua lời gọi super(...).

7. Các lỗi sai thường gặp khi sử dụng Abstract Class

❌ Lỗi 1: Gọi abstract method ngay bên trong constructor của abstract class.
✅ Đây là bẫy khởi tạo dở dang (partially constructed object). Lúc constructor cha chạy, các trường dữ liệu của con chưa kịp gán giá trị, dẫn tới nguy cơ ném lỗi NullPointerException.

❌ Lỗi 2: Lạm dụng abstract class chỉ để gom các hàm tiện ích không có state.
✅ Nếu chỉ cần tập hợp các hàm xử lý dữ liệu thuần tuý, hãy sử dụng interface với static method hoặc các utility class final chứa static methods.

❌ Lỗi 3: Quên từ khoá abstract ở class khi vừa thêm một abstract method mới.
✅ Trình biên dịch sẽ báo lỗi ngay lập tức; hãy luôn kiểm tra định nghĩa class khi bổ sung các phương thức chưa có thân hàm.

8. Tóm tắt

  • abstract class là lớp khung không thể tạo đối tượng trực tiếp; dùng để định nghĩa các thuộc tính và hành vi chung cho cây phân cấp.
  • abstract method buộc các class con concrete phải cung cấp phần thân hàm thực thi cụ thể.
  • Template Method Pattern là thế mạnh đặc trưng của abstract class: dùng phương thức final để khoá thứ tự luồng xử lý và gọi các bước trừu tượng.
  • Abstract class khác biệt cơ bản với interface ở khả năng sở hữu constructor và quản lý instance state.
  • Constructor của abstract class nên đặt phạm vi protected.

9. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Một abstract class có bắt buộc phải chứa ít nhất một abstract method không?
  2. Q2
    Vì sao Template Method trong abstract class thường được đánh dấu từ khoá final?
  3. Q3
    Điểm khác biệt quan trọng nhất về mặt lưu trữ dữ liệu giữa Abstract Class và Interface là gì?
  4. Q4
    Điều gì sẽ xảy ra nếu một class con kế thừa từ abstract class nhưng không override hết tất cả các abstract method?
  5. Q5
    Tại sao không nên gọi abstract method bên trong constructor của abstract class?

Bài tiếp theo: Interface — hợp đồng capability, default method, và đa kế thừa có kiểm soát

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 12/16 của chặng

Ôn phỏng vấn

Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Interface — hợp đồng capability với default method