[ SOLID là gì ] Tìm hiểu SOLID để trở thành Dev chất!

        Lập trình hướng đối tượng (object oriented programming – OOP) là một trong những mô hình lập trình được sử dụng nhiều nhất và cũng là một trong những mô hình hiệu quả nhất để mô hình hoá thế giới thực vào thế giới code. Các tính chất đặc biệt khiến việc “hướng đối tượng” trở nên hiệu quả đó là:

  • Tính trừu tượng (abstraction): Tạo ra các lớp trừu tượng mô hình hoá các đối tượng trong thế giới thực.
  • Tính đóng gói (Encapsulation): Các thực thể của lớp trừu tượng có các giá trị thuộc tính riêng biệt.
  • Tính kế thừa (Inheritance): Các đối tượng có thể dễ dàng kế thừa và mở rộng lẫn nhau.
  • Tính đa hình (Polymorphism): Có thể thực hiện một hành động đơn theo nhiều cách thức khác nhau tuỳ theo loại đối tượng cụ thể đang được gọi.

        Các tính chất đặc biệt này của OOP giúp chúng ta xây dựng được các chương trình giải quyết được nhiều vấn đề cụ thể khác nhau trong thế giới thực. Chúng ta hầu hết đã biết các tính chất này của OOP, nhưng cách thức để phối hợp các tính chất này với nhau để tăng hiệu quả của ứng dụng thì không phải ai cũng nắm được. Một trong những nguyên tắc (chỉ dẫn) để giúp chúng ta xây dựng được các ứng dụng OOP hiệu quả hơn đó là SOLID (đây là từ viết tắt), nó là một bộ 5 chỉ dẫn đã được nhắc tới từ lâu bởi các nhà phát triển phần mềm và được tổng hợp và phát biểu thành nguyên tắc bởi Robert C. Martin, cũng chính là tác giả của cuốn sách The Clean Coder nổi tiếng mà mình đã từng review. Năm nguyên tắc (chỉ dẫn) này bao gồm:

      Một cách trực quan nhất thì các nguyên tắc trên được hiểu như sau:

1.   SRP – Single responsibility principle

Phát biểu: Mỗi lớp chỉ nên chịu trách nhiệm về một nhiệm vụ cụ thể nào đó mà thôi.

      Hình dung rằng nhân viên của một công ty phần mềm cần phải làm 1 trong 3 việc sau đây: lập trình phần mềm (developer), kiểm tra phần mềm (tester), bán phần mềm (salesman). Mỗi nhân viên sẽ có một chức vụ và dựa vào chức vụ sẽ làm công việc tương ứng. Khi đó bạn có nên thiết kế lớp “Employee” với thuộc tính “position” và 3 phương thức developSoftware(), testSoftware() và saleSoftware() không?

srp.png
Hình 1 – Minh hoạ nguyên tắc Single responsibility.

        Mô tả bằng code như sau:

class Employee
{
    string position;

    function developSoftware(){};
    function testSoftware(){};
    function saleSoftware(){};
}

          Câu trả lời là không. Thử hình dung nếu có thêm một chức vụ nữa là quản lí nhân sự, không lẽ ta phải vào sửa lại lớp “Employee” và thêm phương thức mới vào? Nếu có thêm 10 chức vụ nữa thì sao? Khi đó các đối tượng được tạo ra sẽ dư thừa rất nhiều phương thức: Developer thì đâu cần dùng hàm testSoftware() và saleSoftware() đúng không nào, lỡ may dùng lầm phương thức cũng sẽ gây hậu quả khôn lường.

      Nguyên tắc này nói rằng: mỗi lớp 1 trách nhiệm. Ta sẽ tạo 1 lớp trừu tượng là “Employee” có phương thức là “working()”, từ đây bạn kế thừa ra 3 lớp cụ thể là Developer, Tester và Salesman. Ở mỗi lớp này bạn sẽ implement phương thức “working()” cụ thể tuy theo nhiệm vụ của từng người. Khi đó chúng ta sẽ bị tình trạng dùng nhầm phương thức nữa.

        Để hiểu rõ hơn, mọi người có thể xem chi tiết tại: Single responsibility principle.

2.   OCP – Open-Closed principle

        Phát biểu: Không được sửa đổi một Class có sẵn, nhưng có thể mở rộng bằng kế thừa.

Hình dung rằng “tiện nghi sống” của bạn đang là 1 căn nhà, bây giờ bạn muốn có thêm 1 tính năng là “hồ bơi” để thư giãn. Bạn có 2 cách để làm điều này:

  • Cách 1: thay đổi hiện trạng của căn nhà, xây thêm 1 tầng nữa để làm hồ bơi.
  • Cách 2: không làm thay đổi căn nhà đang có, mua thêm 1 mảnh đất cạnh nhà bạn và xây hồ bơi ở đó.
ocp.png
Hình 2 – Minh hoạ nguyên tắc Open-Closed.

        Mặc dù cả 2 cách đều giải quyết được vấn đề nhưng cách đầu tiên có vẻ rất thiếu tự nhiên và kì cục. Cách này làm thay đổi hiện trạng của căn nhà, và nếu không cẩn thận có thể làm hư luôn những thứ đang có. Cách thứ 2 an toàn hơn rất nhiều và đáp ứng tốt được nhu cầu muốn có hồ bơi của bạn.

        Nguyên tắc này có ý rằng: không được thay đổi hiện trạng của các lớp có sẵn, nếu muốn thêm tính năng mới, thì hãy mở rộng bằng cách kế thừa để xây dựng class mới. Làm như vậy sẽ tránh được các tình huống làm hỏng tính ổn định của chương trình đang có.

        Để hiểu rõ hơn, mọi người có thể xem chi tiết tại: Open – Closed principle.

3.   LSP – Liskov substitution principle

        Phát biểu: Các đối tượng kiểu class con có thể thay thế các đối tượng kiểu class cha mà không gây ra lỗi.

Screen Shot 2017-02-01 at 12.35.22 AM.png
Hình 3 – Minh hoạ một trường hợp vi phạm nguyên tắc Liskov substitution. Nếu thiết kế lớp như thế này, thì lớp CleanerStaff sẽ dùng được hàm checkAttendance(), mà điều này là không đúng, nên đây sẽ là một kiểu thiết kế sai nguyên tắc.

        Quay trở lại ví dụ lớp Emloyee trong phần 1, ta giả sử có công ty sẽ điểm danh vào mỗi buổi sáng, và chỉ có các nhân viên thuộc biên chế chính thức mới được phép điểm danh. Ta bổ sung phương thức “checkAttendance()” vào lớp Employee.

        Hình dung có một trường hợp sau: công ty thuê một nhân viên lao công để làm vệ sinh văn phòng, mặc dù là một người làm việc cho công ty nhưng do không được cấp số ID nên không được xem là một nhân viên bình thường, mà chỉ là một nhân viên thời vụ, do đó sẽ không được điểm danh.

       Nguyên tắc này nói rằng: Nếu chúng ta tạo ra một lớp cleanerStaff kế thừa từ lớp Employee, và implement hàm “working()” cho lớp này, thì mọi thứ đều ổn, tuy nhiên lớp này cũng vẫn sẽ có hàm “checkAttendance()” để điểm danh, mà như thế là sai quy định dẫn đến chương trình bị lỗi. Như vậy, thiết kế lớp cleanerStaff kế thừa từ lớp Employee là không được phép. Có nhiều cách để giải quyết tình huống này ví dụ như tách hàm checkAttendance() ra một interface riêng và chỉ cho các lớp Developer, Tester và Salesman implements.

        Để hiểu rõ hơn, mọi người có thể xem chi tiết tại: Liskov substitution principle.

4.   ISP – Interface segregation principle

         Phát biểu: Nếu Interface quá lớn thì nên tách thành các interface nhỏ hơn.

isp.png
Hình 4 – Minh hoạ nguyên tắc Interface segregation – tách interface lớn thành các interface nhỏ hơn.

       Hình dung rằng nếu bạn thiết kế các lớp Ultilities, sẽ có hàng trăm các methods khác nhau, các methods này hỗ trợ các tính toán và xử lí bổ trợ liên quan tới user, tới report, tới DB … Khi methods trở nên quá nhiều thì chúng ta nên tách Interface lớn thành các interface nhỏ hơn, ví dụ như: UserUltilities, ReportUltilities, DBUltilities, … như vậy sẽ dễ dàng quản lí và sử dụng hơn.

        Trong thực tế, bằng việc tách ra các interface nhỏ hơn, ta sẽ dễ dàng áp dụng các quy tắc khác trong SOLID hơn, điều đó làm code trở nên trong sáng và hiệu quả hơn.

        Để hiểu rõ hơn, mọi người có thể xem chi tiết tại: Interface segregation principle.

5.   DIP – Dependency inversion principle

       Phát biểu: Các thực thể (class, module) chỉ nên phụ thuộc vào các abstraction, chứ không phải các đối tượng được implement cụ thể.

Screen Shot 2017-02-01 at 12.19.42 AM.png
Hình 5 – Minh hoạ nguyên tắc Dependency inversion – Mọi thứ nên phụ thuộc vào abstraction chứ không nên phụ thuộc vào implement cụ thể nào cả.

         Hình dung rằng máy tính của chúng ta hoạt động dựa được thì phải có đủ Mainboard, CPU, RAM, ổ cứng. Về phần ổ cứng, bạn có thể dùng loại ổ rắn SSD đời mới để chạy cho nhanh, tuy nhiên cũng có thể dùng ổ đĩa quay HDD thông thường. Nhà sản xuất Mainboard không thể nào biết bạn sẽ dùng ổ SSD hay loại HDD đĩa quay thông thường, tuy nhiên họ sẽ luôn đảm bảo rằng bạn có thể dùng bất cứ thứ gì bạn muốn, miễn là ổ đĩa cứng đó phải có chuẩn giao tiếp SATA để có thể gắn được vào bo mạch chủ. Ở đây chuẩn giao tiếp SATA chính là interface, còn SSD hay HDD đĩa quay là implement cụ thể.

     Quay trở lại với ví dụ nhân viên, lớp trừu tượng Employee sẽ có phương thức “working()”, đây là 1 dạng abstraction, với từng loại nhân viên cụ thể thì sẽ implement hàm này khác nhau ứng với các công việc như developSoftware, testSoftware, … Bằng cách dùng abstruction như thế này, lúc gọi xử lí, ta không cần biết là đối tượng của ta thuộc loại nào, ta chỉ cần gọi Employee->working() là sẽ có kết quả mong muốn. (Nếu ta không thiết kế theo Dependency inversion này, mà implement cụ thể: lớp Developer có phương thức developSoftware(), lớp Tester có phương thức testSoftware(), … khi đó code của ta sẽ không tận dụng được tính đa hình để xử lí linh hoạt được nếu như ta không biết kiểu đối tượng là gì)

        Để hiểu rõ hơn, mọi người có thể xem chi tiết tại: Dependency inversion principle.

Tổng kết

         Trên đây là 5 nguyên tắc thiết kế hướng đối tượng – được gọi là SOLID – mà bất cứ lập trình viên nào cũng phải nắm vững nếu như muốn cải thiện kĩ năng code của mình. Áp dụng các nguyên tắc này vào thiết kế và triển khai sẽ khiến phần mềm của bạn có code trong sáng và rành mạch hơn, dễ bảo trì hơn, dễ mở rộng hơn, có tính kế thừa cao, … và đương nhiên là sẽ trông “pro” hơn rất nhiều.

        Bài này mình chỉ giới thiệu cho mọi người tổng quan về nguyên tắc thiết kế SOLID, nhằm cung cấp cái nhìn trực quan và dễ hiểu cho hầu hết mọi người. Ở các bài viết sau mình sẽ nói chi tiết hơn về từng nguyên lí một, để giúp các bạn nắm được kĩ hơn và hiểu được cách áp dụng của nó, các bạn nhớ đón xem nhé. Cảm ơn rất nhiều.

Vcttai

Tham khảo

  1. SOLID architecture principles.
  2. Nguyên tắc Solid và ví dụ.
  3. Code cứng với SOLID.
  4. The SOLID principles. (Chi tiết)
  5. Object oriented design principles.

14 thoughts on “[ SOLID là gì ] Tìm hiểu SOLID để trở thành Dev chất!

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

  2. Pingback: [Giới thiệu sách] The art of readable code – Cái tên đã nói lên tất cả – Webbynat

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

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

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

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

  7. Pingback: Chuyện lập trình viên, nước mía và sinh tố. – Những dòng code vui

  8. Pingback: [Giới thiệu sách] Sách hay đầu năm 2017 (P1) – Những dòng code vui

  9. Pingback: [ Giới thiệu sách ] The pragmatic programmer – Lập trình viên … tiêu biểu. (P1) – Những dòng code vui

  10. Pingback: [ Giới thiệu sách ] The pragmatic programmer – Lập trình viên … tiêu biểu. (P2) – Những dòng code vui

  11. Pingback: Thật ra thì … phát triển phần mềm là gì? – Những dòng code vui

  12. Pingback: [Code sao cho chuẩn] – Bài 1: Hãy bắt đầu nghĩ tới việc viết code đẹp hơn và dễ hiểu hơn. – Những dòng code vui

  13. Pingback: [Code sao cho chuẩn] – Phần 4: Chúng ta nên định dạng code như thế nào? – Những dòng code vui

  14. Pingback: Nếu bạn muốn học lập trình PHP với Laravel framework. – Những dòng code vui

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