Nguyên lý SOLID như đã biết hỗ trợ chúng ta rất nhiều trong việc xây dựng ứng dụng hay website với khả năng cập nhật và chỉnh sửa về sau một cách dễ dàng hơn. Trong bài Nguyên Lý SOLID trong lập trình hướng đối tượng PHP P1 chúng ta đã biết đến 2 trong 5 nguyên tắc của nguyên lý này là
+ Single Responibility Principle – Nguyên tắc đơn chức năng
+ Open – Close Principle – Nguyên tắc đóng mở
Hôm nay, chúng ta sẽ tìm hiểu tiếp 3 nguyên tắc còn lại của nguyên lý thiết kế hướng đối tượng SOLID với ví dụ qua PHP.
Trong một chương trình, các object của class con có thể thay thế class cha mà không làm thay đổi tính đúng đắn của chương trình.
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Dưới đây là 1 số ví dụ về Nguyên tắc thay thế – Liskov Substitution Principle trong PHP
Yêu cầu đặt ra là viết class cho Hình chữ nhật và Hình vuông, trong đó có phương thức tính diện tích. Đây là cũng là 1 ví dụ kinh điển trong việc mô tả cho nguyên tắc LSP .
Với yêu cầu trên ta có đoạn mã dưới đây
<?php namespace CodeTuTam\Solid\LSP\Examples; // Class hình chữ nhật class Rectangle{ protected $width; protected $height; public function setWidth(int $width):void{ $this->width = $width; } public function setHeight(int $height):void{ $this->height = $height; } public function calcArea(){ return $this->width * $this->height; } } // Class Hình vuông class Square extends Rectangle{ protected $width; protected $height; public function setWidth(int $width):void{ parent::setWidth($width); parent::setHeight($width); } public function setHeight(int $height):void{ parent::setWidth($height); parent::setHeight($height); } } //Chạy thử $rectangle = new Rectangle; $rectangle->setWidth(5); $rectangle->setHeight(10); echo $rectangle->calcArea().PHP_EOL; // 50 $square = new Square; $square->setWidth(5); $square->setHeight(10); echo $square->calcArea().PHP_EOL; //100
Đoạn mã trên chạy trên phiên bản PHP 7.1 trở lên
Khi xem ví dụ thì chúng ta thấy hoàn toàn chưa có vấn đề gì cả. Cụ thể việc tính diện tích của hình chữ nhật 2 cạnh là hoàn toàn chính xác. Đối với hình vuông, do 4 cạnh luôn bằng nhau, do vậy nên khi set độ dài 1 cạnh thì cạnh còn lại cũng bằng tương ứng.
Dó đó khi set Height của hình vuông là 10 thì Width cũng là 10, và diện tích chính xác là 100.
Uhm… Xem chừng có vẻ mọi thứ rất chuẩn xác phải không nào.
Tuy nhiên hãy xem thêm đoạn mã dưới dây để đánh giá
function getRectangle($width, $height):Rectangle{ $rectangle = new Square; $rectangle->setWidth($width); $rectangle->setHeight($height); return $rectangle; } echo getRectangle(5,10)->calcArea(); //100
Tại đây ta có 1 phương thức để lấy ra Rectangle (Hình chữ nhật) với tham số là chiều rộng và dài. Và đương nhiên mong muốn nhận được của chúng ta là 1 đối tượng là Rectangle
Đoạn mã trên hoàn toàn hợp lệ, do Square là 1 class kế thừa từ Rectangle, nhưng kết quả chúng ta nhận được lại không như mong muốn
Chính xác thì nếu kết quả mong muốn phải nhận về là 50 ( = 5 x 10 ) thay vì 100( = 10 x 10).
Đó chính là vấn đề phát sinh khi mở rộng hoặc cập nhật mã nguồn nếu viết đi thiếu những quy tắc.
Cụ thể thì Theo nguyên tắc thay thế, chúng ta phải bảo đảm rằng khi một lớp con kế thừa từ một lớp khác, nó sẽ không làm thay đổi hành vi của lớp đó. ( thay đổi hàm setWidth, set Height của lớp cha)
Để giải quyết bài toán này chúng ta sẽ viết lại đoạn mã trên như sau
Chúng ta nên tạo 1 class Shape sau đó cho Rectangle và Square kế thừa class này
<?php namespace CodeTuTam\Solid\LSP\Examples; abstract class Shape{ protected $width; protected $height; abstract function setWidth(int $width):void; abstract function setHeight(int $height):void; abstract function calcArea():int; } // Class hình chữ nhật class Rectangle{ public function setWidth(int $width):void{ $this->width = $width; } public function setHeight(int $height):void{ $this->height = $height; } public function calcArea():int{ return $this->width * $this->height; } } // Class Hình vuông class Square extends Shape{ public function setWidth(int $width):void{ $this->width = $width; $this->height = $width; } public function setHeight(int $height):void{ $this->width = $height; $this->height = $height; } public function calcArea():int{ return $this->width * $this->height; } }
Khi viết như thế này nếu sử dụng hàm getRectangle bên trên thì sẽ báo lỗi ngay khi chạy chương trình, do đó chúng ta có thể đảm báo tính đúng đắn của chương trình thay vì chương trình vẫn chạy ngon lành nhưng lại lỗi về mặt logic.
Yêu cầu: Xây dựng class các loài động vật
<?php namespace CodeTuTam\Solid\LSP\Examples; class Animal{ protected $name; // Động vật có thể chạy public function run():void{ } //Động vật có thể bay public function fly():void{ } } class Eagle extends Animal{ protected $name = 'Đại bàng'; public function run():void{ echo $this->name." đang chạy...(search youtube có đấy :))) )"; } public function fly():void{ echo $this->name." đang bay..."; } } class Cat extends Animal{ protected $name = 'Mèo'; public function run():void{ echo $this->name." đang chạy..."; } public function fly():void{ throw new \Exception($this->name." Không thể bay"); } } $cat = new Cat; $cat->run(); $cat->fly();
Class Animal có thể viết dạng Abstract class
Trong ví dụ trên ta thấy đối với Mèo – Cat không thể nào bay được, để đảm bảo tính đúng đắn của chương trình, chúng ta không thể cho phép phương thức này được chạy. Và thế là 1 Exception được ném ra khi gọi tới phương thức fly của class Cat.
Ok, mọi thứ rất ổn phải không nào, nhưng thực tế thì không phải vậy?
Giả sử ngày mai chúng ta lại bổ sung thêm phương thức swim … thì thật vô lý chúng ta lại tiếp tục chỉnh sửa hàng loạt các class kế thừa.
Chính điều này làm đoạn mã trên không thỏa mãn Liskov Substitution Principle – Nguyên tắc thay thế do class con đã ném ra lỗi trong 1 hàm của lớp cha, ( tham khảo thêm ví dụ về hàm getRectangle để hiểu rõ hơn)
Để chỉnh sửa điều này chúng ta viết lại đoạn mã như sau
<?php namespace CodeTuTam\Solid\LSP\Examples; class Animal{ protected $name; } interface Runable{ // Động vật có thể chạy public function run():void; } interface Flyable{ //Động vật có thể bay public function fly():void; } class Eagle extends Animal implements Runable,Flyable{ protected $name = 'Đại bàng'; public function run():void{ echo $this->name." đang chạy...(search youtube có đấy :))) )"; } public function fly():void{ echo $this->name." đang bay..."; } } class Cat extends Animal implements Runable{ protected $name = 'Mèo'; public function run():void{ echo $this->name." đang chạy..."; } }
Tách các hành động khác biệt ra các Interface nhỏ hơn. Việc này sẽ tạo ra khá nhiều file khi làm việc tuy vậy chúng đảm bảo tính đúng đắn của mã nguồn. Chính điều này chúng ta không phải implement những hàm thừa, mà bản chất không nên tồn tại
Một số dấu hiện nhận biết việc chúng ta đang vi phạm nguyên tắc LSP
+ Các lớp dẫn xuất có các phương thức ghi đè phương thức của lớp cha nhưng với chức năng hoàn toàn khác.
+ Các lớp dẫn xuất có phương thức ghi đè phương thức của lớp cha là một phương thức rỗng.
+ Các phương thức bắt buộc kế thừa từ lớp cha ở lớp dẫn xuất nhưng không được sử dụng.
+ Phát sinh (Ném) ngoại lệ trong phương thức của lớp dẫn xuất.
Nguyên tắc LSP cũng là một nguyên tắc dễ bị vi phạm nhất trong nguyên lý SOLID. Nó ẩn chứa trong hầu hết mọi đoạn code, ví dụ trong PHP cũng có các interface như Countable, Iterator … giúp cho việc sử dụng được linh hoạt hơn.
Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể.
Many client-specific interfaces are better than one general-purpose interface.
Nguyên tắc này cũng không quá khó để hiểu nếu bạn đã đọc các phần nội dung khác về nguyên lý SOLID trong PHP của CodeTuTam. Chúng ta cần hiểu đơn giản rằng nếu 1 interface có quá nhiều class cần implements thì có thể rằng trong đó sẽ có những hàm không phù hợp về mặt chức năng mà class kế thừa không nhất thiết phải có.
Việc tách nhỏ 1 interface lớn ra là cần thiết, như vậy sau này khi chỉnh sửa, cập nhật interface đó sẽ hạn chế việc ảnh hưởng lớn tới các class kế thừa.
Dưới đây là 1 interface Repository
<?php namespace CodeTuTam\Solid\ISP\Examples; interface Repository{ // Lấy tất cả bản ghi public function getAll(); //Lấy 1 bản ghi public function getOne(); //Cập nhật public function update(); //Phân trang public function paginate(); //Xóa public function delete(); //Cho vào thùng rác public function trash(); }
Trong interface này có rất nhiều phương thức: lấy danh sách bản ghi, lấy 1 bản, sửa, xóa, phân trang…
Trong 1 bài toán cụ thể thì có thể các trường hợp phát sinh như
+ PostRepository sẽ có đủ các phương thức trên
Nhưng
+ SettingRepository thì có thể không cần 1 số phương thức như delete, trash hay phân trang
Chính điều này nếu các class SettingRepository nếu implement interface Repository sẽ có những class thừa thãi không dùng đến. Việc này sẽ ảnh hưởng tới nghiệp vụ của lớp đó. Hoặc sẽ vi phạm vào quy tắc Liskov Substitution Priciple bên trên khi các hàm kế thừa là trống, hay ném ra ngoại lệ.
Để chỉnh phần này ta có thể viết lại dạng như sau
<?php namespace CodeTuTam\Solid\ISP\Examples; interface Repository{ // Lấy tất cả bản ghi public function getAll(); //Lấy 1 bản ghi public function getOne(); //Cập nhật public function update(); } interface RemovableRepository{ //Xóa public function delete(); //Cho vào thùng rác public function trash(); } interface PagingRespository{ //Phân trang public function paginate(); }
Khi tách interface lớn hơn thành các interface nhỏ hơn thì chúng ta sẽ chủ động sử dụng chúng vào từng trường hợp cụ thể một cách rành mạch hơn.
+ Các module cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả 2 nên phụ thuộc vào abstraction.
+ Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại. ( Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)+ High-level modules should not depend on low-level modules. Both should depend on abstractions.
+ Abstractions should not depend upon details. Details should depend upon abstractions.
Cùng xem ví dụ về việc lưu User vào database như sau
<?php namespace CodeTuTam\Solid\DIP\Examples; class UserDB { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->$dbConnection = $dbConnection; } public function store(User $user) { // Lưu user vào Database } }
Như chúng ta thấy, lúc khởi tạo UserDB phải truyền vào 1 đối tượng MySQLConnection. Điều này khiến lớp UserDB phụ thuộc trực tiếp từ cơ sở dữ liệu MySQL. Điều đó có nghĩa là nếu chúng ta thay đổi cơ sở dữ liệu đang sử dụng (sang SQLite, SQL Server…). Chúng ta cần viết lại lớp này và vi phạm Nguyên tắc Đóng mở – Open – Closed Principle.
<?php namespace CodeTuTam\Solid\DIP\Examples; interface DBConnection{ public function connect():void; } class MySQLConnection implements DBConnection{ public function connect():void{ } } class SQLiteConnection implements DBConnection{ public function connect():void{ } } class UserDB { private $dbConnection; public function __construct(DBConnection $dbConnection) { $this->$dbConnection = $dbConnection; } public function store(User $user) { // Lưu user vào Database } } $userDb = new UserDB(new MySQLConnection); $userDb = new UserDB(new SQLiteConnection);
Như bạn thấy, khi đổi lại việc truyền vào là interface DBConnection, thì lúc đó chúng ta có thể thoải mái mở rộng. Thậm chí ta có thể thay đổi kiểu kết nối mà không làm ảnh hưởng tới mã nguồn hiện tại. Việc thay đổi code module này không làm ảnh hưởng đến code module khác.
Trong hai bài viết mình đã giới thiệu cơ bản về nguyên lý SOLID trong lập trình hướng đối tượng với PHP. Nguyên lý này là nền tảng cho các mẫu thiết kế – Design Pattern, và cũng xuất hiện rất nhiều trong các framework PHP hiện nay ví dụ như Laravel.
Việc áp dụng nguyên lý này giúp code dễ dàng mở rộng, không bị quá phụ thuộc lẫn nhau. Điều này giúp cho hệ thống của bạn dễ dàng mở rộng về sau, hay nói cách khác dễ thích nghi với các thay đổi hơn.
Bài viết dựa trên kiến thức bản thân cũng như tìm hiểu thêm không tránh khỏi những sai sót. Rất mong được nghe được góp ý từ các mọi người 😀
Hi vọng bài viết này có thể giúp các bạn có thể tạo ra những mã nguồn chuẩn chỉnh với PHP. Nếu thấy bài viết hay và có ý nghĩa hay like và share để ủng hộ CodeTuTam bạn nhé.
Bình luận: