SOLID là gì? Áp dụng SOLID trong lập trình như thế nào? – https://final-blade.com

SOLID là gì? Áp dụng SOLID trong lập trình như thế nào?

Phần mềm được xem là tốt khi khi nó có kiến trúc tốt. Kiến trúc phần mềm tương tự như móng nhà, móng yếu nhà sẽ không vững. Để viết được phần mềm tốt bạn phải học rất nhiều, điều đầu tiên bạn cần biết là SOLID.

SOLID ra đời như thế nào?

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. 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.

SOLID là gì?

SOLID là viết tắt của 5 chữ cái đầu trong 5 nguyên tắc thiết kế hướng đối tượng. Giúp cho lập trình viên viết ra những đoạn code dễ đọc, dễ hiểu, dễ maintain. Nó được đưa ra bởi Robert C. Martin và Michael Feathers. 5 nguyên tắc đó bao gồm:

  • Single responsibility priciple (SRP)
  • Open/Closed principle (OCP)
  • Liskov substitution principe (LSP)
  • Interface segregation principle (ISP)
  • Dependency inversion principle (DIP)

Single responsibility priciple

Mỗi lớp chỉ nên chịu nghĩa vụ và trách nhiệm về một trách nhiệm đơn cử nào đó mà thôi .

Nguyên lý đầu tiên ứng với chữ S trong SOLID, có ý nghĩa là một class chỉ nên giữ một trách nhiệm duy nhất. Một class có quá nhiều chức năng sẽ trở nên cồng kềnh và trở nên khó đọc, khó maintain. Mà đối với ngành IT việc requirement thay đổi, cần thêm sửa chức năng là rất bình thường, nên việc code trong sáng, dễ đọc dễ hiểu là rất cần thiết.

Ví dụ: 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?

12345678

classEmployee

{

stringposition;

functiondevelopSoftware(){};

functiontestSoftware(){};

functionsaleSoftware(){};

}

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.

Open/Closed principle

Không được sửa đổi một Class có sẵn, nhưng hoàn toàn có thể lan rộng ra bằng thừa kế .

Nguyên lý thứ 2 ứng với chữ O trong SOLID.

Theo nguyên tắc này, mỗi khi ta muốn thêm công dụng cho chương trình, tất cả chúng ta nên viết class mới lan rộng ra class cũ ( bằng cách thừa kế hoặc sở hữu class cũ ) chứ không nên sửa đổi class cũ. Việc này dẫn đến thực trạng phát sinh nhiều class, nhưng tất cả chúng ta sẽ không cần phải test lại những class cũ nữa, mà chỉ tập trung chuyên sâu vào test những class mới, nơi chứa những công dụng mới .
Thông thường việc lan rộng ra thêm công dụng thì phải viết thêm code, vậy để phong cách thiết kế ra một module hoàn toàn có thể thuận tiện lan rộng ra nhưng lại hạn chế sửa đổi code ta cần làm gì. Cách xử lý là tách những phần dễ đổi khác ra khỏi phần khó đổi khác mà vẫn bảo vệ không ảnh hưởng tác động đến phần còn lại .

Đặt vấn đề: Ta cần 1 lớp đảm nhận việc kết nối đến CSDL. Thiết kế ban đầu chỉ có SQL Server và MySQL. Thiết kế ban đầu có dạng như sau:

1234567891011

classConnectionManager

{

publicfunctiondoConnection(ObjectUSDconnection)

{

if(USDconnectioninstanceofSqlServer){

/ / connect with SqlServer

}elseif(USDconnectioninstanceofMySql){

/ / connect with MySql

}

}

}

Sau đó yêu cầu đặt ra phải kết nối thêm đến Oracle và một vài hệ CSDL khác.
Để thêm chức năng ta phải thêm vào code những khối esleif khác, việc này làm code cồng kềnh và khó quản lý hơn.

Giải pháp:

  • Áp dụng Abstract thiết kế lại các lớp SqlServer, MySql, Oracle…
  • Các lớp này đều có chung nhiệm vụ tạo kết nối đến csdl tương ứng có thể gọi chung là Connection.
  • Cách thức kết nối đến csdl thay đổi tùy thuộc vào từng loại kết nối nhưng có thể gọi chung là doConect.
  • Vậy ta có lớp cơ sở Connection có phương thức doConnect, các lớp cụ thể là SqlServer, MySql, Oracle… kế thừa từ Connection và overwrite lại phương thức doConnect phù hợp với lớp đó.

Thiết kế sau khi làm lại có dạng như sau :

12345678910111213141516171819202122232425262728293031

abstractclassConnection()

{

publicabstractfunctiondoConnect();

}

classSqlServerextendsConnection

{

publicfunctiondoConnect()

{

/ / connect with SqlServer

}

}

classMySqlextendsConnection

{

publicfunctiondoConnect()

{

/ / connect with MySql

}

}

classConnectionManager

{

publicfunctiondoConnection(ConnectionUSDconnection)

{

/ / something

/ / ……………..

/ / connection

USDconnection->doConnect();

}

}

Với phong cách thiết kế này khi cần liên kết đến 1 loại csdl mới chỉ cần thêm 1 lớp mới thừa kế Connection mà không cần sửa đổi code của lớp ConnectionManager, điều này thỏa mãn nhu cầu 2 điều kiện kèm theo của nguyên tắc OCP .

Liskov substitution principle

Các đối tượng người tiêu dùng ( instance ) kiểu class con hoàn toàn có thể thay thế sửa chữa những đối tượng người dùng kiểu class cha mà không gây ra lỗi .

Nguyên tắc thứ 3, ứng với chữ L trong SOLID.

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 cấp dưới lao công để làm vệ sinh văn phòng, mặc dầu là một người thao tá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 cấp dưới thông thường, mà chỉ là một nhân viên cấp dưới 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.

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 tiêu đơn cử .

Nguyên lý này rất dễ hiểu. Hãy tưởng tượng tất cả chúng ta có 1 interface lớn, khoảng chừng 100 methods. Việc implements sẽ rất khó khăn vất vả vì những class impliment interface này sẽ bắt buộc phải phải thực thi hàng loạt những method của interface. Ngoài ra còn hoàn toàn có thể dư thừa vì 1 class không cần dùng hết 100 method. Khi tách interface ra thành nhiều interface nhỏ, gồm những method tương quan tới nhau, việc implement và quản trị sẽ dễ hơn .
Ví dụ :

Chúng ta có một interface Animal như sau:

12345

interfaceAnimal{

voideat();

voidrun();

voidfly();

}

Chúng ta có 2 class Dog và Snake implement interface Animal. Nhưng thật vô lý, Dog thì làm sao có thể fly(), cũng như Snake không thể nào run() được? Thay vào đó, chúng ta nên tách thành 3 interface như thế này:

1234567891011

interfaceAnimal{

voideat();

}

interfaceRunnableAnimalextendsAnimal{

voidrun();

}

interfaceFlyableAnimalextendsAnimal{

voidfly();

}

Dependency inversion principle

1. 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.
2. 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 (abstraction), không phải thông qua implementation.)

Có thể hiểu nguyên lí này như sau : những thành phần trong 1 chương trình chỉ nên nhờ vào vào những cái trừu tượng ( abstraction ). Những thành phần trừu tượng không nên nhờ vào vào những thành phần mang tính đơn cử mà nên ngược lại .
Những cái trừu tượng ( abstraction ) là những cái ít biến hóa và dịch chuyển, nó tập hợp những đặc tính chung nhất của những cái đơn cử. Những cái đơn cử dù khác nhau thế nào đi nữa đều tuân theo những quy tắc chung mà cái trừu tượng đã định ra. Việc nhờ vào vào cái trừu tượng sẽ giúp chương trình linh động và thích ứng tốt với những sự biến hóa diễn ra liên tục .
Lấy ví dụ về ổ cứng của máy tính, bạn hoàn toà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 hoàn toàn có thể dùng ổ đĩa quay HDD thường thì. 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ì. Tuy nhiên họ sẽ luôn bảo vệ rằng bạn hoàn toàn có thể dùng bất kỳ thứ gì bạn muốn, miễn là ổ đĩa cứng đó phải có chuẩn tiếp xúc SATA để hoàn toàn có thể gắn được vào bo mạch chủ. Ở đây chuẩn tiếp xúc SATA chính là interface, còn SSD hay HDD đĩa quay là implementation đơn cử .
Trong khi lập trình cũng vậy, khi vận dụng nguyên tắc 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 thừa kế đơn cử. Ví dụ, để liên kết tới Database, ta thường phong cách thiết kế lớp trừu tượng DataAccess có những phương pháp phương pháp chung như save ( ), get ( ), … Sau đó tùy vào việc sử dụng loại DBMS nào ( vd : MySql, MongoDB, … ) mà ta thừa kế và implement những phương pháp này. Tính chất đa hình của OOP được vận dụng rất nhiều trong nguyên tắc này .

Tổng kết

SOLID là 5 nguyên tắc cơ bản trong việc thiết kế phần mềm. Nó giúp chúng ta tổ chức sắp xếp các function, method, class một cách chính xác hơn. Làm sao để kết nối các thành phần, module với nhau.

Rõ ràng, dễ hiểu

Teamwork là điều không hề tránh trong lập trình. Áp dụng SOLID vào việc làm bạn sẽ tạo ra những hàm tốt, dễ hiểu hơn. Giúp cho bạn và đồng nghiệp đọc hiểu code của nhau tốt hơn .

Dễ thay đổi

SOLID giúp tạo ra những module, class rõ ràng, mạch lạc, mang tính độc lập cao. Do vậy khi có sự nhu yếu đổi khác, lan rộng ra từ người mua, ta cũng không tốn quá nhiều sức lực lao động để thực thi việc biến hóa .

Tái sử dụng

SOLID khiến những lập trình viên tâm lý nhiều hơn về cách viết ứng dụng, do vậy code viết ra sẽ mạch lạc, dễ hiểu, dễ sử dụng .
Nguồn : TopDev. vn

Chia sẻ:

  • More

Like this:

Like

Loading …