Nguyên tắc SOLID là gì? Tại sao nên áp dụng vào việc phát triển phần mềm?
SOLID là những nguyên tắc quan trọng của lập trình hướng đối tượng.
SOLID là viết tắt của 5 nguyên tắc:
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Những nguyên tắc này được giới thiệu lần đầu bởi Robert C. Martin. Sau đó được định nghĩa lại bởi Michael Feathers.
Trong bài viết này mình sẽ giới thiệu những lợi ích mà nó mang lại cũng như những ví dụ đơn giản và trực quan nhất về SOLID. (Những ví dụ viết bằng ngôn ngữ Scala)
Tại sao nên áp dụng SOLID trong việc phát triển phần mềm
Về bản chất thì mỗi design pattern hoặc principle được tổng hợp lại để giải quyết những bài toán thường gặp hay lặp đi lặp lại trong quá trình phát triển phần mềm. Thì SOLID sinh ra với mục đích tương tự.
Khi áp dụng SOLID trong việc phát triển phần mềm, thì phần mềm các bạn tạo ra sẽ dễ hiểu (understandable), dễ duy trì (maintainable) và linh hoạt hơn (flexible).
Giờ hãy cùng bắt đầu tìm hiểu về SOLID nhé!
1. Single Responsibility
Một Class chỉ nên giữ một trách nhiệm duy nhất. Hơn thế nữa, nó chỉ có một lý do để thay đổi
(a class should only have one responsibility. Furthermore, it should only have one reason to change.)
Hãy kế qua 1 vài lợi ích của nguyên tắc này:
- Testing: Một class chỉ có một trách nhiệm duy nhất thì sẽ có ít test case hơn
- Lower coupling: Ít chức năng hơn trong một class đồng nghĩa với việc sẽ cần ít Dependence hơn
- Organization: Tổ chức của Class nhỏ hơn sẽ dễ dàng hơn trong việc tìm kiếm.
Khi requirement thay đổi, đồng nghĩa với việc những class trong code của chúng ta cũng phải thay đổi theo. Một Class càng giữ nhiều trách nhiệm (responsibility) sẽ càng phải thay đổi nhiều.
Dần dần những việc thay đổi này sẽ càng trở lên khó hơn. Vì việc thay đổi của trách nhiệm này, có thể dẫn tới thay đổi của trách nhiệm khác.
Hãy vào ví dụ cụ thể nào:
class BookManager { def add(book: Book) def remove(id: Long) def update(book: Book) }
Class này vi phạm nguyên tắc môt Class nhưng giữ nhiều trách nhiệm. Vì thế ta phải tách ra 3 class riêng và mỗi class chỉ đảm nhiệm 1 trách nhiệm
Sau khi tái cấu trúc lại ta sẽ được:
class AddingBookManager { def add(book: Book) } class RemovingBookManager { def remove(id: Long) } class UpdatingBookManager { def update(book: Book) }
Tuy số lượng class sẽ nhiều nên nhưng càng về sau các class này sẽ càng phinh to ra. Chia nhỏ ra các class khác nhau sẽ giúp cho việc chỉnh sửa sau này dễ dàng hơn.
2. Open/Closed
Bạn có thể mở rộng class nhưng không được thay đổi nó
(You should be able to extend a class’s behavior, without modifying it)
Nguyên lý này đề cập đến việc khi thêm chức năng thì nên mở rộng class cũ, không nên thay đổi nó. (Ngoại trừ trường hợp sửa lỗi cho class). Vì việc sửa những class cũ có thể gây ra những lỗi (bug) mới.
Giờ mình muốn viết chức năng thêm sách tiếng việt thì mình sẽ tạo ra 1 class mới và kế thừa (extends) lại class đã có:
class AddingVietnameseBookManager extends AddingBookManager { override def add(book: Book) }
3. Liskov substitution
Theo mình đánh giá nội dung của nguyên lý này khá khó hiểu và trừu tượng. Nguyên tắc này được định nghĩa như sau:
Nếu class A là subtype của class B, ta có thể thay B bằng A mà không làm thay đổi tính đúng đắn của chương trình.
if class A is a subtype of class B, then we should be able to replace B with A without disrupting the behavior of our program.
Lấy 1 ví dụ đơn giản cho dễ hiểu nhé:
class Post { def createPost(db: Database, postMessage: String) { db.add(postMessage) } } class TagPost extends Post { override def createPost(db: Database, postMessage: String) { db.addAsTag(postMessage) } } class MentionPost extends Post { override def createPost(db: Database, postMessage: String) { db.addAsMention(db, postMessage) } } class PostHandler { private val database = new Database(); def handleNewPosts() { val newPosts: List[String] = database.getUnhandledPostsMessages(); newPosts.foreach(postMessage => { val post = if (postMessage.head == '#') new TagPost() else if (postMessage.head == '@') new MentionPost() else new Post(); post.createPost(database, postMessage); } }) } }
4. Interface segregation
Nội dung của nguyên lý này thì tương đối dễ. Hiểu một cách đơn giản thì:
Thay vì tạo ra một interface to ta nên chia nhỏ chúng ra thành các interface nhỏ. Để đảm bảo rằng Implementing Classes không phải implement tất cả method mà nó không dùng đến.
Lấy 1 ví dụ nhé. Nếu ta định nghĩa 1 trait (Vì trong Scala không có khái niệm interface) như sau:
trait Animal { def eat() def run() def fly() }
Không phải tất cả động vật cũng đều eat(), run(), fly(). Nhưng ta vẫn phải Implement cả 3 phương thức này.
Thay vào đó ta có thể tách trait này thành những trait nhỏ hơn.
trait Animal { def eat() } trait RunnableAnimal extends Animal { def run() } trait FlyableAnimal extends Animal { def fly() }
5. Dependency inversion
Depend on abstractions, not on concretions.
Nguyên lý này được diễn tả theo 2 ý:
- High-level module không nên phụ thuộc vào low-level module. Cả 2 nên phụ thuộc vào những abstractions (High-level modules should not depend on low-level modules. Both should depend on abstractions.)
- Những Abstractions không nên phụ thuộc vào Details. Details nên phụ thuộc vào Abstractions. (Abstractions should not depend on details. Details should depend on abstractions)
Để tuân theo nguyên tắc này, bạn có thể sử dụng Dependency injection. Nó thường được sử dụng để đơn giản hoá việc “injecting” các dependencies của class thông qua class contructor.
Hãy cùng xem qua ví dụ này:
class BackEndDeveloper { def writeJava() {} } class FrontEndDeveloper { def writeJavascript() {} } class Project { private val backEndDeveloper: BackEndDeveloper = new BackEndDeveloper() private val frontEndDeveloper: FrontEndDeveloper = new FrontEndDeveloper() def implement() { backEndDeveloper.writeJava() frontEndDeveloper.writeJavascript() } }
Việc bạn tạo đối tượng BackEndDeveloper và FrontEndDeveloper bên trong class Project đã vi phạm nguyên tắc này. Vì sao ư? Vì khi bạn thay đổi BackEndDeveloper hoặc FrontEndDeveloper thì bạn sẽ phải sửa Project class.
Giờ hãy cùng sửa lại cho đúng nhé:
trait Developer { def develop() } class BackEndDeveloper extends Developer { override def develop() { writeJava() } private def writeJava() {} } class FrontEndDeveloper extends Developer { override def develop() { writeJavascript() } private def writeJavascript() {} } class Project(developers: List[Developer]) { def implement() { developers.foreach(_.develop()) } }
References:
- https://www.baeldung.com/solid-principles
- https://itnext.io/solid-principles-explanation-and-examples-715b975dcad4
- https://en.wikipedia.org/wiki/SOLID
- https://medium.com/@cramirez92/s-o-l-i-d-the-first-5-priciples-of-object-oriented-design-with-javascript-790f6ac9b9fa
- https://hackernoon.com/solid-principles-made-easy-67b1246bcdf
- https://toidicodedao.com/2015/03/24/solid-la-gi-ap-dung-cac-nguyen-ly-solid-de-tro-thanh-lap-trinh-vien-code-cung/
- https://kipalog.com/posts/Tim-hieu-nhanh-SOLID-than-thanh