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. Hầu hết lập trình viên đều đã 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 chỉ dẫn để giúp chúng ta sử dụng được OOP hiệu quả hơn đó là nguyên tắc SOLID. Về cơ bản, SOLID 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, sau đó đượ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 này bao gồm:

Các nguyên tắc trên được phát biể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ự, ta sẽ phải sửa lại lớp “Employee”, thêm phương thức mới vào sao? 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.

Áp dụng nguyên tắc Single Responsibility: 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 (instance) 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 mới này cũng lại 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 interface này.

Để 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ành phần hệ thống (class, module, …) chỉ nên phụ thuộc vào những abstractions, không nên phụ thuộc vào các concretions hoặc implementations 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ả.

Lấy ví dụ về ổ cứng của máy tính, bạn có thể dùng loại ổ cứng thể 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à implementation cụ thể.

Trong khi lập trình cũng vậy, khi áp dụng nguyên lý này, ở những lớp trừu tượng cấp cao, ta thường sử dụng interface nhiều hơn thay vì một kiểu kế thừa cụ thể. Ví dụ, để kết nối tới Database, ta thường thiết kế lớp trừu tượng DataAccess có các phương thức phương thức chung như save(), get(), … Sau đó tùy vào việc sử dụng loại DBMS nào (vd: MySql, MongoDB, …) mà ta kế thừa và implement những phương thức này. Tính chất đa hình của OOP được vận dụng rất nhiều trong nguyên lý này.

Để 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é.

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.

16 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

  15. Hi blogger, i’ve been reading your content for some time and I really like coming
    back here. I can see that you probably don’t make money on your site.
    I know one interesting method of earning money, I think you will like it.
    Search google for: dracko’s tricks

    Like

  16. Pingback: Tìm hiểu nhanh về SOLID qua các ví dụ trong PHP - Chia sẻ kiến thức liên quan về lập trình

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s