+6

Tìm hiểu về nguyên lý SOLID

SOLID là từ viết tắt của 5 nguyên tắt thiết kế hướng đối tượng (OOP). Một project áp dụng những nguyên lý này sẽ có code dễ đọc, dễ test, rõ ràng. Và hơn hết nó giúp dev dễ maintain và phát triển project rất nhiều, cụ thể như sau:

  • S: Single-responsibility principle
  • O: Open-closed principle
  • L: Liskov substitution principle
  • I: Interface segregation principle
  • D: Dependency Inversion Principle

Ok, bây giờ cùng mình tìm hiểu rõ hơn nguyên lý này nhé!

S — Single Responsibility Principle(S.R.P)

Một class chỉ nên xử lý duy nhất cho một công việc (Chỉ có thể sửa class cho một lý do duy nhất).

  • Nếu code vi phạm nguyên lý này - tức là 1 class xử lý nhiều nhiệm vụ:
<?php
namespace Demo;
use DB;

class OrdersReport
{
    public function getOrdersInfo($startDate, $endDate)
    {
        $orders = $this->queryDBForOrders($startDate, $endDate);
        
        return $this->format($orders);
    }

    protected function queryDBForOrders($startDate, $endDate)
    { 
        return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get();
    }

    protected function format($orders)
    {
        return '<h1>Orders: ' . $orders . '</h1>';
    }
}

Class OrdersReport đang thực hiện 3 công việc: lấy thông tin Order, đọc dữ liệu Order từ DB và hiển thị Order. Khi thay đổi DB hay thay đổi cách get dữ liệu,..., bắt buộc ta phải sửa class. Càng về sau class sẽ càng phình to ra, dẫn đến khó maintain, khó hiểu, khó clear các chức năng.

Khi requirements thay đổi, code tái cấu trúc, có nghĩa các class được sửa đổi. Nếu một lớp xử lý càng nhiều nhiệm vụ , thì càng có nhiều yêu cầu thay đổi, và những thay đổi đó sẽ càng khó thực hiện. Vậy nên ta cần tách class ra thành các class riêng để xử lý từng công việc.

Ở ví dụ này ta cần tách ra như sau

<?php
namespace Report;
use Report\Repositories\OrdersRepository;

class OrdersReport
{
	protected $repo;
	protected $formatter;

	public function __construct(OrdersRepository $repo, OrdersOutPutInterface $formatter)
	{
		$this->repo = $repo;
		$this->formatter = $formatter;
	}

	public function getOrdersInfo($startDate, $endDate)
	{
		$orders = $this->repo->getOrdersWithDate($startDate, $endDate);

		return $this->formatter->output($orders);
	}
}

namespace Report;

interface OrdersOutPutInterface
{
	public function output($orders);
}

namespace Report;

class HtmlOutput implements OrdersOutPutInterface
{
	public function output($orders)
	{
		return '<h1>Orders: ' . $orders . '</h1>';
	}

}

namespace Report\Repositories;
use DB;

class OrdersRepository
{
    public function getOrdersWithDate($startDate, $endDate)
    {
        return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get();
    }
}

O — Open-Closed Principle

Bạn có thể mở rộng class mà không sửa đổi bên trong nó.

  • Nguyên tắt này là nền tảng để xây dựng code dễ maintain và tái sử dụng.

Ở nguyên tắt này khi có yêu cầu mới, ta có thể tạo một class mới mở rộng (bằng cách kế thừa hoặc sở hữu) class cũ mà không sửa đến class cũ. Hãy cùng mình xem ví dụ sau:

<?php
class Rectangle
{
    public $width;
    public $height;
    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
    }
}

class Circle
{
    public $radius;
    public function __construct($radius)
    {
        $this->radius = $radius;
    }
}

class CostManager
{
    public function calculate($shape)
    {
        $costPerUnit = 1.5;
        if ($shape instanceof Rectangle) {
            $area = $shape->width * $shape->height;
        } else {
            $area = $shape->radius * $shape->radius * pi();
        }
        
        return $costPerUnit * $area;
    }
}

$circle = new Circle(5);
$rect = new Rectangle(8,5);
$obj = new CostManager();
echo $obj->calculate($circle);

Khi muốn tính diện tích của hình tròn, nếu ta thay đổi method calculate trong class CostManager. Nó sẽ phá vỡ nguyên lý Open-closed. Theo nguyên lý này, chúng ta không thể sửa đổi mà cần mở rộng. Vậy làm thế nào để giải quyết vấn đề này, hãy theo dõi đoạn code bên dưới:

<?php
interface AreaInterface
{
    public  function calculateArea();
}

class Rectangle implements AreaInterface
{
    public $width;
    public $height;

    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
    }
    public  function calculateArea(){
        $area = $this->height *  $this->width;
        return $area;
    }
}
  
class Circle implements  AreaInterface
{
    public  $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }
    
    public  function calculateArea(){
        $area = $this->radius * $this->radius * pi();
        return $area;
    }
}

class CostManager
{
    public function calculate(AreaInterface $shape)
    {
        $costPerUnit = 1.5;
        $totalCost = $costPerUnit * $shape->calculateArea();
        return $totalCost;
    }
}

$circle = new Circle(5);
$obj = new CostManager();
echo $obj->calculate($circle);

Bây giờ có thể tính diện tích mà không sửa đổi class CostManager.

L — Liskov Substitution Principle

Nguyên lý này nói rằng, objects phải thay thế được bằng instance của subtype mà không thay đổi việc chạy đúng của chức năng hệ thống.

Hãy tưởng tượng bạn quản lý hai loại máy pha cà phê. Theo kế hoạch người dùng, chúng tôi sẽ sử dụng máy pha cà phê cơ bản hoặc cao cấp, sự khác biệt duy nhất là máy cao cấp tạo ra cà phê vani tốt hơn máy cơ bản. Hành vi chương trình chính phải giống nhau cho cả hai máy

<?php

interface CoffeeMachineInterface {
    public function brewCoffee($selection);
}


class BasicCoffeeMachine implements CoffeeMachineInterface {
  
    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }
    
    protected function brewEspresso() {
        // Brew an espresso...
    }
}


class PremiumCoffeeMachine extends BasicCoffeeMachine {
  
    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            case 'VANILLA':
                return $this->brewVanillaCoffee();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }
    
    protected function brewVanillaCoffee() {
        // Brew a vanilla coffee...
    }
}


function getCoffeeMachine(User $user) {
    switch ($user->getPlan()) {
        case 'PREMIUM':
            return new PremiumCoffeeMachine();
        case 'BASIC':
        default:
            return new BasicCoffeeMachine();
    }
}


function prepareCoffee(User $user, $selection) {
    $coffeeMachine = getCoffeeMachine($user);
    return $coffeeMachine->brewCoffee($selection);
}

I — Interface Segregation Principle

Không nên sử dụng Interface lớn mà thay vào đó tách ra thành các Interface nhỏ hơn, với các mục đích cụ thể.

Nguyên tắt này cho phép phát triển các Interface cụ thể thay vì các Interface chung chung.

Hãy tưởng tượng, tương lai bạn sẽ tạo một FutureCar có thể chạy và bay 😄 (tưởng tượng cao tí), như sau:

<?php

interface VehicleInterface {
    public function drive();
    public function fly();
}

class FutureCar implements VehicleInterface {
    
    public function drive() {
        echo 'Driving a future car!';
    }
  
    public function fly() {
        echo 'Flying a future car!';
    }
}

class Car implements VehicleInterface {
    
    public function drive() {
        echo 'Driving a car!';
    }
  
    public function fly() {
        throw new Exception('Not implemented method');
    }
}

class Airplane implements VehicleInterface {
  
    public function drive() {
        throw new Exception('Not implemented method');
    }
    
    public function fly() {
        echo 'Flying an airplane!';
    }
}

Vấn đề chính ở đây bạn có thể thấy, đó là Car và Airplane có những phương thức trên nhưng mà không sử dụng lại được. Giải pháp là chia VehicleInterface thành hai Interface cụ thể hơn, chỉ được sử dụng khi cần thiết, như sau:

<?php

interface CarInterface {
    public function drive();
}

interface AirplaneInterface {
    public function fly();
}

class FutureCar implements CarInterface, AirplaneInterface {
    
    public function drive() {
        echo 'Driving a future car!';
    }
  
    public function fly() {
        echo 'Flying a future car!';
    }
}

class Car implements CarInterface {
    
    public function drive() {
        echo 'Driving a car!';
    }
}

class Airplane implements AirplaneInterface {
    
    public function fly() {
        echo 'Flying an airplane!';
    }
}

D — Dependency Inversion Principle

A. Các modules cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả hai nên phụ thuộc vào abstractions. B. Abstractions không nên phụ thuộc vào chi tiết. Thông tin chi tiết nên phụ thuộc vào abstractions. Robert C. Martin

Nguyên tắc này cho phép lập trình viên viết code tách rời và dễ dàng tái sử dụng nhiều nơi.

Hãy xét ví dụ sau:

<?php

class UserDB {
  
    private $dbConnection;
    
    public function __construct(MySQLConnection $dbConnection) {
        $this->$dbConnection = $dbConnection;
    }
  
    public function store(User $user) {
        // Store the user into a database...
    }
}

Trong trường hợp này, class 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ông cụ cơ sở dữ liệu đang sử dụng, chúng ta cần viết lại class này và việc này làm vi phạm nguyên tắc Open-Closed.

Giải pháp là phát triển abstraction kết nối cơ sở dữ liệu:

<?php

interface DBConnectionInterface {
    public function connect();
}

class MySQLConnection implements DBConnectionInterface {
  
    public function connect() {
        // Return the MySQL connection...
    }
}

class UserDB {
  
    private $dbConnection;
    
    public function __construct(DBConnectionInterface $dbConnection) {
        $this->$dbConnection = $dbConnection;
    }
  
    public function store(User $user) {
        // Store the user into a database...
    }
}

Lợi ích

Tuân theo các Nguyên tắc SOLID cho chúng ta nhiều lợi ích, chúng làm cho hệ thống của chúng ta có thể tái sử dụng, tái cấu trúc, dễ dàng maintain, mở rộng, phát triển và hơn thế nữa.

Trên đây là bài viết tìm hiểu về nguyên lý SOLID mình tham khảo từ các bài viết: SOLID Principles made easy của tác giả Dhurim Kelmendi và SOLID Principles Simplified with Examples in PHP của tác giả Ideneal.

Hy vọng bài viết sẽ giúp bạn cải thiện chất lượng trong quá trình code hơn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.