Phát triển phần mềm là một môi trường sẽ liên tục thay đổi, để thích nghi được với điều này thì mỗi sản phẩm cần phải có một kiến trúc thiết kế thật tốt. Với nền móng là kiến trúc tốt thì sản phẩm đó sẽ dễ dàng thay đổi và nâng cấp hơn.
Trong bài viết này cần các bạn có kiến thức cơ bản về Object Oriented Programming (OOP)
SOLID là từ viết tắt kết hợp từ 5 chữ cái đầu tiên của 5 nguyên lý, được đưa ra bởi Bob Martin và Michael Feathers.
SOLID được coi như là một "must have skill" mà bất cứ software developer nào cũng cần phải biết. Trong bài viết này mình sẽ giới thiệu đến các bạn từng nguyên lý và sử dụng Java để demo.
Nguyên lý đầu tiên có nội dung như sau:
Every software component should have one and only one responsibility.
Mỗi software component chỉ nên có 1 và chỉ 1 trách nhiệm duy nhất. Trong phạm vi OOP thì chúng ta có thể hiểu software component là: một class, một method hoặc một module. Để hình dung rõ hơn chúng ta cùng xem hình ảnh sau:
Bây giờ thì chắc bạn đã hình dung rõ hơn về khái niệm SRP
, nhưng lại thấy khó hiểu rằng đáng ra một component mà làm được nhiều việc như kia thì càng tiết kiệm effort, càng đa năng và đáng ra phải tốt hơn chứ. Tuy nhiên một component mà thực hiện quá nhiều công việc thì dẫn component đó sẽ có nhiều lý do để thay đổi.
Việc thay đổi source code trong software development sẽ dẫn đến gia tăng chi phí cũng như tiềm ẩn nhiều rủi ro hoặc bug. Nên khi thiết kế chúng ta luôn cần đảm bảo việc chỉnh sửa cần được thực hiện dễ dàng với phạm vi ảnh hưởng ít nhất có thể. Ví dụ với một class User
được thiết kế không tuân thủ SRP
như sau:
Với thiết kế như trên thì class User sẽ có nhiều lý do dẫn đến cần phải thay đổi:
Trong trường hợp hệ thống muốn dùng HotMail thay vì Gmail. Điều này dẫn đến class User
phải thay đổi để đáp ứng yêu cầu mới. Bây giờ đối chiếu theo nguyên lý SRP
ta chỉnh sửa class User như sau:
Với thiết kế này thì class User
không phải thay đối với lý do [2] bên trên nữa, thay vào đó class EmailService
sẽ bị thay đổi. Tuy nhiên, vẫn phải thay đổi thì thay đổi ở class User hay class EmailService có gì khác nhau đâu.
Câu trả lời là có khác nhau. Nếu chúng ta thiết kế theo cách được mô tả ở Figure 3, thì ngoài class User
cần send mail thì có thể N những class khác như Ticket
, Voucher
, Booking
... cũng cần send mail. Điều này dẫn đến cũng là thay đổi vì lý do [2], nhưng:
Khi mỗi software component chỉ có 1 trách nhiệm thì cũng chỉ có 1 lý do cần thay đổi. Tỉ lệ source phải thay đổi càng ít thì càng dễ thay đổi, tiết kiệm chi phí, giảm thiểu rủi ro.
Nguyên lý này có nội dung như sau:
Software components should be close for modification, but open for extension.
Có 2 ý chính trong nguyên lý này:
Để dễ tưởng tượng thì ví dụ bạn có 1 chiếc laptop, tuy nhiên bạn lại không thích bàn phìm trên chiếc laptop đó. Chúng ta sẽ không thể đem chiếc laptop đó ra sửa và lắp loại bàn phím mới vào được, mà thay vào đó hãy setup một chiếc bàn phím ngoài đúng loại mình thích. Vừa lựa chọn được lại bàn phím ưa thích mà không phải sửa gì chiếc laptop. Cũng tương tự như việc: thay lens máy ảnh, dùng loa ngoài cho laptop...
Giả sử chúng ta có một hệ thống bán hàng với chức năng tích điểm thường. Với mỗi đơn hàng, member sẽ được tích điểm thưởng tương đương 1% tổng giá trị đơn hàng.
Member
.Nghiệp vụ có thay đổi khi member chia làm 2 loại: normal
và platinum
. Với normal member thì nghiệp vụ giữ nguyên, tuy nhiên với platinum member thì sẽ tích điểm thưởng tuơng đương 2% tổng giá trị đơn hàng.
Để đạt yêu cầu của nghiệp vụ mới ta có thể sửa class Member
bằng cách thêm 1 field type
và update method incrementAwardPoint()
như sau:
Member
để đạt được nghiệp vụ mới.Tuy nhiên khi chúng ta sửa method incrementAwardPoint()
, sẽ tiềm ẩn rủi ro phần tính toán cũ có thể bị sai. Cách sửa trên là chúng ta đang vi phạm vào ý "close for modification". Ta có 1 phương án mới theo hướng "open for extension" bằng cách
tạo ra 1 class mới PlatinumMember
và override lại method incrementAwardPoint()
để cung cấp 1 cách thức tính mới cho loại member đó. Điều này sẽ giúp chung ta không hề chỉnh sửa vào việc tính toán của xử lý cũ.
PlatinumMember
để đạt được nghiệp vụ mới.Có thể bạn sẽ thắc mắc là việc chỉnh sửa nhỏ như kia thì có gì rủi ro mà phải phát sinh thêm 1 class mới. Đây chỉ là 1 ví dụ nhỏ để các bạn dễ hình dung hơn về nguyên lý. Còn khi vận dụng thực tế thì chúng ta sẽ cân nhắc phạm vi ảnh hưởng, độ khó để quyết định làm theo solution nào cho hợp lý.
Nguyên lý này có nội dung như sau:
Objects should be replaceable with their subtypes without affecting the correctness of the program.
Các objects của class con có thể thay thế object của class cha mà vẫn đảm bảo tính đúng đắn của chương trình. Nói cách khác là object của class con có thể làm được tất cả những công việc mà object của class cha có thể làm.
Để giải thích rõ hơn chúng ta nói về tính chất kế thừa trong OOP. Tính chất kế thừa thể hiện cho mối quan hệ "is-a", ví dụ:
Với mối quan hệ như trên ta có thể triển khai source code có mối quan hệ kế thừa như sau:
Seagull
, Penguin
kế thừa Bird
Bạn có thể thấy vấn đề ở đây là chim cánh cụt thì đúng là 1 loài chim - thỏa mãn "is-a". Nhưng chim cánh cụt có cánh mà lại không thể bay được, dẫn đến nó không thể implement method fly
của class cha Bird
.
Tồn tại 1 unimplemented method thì không phải là cách thiết kế tốt.
Nguyên nhân của việc này là do ban đầu người thiết kế cố gắng thực hiện tính kế thừa tuy nhiên lại định nghĩa sai hành vi tổng quát mà trong đó có những class con không thể thực hiện được => Vi phạm nguyên tắc vì "Các objects của class con KHÔNG THỂ thay thế object của class cha".
Chúng ta có thể giải quyết vấn đề này như cách bên dưới. Tách hành động fly
ra một interface riêng, và chỉ những loài chim nào có thể bay thì implementation
fly
ra một interface riêng, và chỉ những loài chim nào có thể bay thì implementationNguyên lý này có nội dung như sau:
No client should be forced to depend on methods it does not use.
Nguyên lý này khá dễ hiểu, nó phát biểu rằng những implementation của interface không nên bị phụ thuộc vào những method mà nó không dùng đến. Điều này có nghĩa là chúng ta nên phân chia các method vào những interface hợp lý, không nên nhóm nhiều method vào một FAT interface.
Nếu bạn dùng iphone thì liệu Apple có nên tặng kèm 1 một loại dây sạc như này cho bạn không. Tất nhiên là không rồi vì nó dư thừa quá nhiều chức năng không cần thiết.
Ví dụ ta có 1 interface IExportable
định nghĩa 2 method hỗ trợ export csv hoặc pdf.
Tuy nhiên với thông tin sao kê tín dụng CreditStatement
thì không hỗ trợ export csv, dẫn đến method exportCsv()
sẽ throw exception. Cách thiết kế này đã vi phạm nguyên lý vì CreditStatement
đang phải implement một method không liên quan.
CreditStatement
phải implement method không liên quan.Chúng ta có thể sửa bằng cách tách 2 interface như sau. Các class sẽ chọn implement những interface chưa các method mà nó cần thiết.
Mục tiêu của nguyên tắc này giúp tránh được những implement dư thừa. Tuy nhiên cần phải nhớ rằng chúng ta tách interface một cách hợp lý chứ không phải tách nhỏ. Vì nếu tách quá nhỏ thì số lượng interface gia tăng dẫn đến khó để kiểm soát ứng dụng.
Nguyên lý này có nội dung như sau:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
- 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.
- Abstractions không nên phụ thuộc vào chi tiết, mà ngược lại.
Với nguyên lý này chúng ta cần biết khái niệm như nào là module cấp cao
và module cấp thấp
.
Khi module A gọi module B, lúc này module A phụ thuộc (depend) vào module B. Thì module A là module cấp cao
và module B là module cấp thấp
.
Ví dụ với chức năng đăng ký tài khoản, sau khi đăng ký thành công chúng ta sẽ cần gửi email kick hoạt tài khoản. Trường hợp này thì RegisterController
sẽ là module cấp cao và GmailService
là module cấp thấp.
Việc module cấp cao gọi trực tiếp module cấp thấp như Figure 15 sẽ dẫn đến RegisterController
bị phụ thuộc vào GmailService
. Trong trường hợp ứng dụng thay đổi sử dụng dịch vụ Hot mail thay vì dùng Gmail thì lúc này RegisterController
cần thay đổi gọi sang HotMailService
.
Để giải quyết việc phụ thuộc này chúng ta sẽ thiết kế như sau:
RegisterController
sẽ liên kết trực tiếp đến interface EmailService
Khi RegisterController
sẽ liên kết trực tiếp đến interface EmailService
, điều này sẽ giúp cho RegisterController
sẽ có thể linh động sử dụng bất kì service email nào mà bản thân nó không phải thay đổi.
RegisterController
sẽ không bị phụ thuộc vào 1 service mail cụ thể mà có thể linh động sử dụngTa có thể triển khai source code bằng cách RegisterController
sẽ được inject EmailService thay vì trực tiếp GmailService
:
RegisterController
inject EmailService thay vì trực tiếp GmailServiceTrong quá trình execute RegisterController
có thể switch email service khác nhau mà bản thân nó không phải chỉnh sửa gì.
RegisterController
không cần chỉnh sửa khi đổi từ GmailService
sang dùng HotMailService
Cách triển khai code bên trên đang sử dụng Dependency Injection, một trong những cách implement Dependency Inversion Principle.
Gửi lời khen ngợi đến các bạn đã kiên trì cùng mình hoàn thành nội dung liên quan đến 5 nguyên lý trong SOLID. Hi vọng sau bài viết này các bạn sẽ hiểu cơ bản về SOLID và áp dụng vào cách thiết kế để giúp source code của mình dễ dàng mở rộng hơn.
Tuy nhiên cũng cần phải lưu ý rằng, bất cứ nguyên lý nào cũng có 2 mặt lợi và hại. Nếu cứng nhắc áp dụng thì đôi khi source code sẽ bị dài dòng, phức tạp và khó quản lý hơn. Nên bạn cần hiểu và áp dụng SOLID một cách hợp lý chứ không phải cứng nhắc.