Tiếp tục về nội dung OOP của bài trước, nay chúng ta cùng nhau tìm hiểu về nguyên lý "S.O.L.I.D" để phát triển một phần mềm dễ maintain và scale nhất, hoặc nên biết để chém gió hoặc áp dụng vào bài toán cụ thể nào đó. Mình nghĩ khi bạn tìm đọc và nghiên cứu nguyên lý này rồi thì bạn cũng đang ở một level nào đó và tự nhận thức được năng lực của mình khá là "OK" rồi.

Còn về nguồn gốc thì nguyên lý này được phát minh bởi Robert C. Martin vào năm 1995.

Bản thân "S.O.L.I.D" được viết tắt từ 5 nguyên tắc trên, giờ cùng tìm hiểu từng nguyên tắc và bài toán cụ thể nhé.

1. S - Single Responsibility

A class should have one and only one reason to change, meaning that a class should have only one job.Một class nên có một và chỉ một lý do để thay đổi, có nghĩa là một class chỉ làm duy nhất một việc

VD:

Chúng ta đều làm việc với mô hình MVC rồi đúng không, ở đây chúng ta sẽ nói đến Model. Chúng ta làm gì với model ?

Thêm, sửa, xóa, hiển thị ... rất nhiều đúng không ? Giờ áp dụng nguyên tắc này như thế nào ?

Một class chỉ làm duy nhất một việc tức là nó chỉ thêm hoặc chỉ xóa hoặc chỉ cập nhật .... điên thật, mỗi tính năng cần một class. Như vậy số lượng class là cực kỳ lớn.

Nhưng

Giờ thì khi sửa mỗi tính năng thì cực kỳ đơn giản, dễ test, dễ tìm, và số lượng code bên trong file không thể bị phình to, ngoài ra ta dễ kế thừa và áp dụng các design partern khác vào đây, khá "OK" đúng không ?

2. O - Open/Close

Open for extension, meaning that the class’s behavior can be extended.
Closed for modification, meaning that the source code is set and cannot be changed.Dễ mở rộng, khó sửa đổi

Đại khái thì theo nguyên lý này một class khi có thêm tính năng gì đó mới thì ta nên tạo class mới kế thừa lại class cũ kia và phát triển tính năng đó, không nên sửa trực tiếp vào class cha.

VD:

class Swimming
{
    protected string $swimingType;
    public function isSwimming() : boolean 
    {
        return isset($swimingType);
    }
}

Rồi giờ bơi ngửa nào, thêm một function:

//...
public function isBackstroke() : boolean
{
    return $swimingType === 'backstroke';
}

=> KHÔNG NHÉ

Áp dụng nguyên lý này, hãy dùng interface hoặc tạo class mới liên quan đến bơi ngửa và kế thừa lại class bơi, như vậy mới đúng theo nguyên lý. Để làm gì ?

=> Rõ ràng rồi, không hề phụ thuộc vào class cũ, những code cũ đã sử dụng ở đâu thì ở đó vẫn chạy ngon lành, thêm cái mới vào thì chỉ chạy cái mới thôi không liên quan gì đến phần cũ.

3. L - Liskov Substitution

Cái tên Liskov này ở đây bởi vì nguyên lý này được tạo ra bởi Barbara Liskov, người đã giới thiệu khái niệm phân nhóm hành vi này vào năm 1987.

Mé, cái nguyên tắc này khó hiểu vl.

if for each object O1 of type S there is an object O2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when O1 is substituted for O2 then S is a subtype of T.Với mỗi Object con(O1) có một Object khác(O2) trong chương trình chạy, và chương trình vẫn chạy ngon nghẻ khi Object(02) được thay thế cho Object(O1)

=> Được nhiều bài dịch là "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"

VD:

Với Xe Oto thì có xe các xe oto hãng Honda, Toyota, Vinfast đều kế thừa lớp Xe oto này và đổ xăng là chạy phà phà, giờ ông xe điện Tesla cũng kế thừa lớp xe oto này như phải chạy bằng điện cơ => Lỗi. Không đúng nguyên lý này.

Như vậy bằng cách nào đó phải để lớp xe điện Tesla này có thể biến thành class tương tự class Xe oto như vậy mới đúng.

4. I - Interface Segregation

Make fine grained interfaces that are client-specific. Clients should not be forced to implement interfaces they do not useThay 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ể

Cái này thì dễ rồi, giờ ta tạo ra một interface có chức năng thêm, sửa, xóa

interface ActionInterface
{
    public function add() : mixed;
    public function show() : mixed;
    public function edit() : mixed;
    public function remove() : mixed;
}

Giờ khi muốn implement lại class này thì class con phải khai báo đủ 4 phương thức trên. Vậy nếu tôi chỉ muốn tính năng add thôi thì tôi cũng lại khai báo lại cả tính năng xóa, sửa .. làm gì ?

Thay vì tạo ra một interface rồi ném cả đống action mà không dùng đến này gây thừa thãi và tốn dung lượng thì ta tách thành nhiều interface nhỏ khác có các hàm trong đó, VD: ShowAndListActionInterface, AddAndRemoveActionInterface ....

5. D - Dependency Inversion

High level modules should not depend upon low level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend upon abstractions.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à chi tiết phụ thuộc vào Interface. ( Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)

Cái này anh em nào biết DI (Dependency Injection) sẽ dễ hiểu hơn vì nó chính là áp dụng nguyên lý này.

Các module cấp cao không nên phụ thuộc module cấp thấp, tức là sao ?

VD: Nông dân dùng con trâu để làm việc.

=> Nông dân là module cấp cao

=> Con trâu là module cấp thấp

=> Sai

Nông dân (là một đối tượng lao động) dùng con trâu (là một công cụ lao động) để làm việc.

=> Đối tượng lao động dùng công cụ lao động để làm việc

=> Đúng

Như vậy ta trừu tượng hóa lên như vậy sẽ là áp dụng đúng nguyên lý này. Vì mai sau trong bài toán có thể thay đổi như ruộng đất được mua lại để mở công ty thì lúc ý phải là "Công nhân sử dụng máy may để làm việc" chẳng hạn, dù hai đối tượng này thay dổi như nào đi nữa nó vẫn đúng.

Interface (abstraction) không nên phụ thuộc vào chi tiết, mà chi tiết phụ thuộc vào Interface.

Như vậy ở ví dụ trên ta không quan tâm đến nông dân phải làm gì ? chi tiết như nào ? rồi từ đó thiết kế interface là sai.

Ta phải quan tâm đến đối tượng lao động làm gì ?

Ví dụ:

interface Đối tượng lao động
{
    public function làm();
    public function ăn();
}

Giờ thì chi tiết ở đây là các class (chi tiết) như Nông dân, Công nhân, Lập trình viên ... sẽ làm như nào ? ăn như nào ? => chi tiết phụ thuộc vào Interface,