Bài 14: Interface và Abstract Class - Bản thiết kế hệ thống
🎯 Mục tiêu bài học
-
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".
-
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).
-
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
abstractcho Class: Ngăn cản việc khởi tạo Object trực tiếp từ Class này. -
Từ khóa
abstractcho 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?"
-
Dùng Abstract Class khi có quan hệ "Là một" (IS-A):
Doglà mộtAnimal. 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àmSleep()). Nhấn mạnh tính Kế thừa bản chất.
-
Dùng Interface khi có quan hệ "Làm được" (CAN-DO):
Ducklàm được hành độngFly(),Airplane(Máy bay) cũng làm đượcFly(). 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 InterfaceIFlyable. Nhấn mạnh tính Khả năng / Hành vi.
-
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
IUserServicethay vì gọi thẳng classUserServiceđể 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
-
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ảioverride. Không thể khởi tạo đối tượng trực tiếp. -
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 đó. -
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:
-
Tạo một Interface tên là
IDatabasechứa phương thức:void Connect(); -
Tạo 2 Class là
MySQLDatabasevàPostgresDatabasecùng thực thi InterfaceIDatabasenà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. -
Trong hàm
Main, tạo mộtList<IDatabase>, thêm cả 2 loại DB trên vào danh sách. Dùngforeachđể duyệt và gọi hàmConnect().
All rights reserved