Series PHP - Lập trình hướng đối tượng (Phần 3)

Chào mọi người, hôm này mình trở lại đây tạm biệt series này bằng bài viết về Các phương pháp thiết kế hướng đối tượng (SOLID). Các bạn có thể xem lại Phần 1Phần 2 hoặc theo dõi bằng mục lục ở dưới như mọi khi nhé.

Nội dung bài viết

S.O.L.I.D: "DÙNG LÀ CỨNG!"

Không chắc là có cứng được tất cả mọi thứ nhưng nếu áp dụng được thì ít nhất code của bạn sẽ rất dễ đọc, dễ test và việc bảo trì sẽ nhẹ nhàng hơn nhiều, bởi nó không chỉ là lý thuyết mà chính là sự đúc kết từ vô vàn những dự án thành công và thất bại của các developer trên toàn thế giới. Và dưới đây là những điều cơ bản nhất của từng chữ cái trong S.O.L.I.D mà mình muốn gửi đến các bạn:

S: Single responsibility principle

Một class chỉ nên giữ một trách nhiệm duy nhất (Chỉ có thể thay đổi class vì một lý do duy nhất)

Việc một class có nhiều chức năng dẫn đến code quá cồng kềnh, việc thay đổi code sẽ rất khó khăn, mất nhiều thời gian, còn dễ gây ảnh hưởng tới các module đang hoạt động khác. Thay vào đó, ta tách các class ra thành từng chức năng riêng biệt, việc thay đổi và bảo trì sẽ thuận tiện hơn rất nhiều. Ví dụ 1 chút về việc tính diện tích các hình nhé:

class Circle {
    public $radius;

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

class Square {
    public $length;

    public function __construct($length) {
        $this->length = $length;
    }
}
class AreaCalculator {

    protected $shapes;

    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }

    public function sum() {
        // logic to sum the areas
    }

    public function output() {
        return implode('', array(
            "",
                "Sum of the areas of provided shapes: ",
                $this->sum(),
            ""
        ));
    }
}

$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);

echo $areas->output();

Khá ngon đúng không, nhưng mà giờ output mình muốn lấy ra theo nhiều kiểu như HTML, JSON,... thì đúng là ba chấm nhỉ. Vậy thử edit lại 1 chút xem:

$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();
echo $output->HTML();

Việc lớp SumCalculatorOutputter được tạo ra và hoạt động như trên sẽ giải quyết được vấn đề này.

O: Open/closed principle

Có thể thoải mái mở rộng 1 module, nhưng hạn chế sửa đổi bên trong module đó (open for extension but closed for modification)

Đơn giản có thể hiểu như việc sử dụng 1 cây súng, việc thay đổi các chi tiết bên trong khẩu súng để tăng độ chính xác và giảm âm thanh có lẽ không khả thi bằng việc chỉ cần lắp thêm bộ phận giảm thanh vào là được. Nhưng mà tạm gác súng ống lại và tiếp tục để ý hàm sum ở trong class AreaCalculator trên kia nhé:

public function sum() {
    foreach($this->shapes as $shape) {
        if(is_a($shape, 'Square')) {
            $area[] = pow($shape->length, 2);
        } else if(is_a($shape, 'Circle')) {
            $area[] = pi() * pow($shape->radius, 2);
        }
    }

    return array_sum($area);
}   

Càng nhiều hình dạng thì sẽ càng nhiều if else, như vậy sẽ đi ngược lại quy tắc O mà chúng ta đang nói đến đúng không. Vậy thì ta hãy đưa việc tính diện tích vào trong class của mỗi loại hình, thay vào việc kiểm tra xem đó là hình loại nào thì ta sẽ kiểm tra xem nó có phải là ShapeInterface không bằng cách:

interface ShapeInterface {
    public function area();
}

class Circle implements ShapeInterface {
    public $radius;

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

    public function area() {
        return pi() * pow($this->radius, 2);
    }
}

public function sum() {
    foreach($this->shapes as $shape) {
        if(is_a($shape, 'ShapeInterface')) {
            $area[] = $shape->area();
            continue;
        }

        throw new AreaCalculatorInvalidShapeException;
    }

    return array_sum($area);
}    

L: Liskov substitution principle

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

Ở ví dụ hình học trên, chúng ta có thêm yêu cầu tính thể tích khi đó là hình hộp:

class VolumeCalculator extends AreaCalulator {
    public function __construct($shapes = array()) {
        parent::__construct($shapes);
    }

    public function sum() {
        // logic to calculate the volumes and then return and array of output
        return array($summedData);
    }
}    

Trong class SumCalculatorOutputter:

class SumCalculatorOutputter {
    protected $calculator;

    public function __constructor(AreaCalculator $calculator) {
        $this->calculator = $calculator;
    }

    public function JSON() {
        $data = array(
            'sum' => $this->calculator->sum();
        );

        return json_encode($data);
    }

    public function HTML() {
        return implode('', array(
            '',
                'Sum of the areas of provided shapes: ',
                $this->calculator->sum(),
            ''
        ));
    }
}    
$areas = new AreaCalculator($shapes);
$volumes = new AreaCalculator($solidShapes);

$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);

Việc cố gắng gọi phương thức HTML với $output2 sẽ gây ra lỗi E_NOTICE, thay vì return một mảng từ SumCalculator class sum, bạn chỉ cần:

public function sum() {
    // logic to calculate the volumes and then return and array of output
    return $summedData;
}

I: Interface segregation principle

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ể

Vẫn tiếp tục với ví dụ hình học, làm luôn:

interface ShapeInterface {
    public function area();
    public function volume();
}   

Sai sai so với quote ở trên thì phải, chỉnh sửa lại cho hợp lý hơn vậy:

interface ShapeInterface {
    public function area();
}

interface SolidShapeInterface {
    public function volume();
}

class Cuboid implements ShapeInterface, SolidShapeInterface {
    public function area() {
        // calculate the surface area of the cuboid
    }

    public function volume() {
        // calculate the volume of the cuboid
    }
}

Khá ổn rồi nhưng hơi khổ khi dùng hàm sum trong AreaCalculator vì lại phải kiểm tra xem ShapeInterface hay SolidShapeInterface. Thôi thì như vậy nhé:

interface ManageShapeInterface {
    public function calculate();
}

class Square implements ShapeInterface, ManageShapeInterface {
    public function area() { /*Do stuff here*/ }

    public function calculate() {
        return $this->area();
    }
}

class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface {
    public function area() { /*Do stuff here*/ }
    public function volume() { /*Do stuff here*/ }

    public function calculate() {
        return $this->area() + $this->volume();
    }
}    

D: Dependency inversion principle

Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Chúng 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)

Xem thử ví dụ này nhé:

class PasswordReminder {
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

Đầu tiên MySQLConnection là module mức thấp trong khi PasswordReminder ở mức cao, sau đó nếu bạn thay đổi cơ sở dữ liệu, bạn cũng phải chỉnh sửa lớp PasswordReminder. Vi phạm cả DO luôn rồi bạn à 😐 Thay vào đó:

interface DBConnectionInterface {
    public function connect();
}  

class MySQLConnection implements DBConnectionInterface {
    public function connect() {
        return "Database connection";
    }
}

class PasswordReminder {
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

Module mức cao và thấp đều đã phụ thuộc vào interface. Ổn rồi đúng không.

Kết bài

Series PHP - Lập trình hướng đối tượng của mình đến đây là hết. Các chủ đề được chọn dựa trên những phần trọng tâm và tổng hợp từ nhiều nguồn. Hy vọng các bạn sẽ tìm được điều gì đó bổ ích cho bản thân. Hẹn gặp lại mọi người ở những bài viết khác :3