Nguyên tắc Open/Closed (OCP)
Nguyên tắc Open/Closed (OCP) là một trong những nguyên tắc cốt lõi trong thiết kế phần mềm, được đề xuất bởi Bertrand Meyer. Nguyên tắc này phát biểu rằng một module (như lớp, hàm, hoặc module ứng dụng) nên được mở cho việc mở rộng nhưng đóng với việc sửa đổi. Điều này có nghĩa là bạn nên có thể mở rộng chức năng của một module mà không cần thay đổi mã nguồn hiện có.
Để thực hiện nguyên tắc này, thường có hai cách tiếp cận chủ yếu:
- Sử dụng Kế Thừa: Tạo ra một lớp cơ sở hoặc một interface, sau đó kế thừa và mở rộng chức năng bằng cách thêm các lớp con. Bằng cách này, hệ thống có thể mở rộng chức năng thông qua các lớp con mà không cần thay đổi lớp cha ban đầu.
- Sử dụng Đa Hình (Polymorphism): Tận dụng tính đa hình trong lập trình hướng đối tượng, bạn có thể tạo ra các interface hoặc lớp trừu tượng để định nghĩa các phương thức. Sau đó, các lớp cụ thể sẽ thực hiện các phương thức này theo một cách khác nhau. Điều này cho phép thêm các hành vi mới mà không cần thay đổi mã hiện tại.
Ví dụ minh họa:
Ở ví dụ này , ban đầu chúng ta sẽ có một lớp cho việc tính lương nhân viên cơ bản. Sau đó, hệ thống được mở rộng để tính lương cho các loại nhân viên khác mà không cần sửa đổi mã của lớp hiện tại, chỉ cần thêm lớp mới.
Yêu cầu ban đầu: Giả sử ban đầu bạn chỉ cần tính lương cho nhân viên toàn thời gian. Ta sẽ có một lớp FullTimeEmployee để tính lương.
<?php
interface Employee {
public function calculateSalary(): float;
}
class FullTimeEmployee implements Employee {
private $baseSalary;
public function __construct($baseSalary) {
$this->baseSalary = $baseSalary;
}
public function calculateSalary(): float {
return $this->baseSalary;
}
}
$fullTimeEmployee = new FullTimeEmployee(3000);
echo "Full Time Employee Salary: " . $fullTimeEmployee->calculateSalary() . "\n";
?>Mở rộng hệ thống
Bây giờ, giả sử bạn muốn thêm khả năng tính lương cho nhân viên bán thời gian. Thay vì sửa đổi lớp FullTimeEmployee, bạn có thể tạo một lớp mới PartTimeEmployee, tuân theo nguyên tắc Open/Closed.
<?php
interface Employee {
public function calculateSalary(): float;
}
class FullTimeEmployee implements Employee {
private $baseSalary;
public function __construct($baseSalary) {
$this->baseSalary = $baseSalary;
}
public function calculateSalary(): float {
return $this->baseSalary;
}
}
class PartTimeEmployee implements Employee {
private $hourlyRate;
private $hoursWorked;
public function __construct($hourlyRate, $hoursWorked) {
$this->hourlyRate = $hourlyRate;
$this->hoursWorked = $hoursWorked;
}
public function calculateSalary(): float {
return $this->hourlyRate * $this->hoursWorked;
}
}
$fullTimeEmployee = new FullTimeEmployee(3000);
echo "Full Time Employee Salary: " . $fullTimeEmployee->calculateSalary() . "\n";
$partTimeEmployee = new PartTimeEmployee(20, 120);
echo "Part Time Employee Salary: " . $partTimeEmployee->calculateSalary() . "\n";
?>Giải thích
- Interface Employee: Đóng vai trò là abstraction chung cho tất cả các loại nhân viên. Mỗi loại nhân viên sẽ có phương thức calculateSalary() riêng.
- FullTimeEmployee và PartTimeEmployee: Là các lớp con thực hiện interface Employee. Mỗi loại nhân viên có cách tính lương riêng, một dựa trên lương cơ bản, và một dựa trên số giờ làm và mức lương theo giờ.
- Khi cần thêm loại nhân viên mới, bạn chỉ việc tạo một lớp mới implement interface Employee mà không cần sửa đổi các lớp hiện có.
- Cách tiếp cận này đảm bảo rằng hệ thống của bạn được mở rộng dễ dàng (thêm loại nhân viên mới) mà không cần thay đổi mã nguồn đã được viết và kiểm thử trước đó, tuân thủ nguyên tắc Open/Closed.
Bên cạnh Open/Closed, Liskov Substitution Principle (LSP) giữ vai trò then chốt, bảo đảm mở rộng mà không phá vỡ hành vi đã đúng, từ đó duy trì kiến trúc vững và dễ bảo trì.
Nguyên tắc thay thế của Liskov (LSP)
Khi phát triển phần mềm hướng đối tượng, một trong những nguyên tắc quan trọng góp phần tạo nên kiến trúc hệ thống bền vững, dễ bảo trì là Nguyên tắc thay thế của Liskov (Liskov Substitution Principle – LSP). Vậy LSP là gì? Làm thế nào chúng ta có thể áp dụng nó trong PHP? Hãy cùng tìm hiểu qua bài nội dung bên dưới nhé!
- LSP là gì?
Nguyên tắc thay thế của Liskov phát biểu rằng:
"Các đối tượng của lớp con phải có khả năng thay thế cho đối tượng của lớp cha mà không làm thay đổi đúng đắn của chương trình."
Nói cách khác, khi bạn sử dụng đối tượng của lớp con thay cho đối tượng lớp cha, chương trình vẫn hoạt động trơn tru mà không sinh lỗi. Lớp con không nên phá vỡ các hành vi đã thiết lập của lớp cha.
- Ví dụ vi phạm LSP
Giả sử bạn có một class Rectangle và một class Square kế thừa từ Rectangle:
<?php
class Rectangle {
protected $width;
protected $height;
public function setWidth($width) {
$this->width = $width;
}
public function setHeight($height) {
$this->height = $height;
}
public function getArea() {
return $this->width * $this->height;
}
}
class Square extends Rectangle {
public function setWidth($width) {
$this->width = $width;
$this->height = $width;
}
public function setHeight($height) {
$this->height = $height;
$this->width = $height;
}
}Sẽ ra sao nếu bạn viết một hàm thao tác với Rectangle?
<?php
function printArea(Rectangle $rect) {
$rect->setWidth(5);
$rect->setHeight(10);
echo $rect->getArea();
}
// Kỳ vọng in ra: 50
printArea(new Rectangle()); // 50
// Nhưng Square sẽ ra: 100
printArea(new Square()); // 100 (sai!)- Cách tuân thủ LSP
Để tránh vi phạm LSP, chỉ nên sử dụng kế thừa khi mối quan hệ thực sự là "is-a" (Square thật sự là Rectangle hay không?). Ngoài ra, bạn có thể dùng interface hoặc các class tách biệt rõ ràng:
<?php
interface Shape {
public function getArea();
}
class Rectangle implements Shape {
protected $width;
protected $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
public function getArea() {
return $this->width * $this->height;
}
}
class Square implements Shape {
protected $side;
public function __construct($side) {
$this->side = $side;
}
public function getArea() {
return $this->side * $this->side;
}
}Lúc này, các đối tượng Rectangle và Square đều đúng vai trò, không ép buộc sự thay thế lẫn nhau một cách bất hợp lý.
- Kết luận
Nguyên tắc LSP giúp code của bạn dễ bảo trì, dễ mở rộng, ít lỗi và đảm bảo rằng lớp con không phá vỡ công năng của lớp cha.
Hãy nhớ, chỉ sử dụng kế thừa khi thực sự phù hợp, và luôn kiểm tra lại xem lớp con có thể thay thế an toàn cho lớp cha hay chưa.
Ngoài ra, để bắt đầu hành trình SOLID với Phần 1: Overview và SRP — viên gạch đầu tiên cho kiến trúc sạch. Mời mọi người xem!

