0

Bài 14: Interface và Abstract Class - Bản thiết kế hệ thống

🎯 Mục tiêu bài học

  1. Hiểu và sử dụng Abstract Class (Lớp trừu tượng) để tạo ra các "bản thiết kế dở dang".

  2. Nắm vững Interface (Giao diện) - khái niệm quan trọng bậc nhất trong kiến trúc Backend (nhất là với ASP.NET Core sau này).

  3. Phân biệt được khi nào nên dùng Abstract Class, khi nào bắt buộc phải dùng Interface.

📖 Phần 1: Abstract Class (Lớp trừu tượng)

Đôi khi, bạn muốn tạo ra một Lớp cha làm khuôn mẫu, nhưng bản thân Lớp cha đó lại quá chung chung để có thể tồn tại ngoài đời thực.

Ví dụ: Bạn có lớp HinhHoc (Shape), lớp con là HinhTron và HinhVuong. Bạn có thể tính diện tích hình tròn, hình vuông, nhưng "Diện tích của một Hình học chung chung" thì tính thế nào? Không thể tính được! Do đó, HinhHoc chỉ nên là một khái niệm trừu tượng. Bạn cấm không cho ai dùng từ khóa new HinhHoc().

  • Từ khóa abstract cho Class: Ngăn cản việc khởi tạo Object trực tiếp từ Class này.

  • Từ khóa abstract cho Method: Ép buộc tất cả các Lớp con kế thừa bắt buộc phải ghi đè (override) phương thức đó, vì Lớp cha chỉ đặt tên hàm chứ không viết nội dung.

C#

// Lớp trừu tượng (Không thể dùng "new Animal()")
abstract class Animal 
{
    public string Name { get; set; }

    // Phương thức bình thường (Lớp con được dùng lại)
    public void Sleep() 
    {
        Console.WriteLine($"{Name} dang ngu zzz...");
    }

    // Phương thức trừu tượng (Không có thân hàm { }, ép lớp con phải tự viết)
    public abstract void MakeSound(); 
}

class Dog : Animal
{
    // BẮT BUỘC phải override MakeSound, nếu không sẽ bị báo lỗi đỏ
    public override void MakeSound()
    {
        Console.WriteLine("Gau gau!");
    }
}

📖 Phần 2: Interface (Bản hợp đồng / Tiêu chuẩn)

Nếu Abstract Class là "Cha truyền con nối", thì Interface giống như một "Bản hợp đồng" hoặc "Chứng chỉ kỹ năng". Bất kỳ Class nào ký vào bản hợp đồng này (thực thi Interface) đều bắt buộc phải có các hành động (Methods) mà hợp đồng yêu cầu.

  • Đặc điểm 1: Tên Interface trong C# luôn bắt đầu bằng chữ I (ví dụ: ILogger, IDatabase, IRepository).

  • Đặc điểm 2: (Trong mô hình cơ bản) Interface chỉ chứa phần khai báo tên hàm, hoàn toàn không có code bên trong ({}). Không có biến, không có constructor.

  • Đặc điểm 3: Đột phá lớn nhất - Một Class chỉ kế thừa được 1 Class cha, nhưng có thể thực thi (ký) hàng chục Interface cùng lúc!

C#

// Khai báo Interface (Bản hợp đồng yêu cầu khả năng Bay)
interface IFlyable
{
    void Fly(); // Chỉ gọi tên hàm, không viết code xử lý
}

// Khai báo Interface (Bản hợp đồng yêu cầu khả năng Bơi)
interface ISwimmable
{
    void Swim();
}

// Vịt là Động vật, đồng thời ký 2 hợp đồng: Biết bay và Biết bơi
class Duck : Animal, IFlyable, ISwimmable
{
    public override void MakeSound() { Console.WriteLine("Quac quac!"); }
    
    // Bắt buộc thực thi hợp đồng IFlyable
    public void Fly() { Console.WriteLine("Vit dang bay tren troi."); }
    
    // Bắt buộc thực thi hợp đồng ISwimmable
    public void Swim() { Console.WriteLine("Vit dang boi duoi ao."); }
}

📖 Phần 3: Khi nào dùng cái nào? (Tư duy Backend)

Câu hỏi kinh điển khi đi phỏng vấn: "Khi nào dùng Abstract Class, khi nào dùng Interface?"

  1. Dùng Abstract Class khi có quan hệ "Là một" (IS-A):

    • Dog là một Animal. Chúng chia sẻ chung những thuộc tính cốt lõi (tên, tuổi) và một phần logic code (như hàm Sleep()). Nhấn mạnh tính Kế thừa bản chất.
  2. Dùng Interface khi có quan hệ "Làm được" (CAN-DO):

    • Duck làm được hành động Fly(), Airplane (Máy bay) cũng làm được Fly(). Máy bay và con vịt chả có họ hàng gì với nhau, không thể nhét chung vào một Lớp cha được. Nhưng ta có thể gom chúng lại thông qua Interface IFlyable. Nhấn mạnh tính Khả năng / Hành vi.
  3. Dependency Injection trong ASP.NET Core: 99% Backend C# dùng Interface để kết nối các tầng của hệ thống với nhau (ví dụ: Controller gọi Interface IUserService thay vì gọi thẳng class UserService để dễ dàng bảo trì và đổi DB không bị sập app).

💻 Phần 4: Thực hành (Mô phỏng Ghi Log hệ thống Backend)

Trong thực tế, ta thường dùng Interface để tạo ra sự linh hoạt: Hệ thống có thể ghi Log ra Màn hình (Console), hoặc ghi Log ra File chữ, tùy theo cấu hình.

C#

using System;
using System.Collections.Generic;

namespace Lesson14
{
    // 1. Tạo bản hợp đồng: Bất cứ ai làm "Logger" đều phải có hàm LogError
    interface ILogger
    {
        void LogError(string message);
    }

    // 2. Class ghi Log ra màn hình (ký hợp đồng ILogger)
    class ConsoleLogger : ILogger
    {
        public void LogError(string message)
        {
            Console.WriteLine($"[CONSOLE ERROR]: {message}");
        }
    }

    // 3. Class ghi Log ra File (ký hợp đồng ILogger)
    class FileLogger : ILogger
    {
        public void LogError(string message)
        {
            // Mô phỏng việc ghi vào file log.txt
            Console.WriteLine($"[FILE ERROR]: Ghi '{message}' vao file text thanh cong.");
        }
    }

    // 4. Hệ thống cốt lõi chỉ cần biết ILogger, không cần quan tâm là log đi đâu
    class ServerApplication
    {
        private ILogger _logger;

        // Constructor nhận vào một giao diện ILogger (Dependency Injection cơ bản)
        public ServerApplication(ILogger logger)
        {
            _logger = logger;
        }

        public void ProcessData()
        {
            Console.WriteLine("Server dang xu ly du lieu...");
            // Giả lập xảy ra lỗi
            _logger.LogError("Mat ket noi Database!"); 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Nếu chạy dưới máy Dev: Ghi log ra màn hình
            ILogger devLogger = new ConsoleLogger();
            ServerApplication devApp = new ServerApplication(devLogger);
            devApp.ProcessData();

            Console.WriteLine("---------------------");

            // Nếu đưa lên Product thật: Đổi sang ghi log vào File mà KHÔNG CẦN SỬA code của ServerApplication
            ILogger prodLogger = new FileLogger();
            ServerApplication prodApp = new ServerApplication(prodLogger);
            prodApp.ProcessData();

            Console.ReadLine();
        }
    }
}

Bạn thấy sự lợi hại chưa? Dùng Interface giúp code cực kỳ linh hoạt và dễ thay thế lốp dự phòng!

📝 Tóm tắt bài học

  1. Abstract Class (abstract) dùng làm khuôn mẫu dở dang, có thể chứa code thật hoặc hàm rỗng ép lớp con phải override. Không thể khởi tạo đối tượng trực tiếp.

  2. Interface (interface) là bản hợp đồng. Bắt đầu bằng chữ I. Ép Class phải thực thi các hàm trong đó.

  3. Class chỉ kế thừa được 1 Abstract Class, nhưng có thể thực thi nhiều Interface.

🛠 Bài tập thực hành (Kết thúc chặng OOP)

Yêu cầu:

  1. Tạo một Interface tên là IDatabase chứa phương thức: void Connect();

  2. Tạo 2 Class là MySQLDatabase và PostgresDatabase cùng thực thi Interface IDatabase này. Mỗi class tự in ra dòng chữ "Đang kết nối tới MySQL..." hoặc "Đang kết nối tới PostgreSQL..." tương ứng.

  3. Trong hàm Main, tạo một List<IDatabase>, thêm cả 2 loại DB trên vào danh sách. Dùng foreach để duyệt và gọi hàm Connect().


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í