+4

Abstract Class là gì? Đừng nhầm lẫn nó với Interface nữa!

Chào anh em cộng đồng Viblo!

Nhớ lại thời chúng ta còn mài đũng quần trên ghế nhà trường, môn Lập trình hướng đối tượng (OOP) với C++ hay Java luôn là một nỗi ám ảnh. Thầy cô dạy: "Tính trừu tượng là ẩn đi chi tiết, chỉ hiện thị những tính năng thiết yếu". Nghe thì hàn lâm, học thuộc lòng đi thi thì qua môn, nhưng ra đi làm thực tế thì... hoang mang tột độ.

Đặc biệt, câu hỏi "tử huyệt" trong các buổi phỏng vấn Backend: "Abstract Class khác gì Interface? Khi nào dùng cái nào?" đã đánh rớt không biết bao nhiêu anh em fresher.

Hôm nay, chúng ta sẽ gạt bỏ đống lý thuyết sách vở sang một bên. Cùng mổ xẻ Abstract Class dưới góc độ thực chiến trong các hệ thống E-commerce nhé. Lên xe thôi!

1. Bản chất thật của Abstract Class (Lớp trừu tượng)

Nói một cách dân dã: Abstract Class giống như một bản thiết kế nhà xây dở.

Người thầu xây dựng (Abstract Class) đã đổ sẵn móng, xây sẵn tường bao, đi sẵn đường ống nước (đây là các thuộc tính và hàm có logic dùng chung). Tuy nhiên, họ cố tình để trống phần sơn tường và lát gạch (đây là các Abstract Methods - hàm trừu tượng), và ép buộc người mua nhà (Class con kế thừa) phải tự quyết định xem sơn màu gì, lát gạch loại nào.

Đặc điểm nhận diện "bản thiết kế dở dang":

  • Bạn KHÔNG THỂ khởi tạo trực tiếp nó (Không thể new AbstractClass()). Bản thiết kế thì không thể dọn vào ở được!
  • Nó dùng để tạo ra một "khuôn mẫu cốt lõi" cho các Class con kế thừa (extends) và tái sử dụng code.

2. Nỗi đau thực tế: Code lặp lại ngập tràn (DRY Violation)

Giả sử bạn đang làm Backend cho một hệ thống bán lẻ (như Hasaki chẳng hạn). Hệ thống của bạn bán 2 loại sản phẩm: Sản phẩm vật lý (Mỹ phẩm, máy rửa mặt) và Sản phẩm số (Voucher giảm giá gửi qua email).

Code "Ngây thơ" (Chưa dùng Abstract):

Bạn tạo 2 class rời rạc:

class PhysicalProduct 
{
    public string $name;
    public float $price;

    public function getDisplayPrice(): string {
        return number_format($this->price) . ' VNĐ';
    }

    // Sản phẩm vật lý tính phí ship theo cân nặng
    public function calculateShippingFee(float $weight): float {
        return $weight * 15000;
    }
}

class DigitalProduct 
{
    public string $name;
    public float $price;

    // Lặp lại code y hệt class trên!
    public function getDisplayPrice(): string {
        return number_format($this->price) . ' VNĐ';
    }

    // Voucher gửi qua mail, không có phí ship
    public function calculateShippingFee(float $weight): float {
        return 0;
    }
}

Vấn đề: Hàm getDisplayPrice() và các thuộc tính $name, $price bị lặp lại ở cả 2 nơi. Nếu sau này sếp yêu cầu đổi format tiền tệ sang USD, bạn phải đi sửa ở N class khác nhau.

3. Cứu tinh xuất hiện: Gom code với Abstract Class

Chúng ta sẽ tạo ra một Lớp trừu tượng BaseProduct để chứa những thứ chung nhất, và ép các class con phải tự lo những thứ đặc thù.

Code "Thực chiến" với Abstract Class:

abstract class BaseProduct 
{
    protected string $name;
    protected float $price;

    public function __construct(string $name, float $price) {
        $this->name = $name;
        $this->price = $price;
    }

    // Lợi ích 1: Logic CHUNG được viết 1 lần duy nhất ở đây (Share code)
    public function getDisplayPrice(): string {
        return number_format($this->price) . ' VNĐ';
    }

    // Lợi ích 2: Ép buộc các class con PHẢI tự implement logic ĐẶC THÙ này
    abstract public function calculateShippingFee(float $weight): float;
}

Bây giờ, các class con chỉ cần tập trung vào nghiệp vụ riêng của nó:

// Sản phẩm vật lý
class PhysicalProduct extends BaseProduct 
{
    public function calculateShippingFee(float $weight): float {
        return $weight * 15000; // Phí giao hàng vật lý
    }
}

// Sản phẩm số (Voucher)
class DigitalProduct extends BaseProduct 
{
    public function calculateShippingFee(float $weight): float {
        return 0; // Voucher gửi qua mail, freeship
    }
}

Sử dụng vô cùng gọn gàng:

$toner = new PhysicalProduct("Toner Klairs", 350000);
echo $toner->getDisplayPrice(); // Gọi hàm của lớp cha
echo $toner->calculateShippingFee(0.5); // Logic của lớp con

// Báo lỗi ngay lập tức vì không thể khởi tạo class trừu tượng!
// $error = new BaseProduct("Lỗi rồi", 0);

4. Phân định "Tử huyệt": Abstract Class vs Interface

Đến đây, nhiều anh em sẽ hỏi: "Ủa, ép các class phải implement hàm thì Interface cũng làm được mà? Vậy Interface sinh ra làm gì?"

Đây là quy tắc vàng để anh em lựa chọn:

A. Abstract Class là mối quan hệ "IS-A" (Là một)

  • PhysicalProduct LÀ MỘT BaseProduct. Con chó LÀ MỘT Động vật.
  • Mục đích chính: Kế thừa bản chất và CHIA SẺ CODE LOGIC. Các hàm thông thường trong Abstract class có phần thân { ... } để class con dùng lại mà không cần viết lại.

B. Interface là mối quan hệ "CAN-DO" (Có thể làm gì / Hành vi)

  • FileLogger CÓ THỂ ghi log, DatabaseLogger CÓ THỂ ghi log. Chúng ta ký một "bản hợp đồng" mang tên LoggerInterface.
  • Mục đích chính: Đảm bảo các class hoàn toàn khác biệt nhau về mặt bản chất nhưng lại có chung một khả năng nào đó. Interface KHÔNG THỂ CHỨA CODE LOGIC (trước PHP 8/Java 8), nó chỉ chứa tên hàm trống rỗng.

Ví dụ kết hợp: Chiếc xe VinFast VF8 kế thừa bản chất từ Abstract Class Car (để dùng chung hàm run(), stop()), nhưng nó lại implement Interface ElectricChargeable (để cam kết có khả năng sạc điện giống như cái Điện thoại di động, dù Xe và Điện thoại chả có họ hàng gì với nhau!).

5. Góc nhìn mở rộng: Vượt ra khỏi PHP đến với Golang

Nếu anh em nào đang làm PHP/C++ và bắt đầu mon men tìm hiểu Go (Golang) để tối ưu hệ thống Backend, anh em sẽ gặp một cú sốc văn hóa lớn: Go KHÔNG CÓ Abstract Class! Thậm chí Go không có khái niệm Kế thừa (Inheritance) theo nghĩa truyền thống!

Thay vào đó, Go sử dụng nguyên lý Composition over Inheritance (Ưu tiên Lắp ráp hơn là Kế thừa) và kết hợp với Implicit Interface (Interface ngầm định).

Điều này cho chúng ta một bài học lớn về Architecture: Đừng lạm dụng Abstract Class để tạo ra những cây kế thừa sâu 4-5 tầng (Deep Inheritance Tree). Khi hệ thống quá phức tạp, việc lắp ghép các "mô-đun nhỏ" bằng Interface (như trong bài Strategy Pattern mình từng viết) sẽ dễ maintain hơn rất nhiều so với việc bắt tất cả kế thừa từ một ông tổ Abstract khổng lồ.

Lời kết

Tổng kết lại, Abstract Class là công cụ tuyệt vời khi bạn có một nhóm đối tượng có chung bản chất và bạn muốn chúng tái sử dụng một số logic cốt lõi (Share code), nhưng vẫn muốn áp đặt một bộ luật để chúng phải tự định nghĩa các hành vi đặc thù riêng.

Hiểu sâu về OOP không chỉ giúp anh em vượt qua vòng phỏng vấn dễ dàng, mà còn là nền tảng để học các Design Pattern hay chuyển đổi qua lại giữa các ngôn ngữ Backend (PHP, Go, Java) một cách nhẹ nhàng.

Anh em có hay lạm dụng Abstract Class trong dự án không? Cùng chém gió ở phần bình luận nhé! Nếu bài viết gãi đúng chỗ ngứa, đừng quên thả 1 Upvote ủng hộ mình!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí