SOLID Principles trong Laravel & PHP: Giải Thích Dễ Hiểu Để Đi Làm & Phỏng Vấn
SOLID là 5 nguyên tắc vàng của lập trình hướng đối tượng. Code tuân thủ SOLID thì dễ đọc, dễ test, dễ mở rộng — và là thứ interviewer hỏi nhiều nhất trong các buổi phỏng vấn senior. Bài này giải thích từng nguyên tắc bằng ngôn ngữ đời thường kèm ví dụ Laravel thực tế.
Nội dung bài viết
SSingle Responsibility Principle
Single Responsibility Principle
Một class chỉ nên có một lý do để thay đổi
Một câu để nhớ: Đầu bếp chỉ nấu ăn — không giao hàng, không tính tiền. Mỗi class chỉ làm một việc duy nhất.
“Một lý do để thay đổi” nghĩa là: nếu phải sửa class này, lý do phải là cùng một chủ đề. Class UserController chỉ nên thay đổi khi logic HTTP thay đổi — không phải khi business logic hay email template thay đổi.
class UserService {
// ❌ Xử lý DB
public function createUser(array $data): User {
return User::create($data);
}
// ❌ Gửi email — không liên quan đến tạo user
public function sendWelcomeEmail(User $user): void {
Mail::to($user->email)->send(new WelcomeMail($user));
}
// ❌ Upload avatar — lại một việc khác
public function uploadAvatar(User $user, UploadedFile $file): string {
$path = $file->store('avatars', 'public');
$user->update(['avatar' => $path]);
return $path;
}
// ❌ Tạo PDF report — hoàn toàn không liên quan
public function generateUserReport(User $user): string {
return PDF::loadView('reports.user', compact('user'))->save();
}
}// Chỉ xử lý CRUD user
class UserRepository {
public function create(array $data): User {
return User::create($data);
}
}
// Chỉ gửi email
class UserMailer {
public function sendWelcome(User $user): void {
Mail::to($user->email)->send(new WelcomeMail($user));
}
}
// Chỉ xử lý file upload
class AvatarUploader {
public function upload(User $user, UploadedFile $file): string {
$path = $file->store('avatars', 'public');
$user->update(['avatar' => $path]);
return $path;
}
}
// Controller gọi đúng service cần thiết
class UserController {
public function __construct(
private UserRepository $users,
private UserMailer $mailer,
) {}
public function store(Request $request) {
$user = $this->users->create($request->validated());
$this->mailer->sendWelcome($user);
return response()->json($user, 201);
}
}- Dễ test hơn — mỗi class nhỏ, mock dễ dàng
- Thay đổi email template không ảnh hưởng đến DB logic
- Tái sử dụng: UserMailer dùng được ở nhiều nơi khác
- Dễ tìm bug — biết ngay vấn đề ở class nào
Câu hỏi phỏng vấn thường gặp
- Q1.SRP là gì? Cho ví dụ vi phạm SRP trong thực tế?
- Q2."Một lý do để thay đổi" có nghĩa là gì?
- Q3.Làm sao biết một class đang vi phạm SRP? (class quá dài, method tên khác chủ đề)
- Q4.SRP áp dụng cho method, module không — hay chỉ cho class?
OOpen/Closed Principle
Open/Closed Principle
Mở để mở rộng, đóng để chỉnh sửa
Một câu để nhớ: Cắm thêm thiết bị vào ổ cắm — không cần đục tường thêm dây điện. Class nên mở để thêm tính năng mới nhưng đóng để không sửa code cũ.
class PaymentProcessor {
public function process(string $method, float $amount): bool {
// ❌ Mỗi lần thêm payment mới phải sửa class này
if ($method === 'stripe') {
// Stripe logic...
return true;
} elseif ($method === 'paypal') {
// PayPal logic...
return true;
} elseif ($method === 'momo') {
// Thêm MoMo → phải sửa class cũ ❌
return true;
}
// Rủi ro break code cũ mỗi lần thêm
return false;
}
}// Interface — "hợp đồng" cố định, không đổi
interface PaymentGateway {
public function charge(float $amount): bool;
public function refund(string $transactionId): bool;
}
// Mỗi payment = 1 class riêng
class StripeGateway implements PaymentGateway {
public function charge(float $amount): bool { /* ... */ return true; }
public function refund(string $id): bool { /* ... */ return true; }
}
class PayPalGateway implements PaymentGateway {
public function charge(float $amount): bool { /* ... */ return true; }
public function refund(string $id): bool { /* ... */ return true; }
}
// Thêm MoMo → chỉ tạo class mới, KHÔNG sửa code cũ ✅
class MoMoGateway implements PaymentGateway {
public function charge(float $amount): bool { /* ... */ return true; }
public function refund(string $id): bool { /* ... */ return true; }
}
// Processor không cần biết gateway cụ thể là gì
class PaymentProcessor {
public function __construct(private PaymentGateway $gateway) {}
public function process(float $amount): bool {
return $this->gateway->charge($amount);
}
}
// Laravel binding — đổi gateway chỉ sửa 1 dòng này
$this->app->bind(PaymentGateway::class, StripeGateway::class);StripeGateway sang MoMoGateway chỉ bằng cách sửa 1 dòng binding — không đụng đến PaymentProcessor hay Controller.- Thêm tính năng mới không làm hỏng code cũ
- Giảm rủi ro regression bug khi release
- Team có thể làm việc song song — mỗi người viết 1 implementation
- Code dễ unit test hơn vì mỗi class nhỏ, độc lập
Câu hỏi phỏng vấn thường gặp
- Q1.OCP là gì? Ví dụ thực tế bạn từng áp dụng?
- Q2."Mở để mở rộng, đóng để sửa" — làm thế nào đạt được điều này trong PHP?
- Q3.OCP liên quan thế nào đến Interface và Polymorphism?
- Q4.Khi nào KHÔNG cần áp dụng OCP? (YAGNI — You Ain't Gonna Need It)
LLiskov Substitution Principle
Liskov Substitution Principle
Subclass phải thay thế được parent class mà không phá vỡ chương trình
Một câu để nhớ: Nếu bạn đặt xe máy điện vào chỗ xe máy xăng, nó phải chạy được bình thường — không được bất ngờ phát nổ hay đứng im.
Nói đơn giản: nếu code dùng Animal, thì thay bằng Dog extends Animal hay Cat extends Animal đều phải hoạt động đúng.
class File {
public function read(): string { return file_get_contents($this->path); }
public function write(string $content): void {
file_put_contents($this->path, $content);
}
}
// ❌ ReadOnlyFile extends File nhưng không hỗ trợ write()
class ReadOnlyFile extends File {
public function write(string $content): void {
// Vi phạm LSP: throw exception thay vì hoạt động bình thường
throw new Exception("Cannot write to read-only file!");
}
}
// Code này sẽ crash nếu truyền ReadOnlyFile
function processFile(File $file): void {
$content = $file->read();
$file->write($content . "
---processed---"); // 💥 Nổ!
}// Tách thành 2 interface rõ ràng
interface Readable {
public function read(): string;
}
interface Writable {
public function write(string $content): void;
}
// File đầy đủ implement cả 2
class File implements Readable, Writable {
public function __construct(private string $path) {}
public function read(): string { return file_get_contents($this->path); }
public function write(string $content): void {
file_put_contents($this->path, $content);
}
}
// ReadOnlyFile chỉ implement Readable — trung thực về khả năng
class ReadOnlyFile implements Readable {
public function __construct(private string $path) {}
public function read(): string { return file_get_contents($this->path); }
}
// Function chỉ nhận thứ nó thực sự dùng
function readFile(Readable $file): string {
return $file->read(); // ✅ An toàn với cả File lẫn ReadOnlyFile
}
function saveFile(Writable $file, string $content): void {
$file->write($content); // ✅ Chỉ nhận class có thể write
}throw new Exception("Not supported"), hoặc override method để không làm gì ({}), hoặc phải dùng instanceof để check type trước khi gọi method.- Code an toàn hơn khi dùng polymorphism
- Tránh bug khó tìm do subclass hoạt động bất ngờ
- Dễ thay thế implementation mà không lo crash
- Thiết kế hierarchy rõ ràng, hợp logic
Câu hỏi phỏng vấn thường gặp
- Q1.LSP là gì? Cho ví dụ vi phạm LSP?
- Q2.Tại sao Square extends Rectangle lại vi phạm LSP? (classic interview question)
- Q3.LSP liên quan thế nào đến thiết kế Interface?
- Q4.Làm sao phát hiện code đang vi phạm LSP?
IInterface Segregation Principle
Interface Segregation Principle
Không nên ép class implement những method nó không dùng
Một câu để nhớ: Nhân viên văn phòng không cần biết lái xe tải chỉ vì cùng là nhân viên công ty. Interface nên được chia nhỏ thay vì một interface khổng lồ.
// ❌ Một interface ôm đồm mọi thứ
interface UserInterface {
public function find(int $id): User;
public function create(array $data): User;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
public function sendEmail(User $user, string $msg): void; // ❌ không phải DB
public function uploadAvatar(User $user, $file): string; // ❌ không phải DB
public function generateReport(User $user): string; // ❌ không phải DB
public function exportToCsv(array $users): string; // ❌ không phải DB
}
// Class bị ép implement những method không liên quan
class UserRepository implements UserInterface {
public function find(int $id): User { /* ... */ }
public function create(array $data): User { /* ... */ }
// ...
public function sendEmail(User $user, string $msg): void {
// ❌ Repository không gửi email! Nhưng interface ép phải có
throw new Exception("Not implemented");
}
public function generateReport(User $user): string {
throw new Exception("Not implemented"); // ❌
}
}// Mỗi interface chỉ 1 nhóm trách nhiệm
interface UserCrudInterface {
public function find(int $id): ?User;
public function create(array $data): User;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
}
interface UserNotifiableInterface {
public function sendEmail(User $user, string $message): void;
public function sendSms(User $user, string $message): void;
}
interface UserReportableInterface {
public function generateReport(User $user): string;
public function exportToCsv(array $users): string;
}
// Mỗi class chỉ implement những gì nó thực sự làm
class UserRepository implements UserCrudInterface {
public function find(int $id): ?User { return User::find($id); }
public function create(array $data): User { return User::create($data); }
public function update(int $id, array $data): bool { /* ... */ return true; }
public function delete(int $id): bool { return User::destroy($id) > 0; }
// Không bị ép viết sendEmail hay generateReport ✅
}
class UserNotificationService implements UserNotifiableInterface {
public function sendEmail(User $user, string $message): void {
Mail::to($user->email)->send(new GenericMail($message));
}
public function sendSms(User $user, string $message): void { /* ... */ }
}
// Controller chỉ inject interface nó cần
class UserController {
public function __construct(
private UserCrudInterface $users,
private UserNotifiableInterface $notifier,
) {}
}- Không phải implement method "rỗng" hay throw exception
- Interface nhỏ gọn, dễ mock trong test
- Thay đổi một interface không ảnh hưởng các class khác
- Code rõ ràng — nhìn vào interface biết ngay class làm gì
Câu hỏi phỏng vấn thường gặp
- Q1.ISP là gì? Ví dụ về "fat interface" trong thực tế?
- Q2.ISP và SRP khác nhau thế nào? (SRP cho class, ISP cho interface)
- Q3.Khi nào nên tách interface? Khi nào không cần?
- Q4.ISP liên quan thế nào đến LSP?
DDependency Inversion Principle
Dependency Inversion Principle
Phụ thuộc vào abstraction, không phụ thuộc vào implementation cụ thể
Một câu để nhớ: Ổ cắm điện phụ thuộc vào tiêu chuẩn phích cắm (interface), không phụ thuộc vào thương hiệu cụ thể. Bất kỳ thiết bị đúng tiêu chuẩn đều cắm được.
DIP có 2 phần: (1) Module cấp cao không phụ thuộc module cấp thấp — cả 2 phụ thuộc vào abstraction. (2) Abstraction không phụ thuộc vào chi tiết — chi tiết phụ thuộc vào abstraction.
class OrderService {
private MySQLOrderRepository $repository; // ❌ Cụ thể
private StripePayment $payment; // ❌ Cụ thể
private SmtpMailer $mailer; // ❌ Cụ thể
public function __construct() {
// ❌ Tự new — không inject từ ngoài
$this->repository = new MySQLOrderRepository();
$this->payment = new StripePayment();
$this->mailer = new SmtpMailer();
}
public function placeOrder(array $data): Order {
$order = $this->repository->save($data);
$this->payment->charge($order->total);
$this->mailer->send($order->user_email, 'Order confirmed');
return $order;
}
}
// Muốn test? Phải có MySQL thật, Stripe thật, SMTP thật — nightmare!// Định nghĩa abstraction (interface)
interface OrderRepositoryInterface {
public function save(array $data): Order;
public function find(int $id): ?Order;
}
interface PaymentInterface {
public function charge(float $amount): bool;
}
interface MailerInterface {
public function send(string $to, string $subject): void;
}
// High-level module phụ thuộc vào abstraction
class OrderService {
public function __construct(
private OrderRepositoryInterface $repository, // ✅ Interface
private PaymentInterface $payment, // ✅ Interface
private MailerInterface $mailer, // ✅ Interface
) {}
public function placeOrder(array $data): Order {
$order = $this->repository->save($data);
$this->payment->charge($order->total);
$this->mailer->send($order->user_email, 'Order confirmed');
return $order;
}
}
// Low-level modules implement interface
class MySQLOrderRepository implements OrderRepositoryInterface { /* ... */ }
class StripePayment implements PaymentInterface { /* ... */ }
class SmtpMailer implements MailerInterface { /* ... */ }
// Binding trong AppServiceProvider
$this->app->bind(OrderRepositoryInterface::class, MySQLOrderRepository::class);
$this->app->bind(PaymentInterface::class, StripePayment::class);
$this->app->bind(MailerInterface::class, SmtpMailer::class);
// Test dễ dàng — mock interface
class OrderServiceTest extends TestCase {
public function test_place_order() {
$mockRepo = Mockery::mock(OrderRepositoryInterface::class);
$mockPayment = Mockery::mock(PaymentInterface::class);
$mockMailer = Mockery::mock(MailerInterface::class);
$mockRepo->shouldReceive('save')->once()->andReturn(new Order());
$mockPayment->shouldReceive('charge')->once()->andReturn(true);
$mockMailer->shouldReceive('send')->once();
$service = new OrderService($mockRepo, $mockPayment, $mockMailer);
$service->placeOrder(['product_id' => 1, 'total' => 100]);
}
}- Test cực dễ — mock interface thay vì class thật
- Đổi database, payment, mailer không cần sửa business logic
- Code có thể tái sử dụng ở context khác nhau
- Giảm coupling — các module độc lập hơn
Câu hỏi phỏng vấn thường gặp
- Q1.DIP là gì? Khác gì với Dependency Injection?
- Q2."Phụ thuộc vào abstraction" có nghĩa cụ thể là gì?
- Q3.DIP và Service Container trong Laravel liên quan thế nào?
- Q4.Tại sao DIP giúp việc test dễ hơn? Cho ví dụ.
Tổng kết & Checklist thực hành
| Chữ | Nguyên tắc | Câu hỏi tự kiểm tra |
|---|---|---|
| S | Single Responsibility | Class này có nhiều hơn 1 lý do để thay đổi không? |
| O | Open/Closed | Thêm tính năng mới có cần sửa class cũ không? |
| L | Liskov Substitution | Subclass có thể thay thế parent mà không crash không? |
| I | Interface Segregation | Class có phải implement method mà nó không dùng không? |
| D | Dependency Inversion | Class có phụ thuộc vào concrete class không? |
Checklist khi review code
💡 Lời khuyên cho phỏng vấn
- →Khi được hỏi về SOLID, đừng chỉ đọc định nghĩa — hãy kể ví dụ từ dự án thực tế của bạn.
- →Nhớ mối liên hệ: DI (Design Pattern) implement DIP (nguyên tắc SOLID).
- →Câu hỏi phổ biến nhất: "Square extends Rectangle có vi phạm LSP không?" → Có, vì setWidth/setHeight của Square thay đổi cả 2 chiều, phá vỡ kỳ vọng của Rectangle.
- →Thừa nhận trade-off: SOLID tốt nhưng over-engineer cũng có hại. App nhỏ không nhất thiết cần repository cho mọi model.
Bài liên quan
Design Pattern trong Laravel & PHP
DI, Repository, Singleton, Factory, Observer — kèm code thực tế