[ SOLID là gì ] Nguyên tắc 2: Đóng và Mở – Open / Closed principle (OCP)

        Chúng ta tiếp tục tìm hiểu bộ nguyên tắc lập trình SOLID – là các nguyên tắc giúp chúng ta thiết kế chương trình và viết code tốt hơn: code trong sáng và rành mạch, dễ bảo trì, dễ mở rộng trong tương lai. Cùng nhắc lại một chút, SOLID gồm 5 nguyên tắc lập trình sau đây:

  1. Single Responsibility principle.
  2. Open-Closed principle.
  3. Liskov substitution principle.
  4. Interface segregation principle.
  5. Dependency inversion principle.

         Ở bài trước, chúng ta đã tìm hiểu về tính “đơn nhiệm” và các lợi ích của nó. Hôm nay, chúng ta sẽ đi phân tích và tìm hiểu nguyên lí thứ hai: Open/Closed priciple – Dễ mở rộng, Khó sửa đổi.

Nguyên tắc thứ 2 của SOLID nói gì?

        Phát biểu: Chúng ta nên hạn chế việc chỉnh sửa bên trong một Class hoặc Module có sẵn, thay vào đó hãy xem xét mở rộng chúng.

(Software modules should be closed for modifications but open for extensions)

        Nhìn vào phát biểu trên, chúng ta thấy rằng, có hai vấn đề cần lưu tâm trong nguyên lí này:

  • Hạn chế sửa đổi: Ta không nên chỉnh sửa source code của một module hoặc class có sẵn, vì sẽ ảnh hưởng tới tính đúng đắn của chương trình.
  • Ưu tiên mở rộng: Khi cần thêm tính năng mới, ta nên kế thừa và mở rộng các module/class có sẵn thành các module con lớn hơn. Các module/class con vừa có các đặc tính của lớp cha (đã được kiểm chứng đúng đắn), vừa được bổ sung tính năng mới phù hợp với yêu cầu.

        Chúng ta có thể hình dung nguyên lí này trong thực tế như thế nào nhỉ? Hãy thử hình dung thế này: bạn có một chiếc máy ảnh DSLR, bạn đã có một ống kính kèm theo máy để chụp phong cảnh hoa lá, bây giờ bạn muốn có tình năng “chuyên chụp chân dung” thì làm thế nào?

        Chúng ta sẽ mở rộng bộ máy ảnh ra bằng dùng các ống kính khác, ví dụ như các ống kính được thiết kế để chuyên chụp chân dung, chứ không nên đem cái ống kính đang có ra để sửa lại. Tương tự, máy ảnh cũng có đèn flash nhưng công suất yếu, bạn muốn đèn sáng hơn thì hãy ráp thêm một cái flash rời, không nên mở cái flash bên trong máy ra để thay bóng đèn công suất cao hơn, điều đó có thể làm được nhưng rất … stupid.

abd35e9c34e64d178ca73291b9482bbd.jpg
Ta nên mở rộng thành nhiều module con để có thêm nhiều tính năng, nên cân nhắc kĩ nếu như ta muốn sửa đổi một thứ đã có sẵn và đang hoạt động tốt.

Áp dụng nguyên lí này trong phần mềm như thế nào?

        Khi bạn thiết kế một phần mềm, bạn cần chú ý tới khả năng bảo trì và mở rộng của nó. Khi tạo ra các lớp hoặc một module nào đó, hãy luôn đặt câu hỏi: code thế này đã tách bạch chưa? Làm thế này có thể dễ dàng mở rộng trong tương lai? Nếu có thì làm như thế nào? … Bằng cách đó, chúng ta luôn đặt vấn đề để cải tiến phần mềm của mình.

        Chúng ta sẽ tiếp tục ví dụ về lớp Student ở bài trước, giả sử trường học có thêm 2 loại sinh viên nữa: sinh viên hệ cử nhân tài năng và sinh viên nước ngoài. Khi đó xử lí của phần mềm chúng ta có một chút thay đổi: sinh viên hệ cử nhân tài năng sẽ được giảm 20% học phí, sinh viên nước ngoài thì học phí sẽ cao hơn 30%. Chúng ta giải quyết chức năng này thế nào đây?

         Cách thông thường chúng ta hay nghĩ đến là cách sau đây: thêm thuộc tính cho lớp Student.

class Student
{
    string name;
    int age;
    int student_type;

    string getStudentInfoJson()
    {
        return json_encode( array(name, age) );
    }

    int payTuitionFee()
    {
        if( this->student_type == 'foreign' )
            return STANDARD_FEE * 1.3;
        else if ( this->student_type == 'talented' )
            return STANDARD_FEE * 0.8;
        else
            return STANDARD_FEE;
    }
}

        Đoạn code trên hoàn toàn có thể hoạt động đúng, cho ra kết quả tính toán chính xác. Tuy nhiên, nếu bạn thiết kế chương trình như thế này thì thực sự có nhiều điểm không hợp lí lắm: Nếu chúng ta lại có thêm 1 kiểu student nữa thì sao (vd: sinh viên cao học, sinh viên tại chức, …), khi đó chúng ta lại phải vào sửa lại hàm payTuitionFee() để đáp ứng dược nhu cầu mới hay sao? Code mới lúc đó sẽ ảnh hưởng tới code cũ, như vậy có khả năng là sẽ làm hư luôn code cũ, …

         Rõ ràng là chúng ta nên có một phương pháp an toàn và thân thiện hơn.

Vậy chúng ta giải quyết thế nào bây giờ?

        Chúng ta sẽ dụng nguyên tắc thứ hai cua SOLID, để sửa lại đoạn code trên. Hãy nhớ lại nguyên lí: Hạn chế sửa đổi – ưu tiên mở rộng, điều này có nghĩa là phải thiết kế để ta sẽ không phải chỉnh sửa lại hàm payTuitionFee() trong tương lai nếu như có thêm yêu cầu mới.

        Để làm được điều này thì chúng ta sẽ tách đối tượng sinh viên cử nhân tài năng thành một lớp riêng, tách các đối tượng sinh viên nước ngoài ra một lớp riêng. Cả hai đối tượng này suy cho cùng cũng đều là sinh viên, do đó cả hai lớp mới sẽ được kế thừa từ lớp Student. Source code mới trông như sau:

  • Thiết kế lớp Student như là một lớp cơ sở, không cần thuộc tính student_type nữa:
class Student
{
    string name;
    int age;

    string getStudentInfoJson()
    {
        return json_encode( array(name, age) );
    }

    int payTuitionFee()
    {
        return STANDARD_FEE;
    }
}
  • Sau đó mở rộng các lớp con bằng cách kế thừa:
class AdvancedStudent extends Student
{
    int payTuitionFee()
    {
        return STANDARD_FEE * 0.8;
    }
}

class ForeignStudent extends Student
{
    int payTuitionFee()
    {
        return STANDARD_FEE * 1.3;
    }
}

         Có thể thấy rằng, cách thiết kế này làm cho lớp Student trở nên: ĐÓNG với mọi sự thay đổi bên trong, nhưng luôn MỞ cho sự kế thừa để mở rộng sau này. Sau này, khi nhu cầu mở rộng chương trình xuất hiện, có thêm nhiều đối tượng nữa cần xử lí (vd: sinh viên hệ từ xa, sinh viên hệ cao đẳng, …) thì chúng ta chỉ cần thêm lớp mới là sẽ giải quyết được vấn đề, trong khi vẫn đảm bảo được chương trình có sẵn không bị ảnh hưởng, nhờ đó mà hạn chế được phạm vi test, giúp giảm chi phí phát triển. Đó cũng là một trong những lợi ích ở khía cạnh dễ bảo trì sản phẩm.

Chúng ta thậm chí còn làm được nhiều việc hơn nữa

        Với nguyên tắc này, lợi ích mang lại ở việc dễ dàng mở rộng còn được thể hiện ở nhiều khía cạnh khác nữa. Giả sử như nhà trường có thêm những chính sách riêng dành cho những sinh viên nước ngoài: như yêu cầu thêm chứng chỉ ngoại ngữ Ielts, chứng minh rằng có VISA du học, … Những yêu cầu này dễ dàng mở rộng khi triển khai lớp ForeignStudent mà không hề ảnh hưởng gì tới các logic khác:

class ForeignStudent extends Student
{
    int IELTS_mark;
    string Visa;

    int payTuitionFee()
    {
        return STANDARD_FEE * 1.3;
    }

    bool isQualifyForEnglish()
    {
        return this->IELTS_mark > 8.0;
    }

    string showVisa()
    {
        return this->Visa;
    }
}

        Mọi người thấy đó, việc thiết kế các lớp dựa trên nguyên lí 2 của SOLID giúp phần mềm của chúng ta dễ bảo trì và dễ mở rộng hơn rất nhiều. Tất nhiên không phải lúc nào ta cũng không được chỉnh sửa code có sẵn, tuy nhiên thông thường thì chúng ta không nên làm như vậy. Tóm lại, SOLID là một guideline tốt giúp bạn thiết kế và lập trình tốt hơn.

nguyen-ly-solid-trong-lap-trinh-huong-doi-tuong-va-vi-du-su-dung-c-p2

Tham khảo:

  1. https://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp
  2. http://thaotrinh.info/nguyen-ly-solid-trong-lap-trinh-huong-doi-tuong-va-vi-du-su-dung-c-p2/
  3. https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

5 thoughts on “[ SOLID là gì ] Nguyên tắc 2: Đóng và Mở – Open / Closed principle (OCP)

  1. Pingback: [ SOLID là gì ] Nguyên tắc 3: Tính khả dĩ thay thế – Liskov substitution principle (LSP) – Webbynat

  2. Pingback: Tìm hiểu SOLID để trở thành Dev chất! – Webbynat

  3. Pingback: [ SOLID là gì ] Nguyên tắc 4: Chia nhỏ interface – Interface segregation principle (ISP) – Webbynat

  4. Pingback: [ SOLID là gì ] Nguyên tắc 1: Đơn nhiệm – Single Responsibility principle (SRP). – Webbynat

  5. Pingback: [ SOLID là gì ] Nguyên tắc 5: Tính tương thích động – Dependency Inversion principle (DIP) – Webbynat

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s