Tổng Quan về Nguyên Tắc SOLID
Giới Thiệu về SOLID
Nguyên tắc SOLID là một bộ quy tắc thiết kế quan trọng trong lập trình hướng đối tượng (Object-Oriented Design - OOD), được phát triển bởi Robert C. Martin, người được biết đến với tên gọi Uncle Bob. Bộ quy tắc này bao gồm năm nguyên tắc cơ bản, nhằm giúp lập trình viên thiết kế phần mềm dễ dàng bảo trì và mở rộng trong quá trình phát triển.
Dù những nguyên tắc này có thể áp dụng cho nhiều ngôn ngữ lập trình khác nhau, bài viết này sẽ sử dụng PHP làm ngôn ngữ minh họa. Việc áp dụng SOLID sẽ hỗ trợ lập trình viên trong việc tránh các tình huống không mong muốn (code smells) và cải thiện quy trình phát triển phần mềm theo các phương pháp Agile hoặc Adaptive. Nguyên tắc SOLID bao gồm:
- S - Nguyên Tắc Đơn Trách Nhiệm (Single Responsibility Principle - SRP)
- O - Nguyên Tắc Mở-Đóng (Open-Closed Principle - OCP)
- L - Nguyên Tắc Thay Thế Liskov (Liskov Substitution Principle - LSP)
- I - Nguyên Tắc Phân Tách Giao Diện (Interface Segregation Principle - ISP)
- D - Nguyên Tắc Đảo Ngược Sự Phụ Thuộc (Dependency Inversion Principle - DIP)
Nguyên Tắc Đơn Trách Nhiệm (SRP)
Nguyên tắc Đơn Trách Nhiệm (SRP) nêu rõ rằng: Mỗi lớp trong chương trình chỉ nên có một lý do để thay đổi. Điều này có nghĩa là mỗi lớp nên chỉ đảm nhận một trách nhiệm duy nhất.
Ví dụ minh họa SRP:
Giả sử bạn đang phát triển một ứng dụng tính toán diện tích cho một tập hợp hình học như hình vuông và hình tròn. Ban đầu, bạn có thể tạo ra các lớp hình học như sau:
php
class Square
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
}
class Circle
{
public $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
}
Bạn tiếp tục tạo một lớp AreaCalculator
để thực hiện tính toán diện tích tổng:
php
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
$area = [];
foreach ($this->shapes as $shape) {
if ($shape instanceof Square) {
$area[] = pow($shape->length, 2);
} elseif ($shape instanceof Circle) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
}
Khi bạn cần xuất kết quả, một phương thức như sau có thể được sử dụng:
php
public function output()
{
return "Tổng diện tích các hình: " . $this->sum();
}
Tuy nhiên, vấn đề nảy sinh khi lớp AreaCalculator
quản lý cả hai nhiệm vụ là tính toán diện tích và hiển thị kết quả. Nếu bạn muốn thay đổi cách hiển thị kết quả (ví dụ như xuất ra định dạng JSON), bạn sẽ cần chỉnh sửa lớp này, dẫn đến việc vi phạm nguyên tắc SRP.
Giải Pháp:
Để giải quyết vấn đề này, bạn có thể tạo một lớp riêng để xử lý đầu ra:
php
class SumCalculatorOutputter
{
protected $calculator;
public function __construct(AreaCalculator $calculator)
{
$this->calculator = $calculator;
}
public function JSON()
{
return json_encode(['sum' => $this->calculator->sum()]);
}
public function HTML()
{
return "Tổng diện tích các hình: " . $this->calculator->sum();
}
}
Bây giờ, việc tính toán và hiển thị đã tách biệt, hoàn toàn tuân thủ nguyên tắc SRP.
Nguyên Tắc Mở-Đóng (OCP)
Nguyên tắc Mở-Đóng (OCP) khẳng định rằng: Một lớp nên có khả năng mở rộng mà không cần sửa đổi mã nguồn gốc.
Ví dụ minh họa OCP:
Tiếp tục với lớp AreaCalculator
, giả sử bạn muốn thêm hỗ trợ cho các hình khác như tam giác hoặc lục giác.
Nếu bạn liên tục thêm các điều kiện if-else
, mã nguồn sẽ trở nên phức tạp và vi phạm OCP.
Giải Pháp:
Thay vì làm điều đó, bạn có thể đưa logic tính toán diện tích vào từng lớp hình và sử dụng một giao diện ShapeInterface
:
php
interface ShapeInterface
{
public function area();
}
class Square implements ShapeInterface
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
public function area()
{
return pow($this->length, 2);
}
}
class Circle implements ShapeInterface
{
public $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
public function area()
{
return pi() * pow($this->radius, 2);
}
}
Giờ đây, AreaCalculator
đơn giản chỉ cần gọi phương thức area()
mà không cần quan tâm đến loại hình cụ thể:
php
public function sum()
{
$area = [];
foreach ($this->shapes as $shape) {
if ($shape instanceof ShapeInterface) {
$area[] = $shape->area();
}
}
return array_sum($area);
}
Phương pháp này giúp bạn thêm hình mới mà không cần chỉnh sửa AreaCalculator
, hoàn toàn tuân thủ OCP.
Nguyên Tắc Thay Thế Liskov (LSP)
Nguyên tắc Liskov (LSP) chỉ ra rằng: Một lớp con phải có khả năng thay thế cho lớp cha của nó mà không làm ảnh hưởng đến tính đúng đắn của chương trình.
Ví dụ minh họa LSP:
Giả sử bạn mở rộng AreaCalculator
thành một lớp VolumeCalculator
:
php
class VolumeCalculator extends AreaCalculator
{
public function sum()
{
return $summedData; // Giá trị đơn thay vì mảng
}
}
Lớp VolumeCalculator
có thể thay thế AreaCalculator
trong chương trình mà không làm sai lệch bất kỳ tính năng nào.
Nguyên Tắc Phân Tách Giao Diện (ISP)
Nguyên tắc Phân Tách Giao Diện (ISP) đề xuất rằng: Một giao diện không nên ép các lớp triển khai các phương thức không cần thiết.
Ví dụ minh họa ISP:
Nếu bạn có một giao diện ShapeInterface
với phương thức volume()
, nhưng một hình vuông không có thể tích, điều này sẽ gây ra vi phạm ISP.
Giải Pháp:
Bạn có thể tách giao diện thành hai phần:
php
interface ShapeInterface
{
public function area();
}
interface ThreeDimensionalShapeInterface
{
public function volume();
}
Bây giờ, các hình khối 3 chiều như Cuboid
sẽ triển khai ThreeDimensionalShapeInterface
, trong khi các hình phẳng chỉ cần triển khai ShapeInterface
, hoàn toàn tuân thủ ISP.
Nguyên Tắc Đảo Ngược Sự Phụ Thuộc (DIP)
Nguyên tắc Đảo Ngược Sự Phụ Thuộc (DIP) nêu rõ: Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào abstract.
Ví dụ minh họa DIP:
Khi lớp PasswordReminder
phụ thuộc vào MySQLConnection
:
php
class MySQLConnection
{
public function connect()
{
return 'Database connection';
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
Nếu có sự thay đổi về cơ sở dữ liệu, bạn sẽ cần chỉnh sửa PasswordReminder
, gây ra vi phạm DIP.
Giải Pháp:
Bạn nên tạo một giao diện DBConnectionInterface
:
php
interface DBConnectionInterface
{
public function connect();
}
Và sau đó sử dụng giao diện này trong lớp PasswordReminder
:
php
class PasswordReminder
{
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
Cách này giúp cho PasswordReminder
không bị ràng buộc vào bất kỳ loại cơ sở dữ liệu cụ thể nào, hoàn toàn tuân thủ DIP.
Kết Luận
Qua việc phân tích sâu sắc năm nguyên tắc trong bộ SOLID, chúng ta có thể áp dụng những quy tắc này trong lập trình để tối ưu hóa quy trình thiết kế phần mềm. Việc tuân thủ các nguyên tắc này không chỉ giúp giảm thiểu rủi ro mà còn nâng cao khả năng bảo trì và mở rộng hệ thống trong tương lai.
source: viblo