SOLID là 5 nguyên tắc đầu tiên và cơ bản mà bất cứ programmer nào cũng cần phải hiểu rõ:
- S – SRP – Single Responsibility Principle - a class should have only a single responsibility (i.e. only one potential change in the software's specification should be able to affect the specification of the class)
- O – OCP – Open/Closed Principle - software entities … should be open for extension, but closed for modification.
- L – LSP – Liskov Substitution Principle - objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program
- I – ISP – Interface Segregation Principle - many client-specific interfaces are better than one general-purpose interface
- D – DIP – Dependency Inversion Principle - one should “Depend upon Abstractions. Do not depend upon concretions
Những nguyên tắc này tuy rất khó nhớ tên nhưng ít nhiều ta đều tiếp xúc trong công việc hàng ngày. Vì vậy nhận ra khi nào ta đang follow nguyên tắc gì sẽ rất có lợi cho công việc.
Ý nghĩa của từng nguyên tắc như sau:
SRP
– a piece of software chỉ nên thực hiện 1 mục đích, 1 trách nhiệm duy nhất. Về cơ bản, các đơn vị cấu trúc của program gồm có:- Statement
- Code block
- Method/function
- Class/Interface
- Module
- Project/Library
- Solution
Đối với mỗi level, chúng ta cần phải rất rõ ràng về công việc mà chúng thực hiện. Điều này giúp cho code dễ đọc, program dễ hiểu và dễ maintain. Sau khi viết code xong, khi đọc lại hoặc khi ng khác đọc sẽ dễ dàng follow đc vấn đề.Ví dụ 1:1Console.WriteLine(
"You are at the index "
+ i++);
Dòng code trên làm 2 việc: in ra index hiện tại và tăng nó lên 1. Đối với ng viết, do đã là thói quen nên họ dễ dàng nhận ra giá trị của biến i sau dòng này. Nhưng đối với ng khác, rất dễ xảy ra nhầm lẫn.Ví dụ 2:1234567public
void
Insert(Entity obj) {
using
(
var
ctx =
new
MyContext()) {
ctx.Entities.InsertOnSubmit(obj);
ctx.SubmitChanges();
_messageService.TellSomebody(
"new object inserted."
);
}
}
Đoạn code trên có 2 vấn đề:- Trong using block, thực hiện 2 việc là insert object và gọi messageService. Mặc dù code chạy bình thường nhưng đây vẫn là bad practice, messageService cần phải nằm ngoài using ctx.
- Trong function Insert thực hiện 2 nhiệm vụ khác nhau. Nếu trong tương lai phát sinh ra nhu cầu Insert nhưng ko kèm notify cho messageService thì function này sẽ phải sửa lại (cùng với tất cả những nơi đã dùng tới nó)
Cứ tiếp tục như thế, đôi khi chúng ta cảm thấy sẽ “tiện lợi” ghi gộp nhiều thứ lại với nhau. Nhưng thực tế sẽ tạo ra những vấn đề tiềm ẩn cho sau này (technical debt)OCP
open for extension but close for modification? tức là sao?Nôm na là ta phải thiết kế software sao cho nó có thể mở rộng dễ dàng mà ko cần phải đập đi làm lại. Còn gì bực bội hơn khi được giao viết thêm functionality cho 1 hệ thống mà phát hiện ra cần phải sửa rất nhiều mới support đc chức năng ABC? Điều này cũng giống như thiết kế 1 chiếc xe hơi với các bộ phận tiêu chuẩn có thể dễ dàng thay thế hoặc nâng cấp mà ko cần phải chế tạo lại cả chiếc xe.Nguyên tắc này thường đc triển khai dựa trên tính kế thừa và tính đa hình của OOP như trong sơ đồ sau:Ở đây tôi đã define Superman và Spiderman, và method Fight của 2 entity này đã fixed và không thay đổi được, tức là Closed. Vậy hệ thống có Open hay không? Làm gì để tùy chỉnh?- có thể extend và override method của class cũ, chẳng hạn tạo class AmazingSpiderman -> Spiderman với khả năng Fight more amazing
- extend từ Abstract class và implement abstract method Fight
Như vậy hệ thống đảm bảo đc tính OCP.LSP
có khi nào bạn thấy ko hài lòng về 1 property nào đó của 1 class và viết 1 class con để modify property đó? chẳng hạn property của base class chỉ trả về integer từ 1..10, bạn subclass để nó trả về 1..100? Như vậy là bạn đang violate nguyên tắc LSP.Lý do: khi đoạn code nào đó sử dụng đến property này với understanding là nó sẽ trả về 1..10, sẽ bị surprised khi nó trả về 1..100 và ko thể handle từ 11..100
Ví dụ sau ko hoàn toàn về LSP nhưng cũng cho thấy sự vi phạm tính đa hình của class:123ISuperHero hero = HeroFactory.GetSuperHero(
"IronMan"
);
hero.Fight();
(hero
as
IronMan).FireBeam();
Đoạn code trên đã phải convert đối tượng về một subtype để thực hiện method cần thiết.ISP
khi bạn implement 1 interface mà có những method bạn ko dùng đến, đó là dấu hiệu của problem.1234567891011interface
IPlayer {
void
Run();
void
Shoot();
void
Catch();
}
class
Striker : IPlayer {
public
void
Run(){...}
public
void
Shoot(){...}
public
void
Catch(){...}
}
Có thể thấy Striker sẽ ko implement đc method Catch. Và nếu ta gọi Player.Catch() có thể sẽ dẫn đến runtime expception. Vì vậy việc class Striker ko thể implement đc method Catch cho thấy 1 technical debt.1234567891011121314151617181920interface
IPlayer {
void
Run();
}
interface
IAttacking : IPlayer {
void
Shoot();
}
interface
IGoalkeeping {
void
Catch();
}
class
Striker : IAttacking {
public
void
Run(){...}
public
void
Shoot(){...}
}
class
Goalkeeper : IGoalkeeping {
public
void
Run(){...}
public
void
Catch();
}
Như vậy, rõ ràng mọi method trong class đều có ý nghĩa rất rõ ràng, ko lỗi.DIP
– hmm… đây là 1 nguyên tắc về decoupled, thuộc về design nhiều hơn là coding, vì vậy tương đối khó để nắm bắt. Có thể liên tưởng qua ví dụ thực tế như sau:
Ex: giả sử A làm ăn với B, A mua dịch vụ do B cung cấp. Đôi khi B ko thực hiện đúng trách nhiệm của mình, A mặc dù bực bội về việc đó, nhưng đã ký kết làm ăn nên ko thể đổi được, và cũng ko có đủ khả năng tìm đối tác khác. Như vậy A đã phụ thuộc chặt chẽ vào B.
Tuy nhiên, nếu ban đầu A ko trực tiếp làm việc với B, mà thông qua trung gian X. A đưa ra yêu cầu dành cho nhà phân phối của mình và X sẽ tìm người đáp ứng được nhu cầu đó, giả sử ban đầu là B. Nếu sau đó B ko thực hiện đúng trách nhiệm, X sẽ thay B bằng nhà cung cấp C. Như vậy việc sử dụng dịch vụ của A sẽ ko bị gián đoạn và A cũng ko cần làm việc trực tiếp với B hay C.Điều này minh họa trong code như sau:1234567class
Messi {
public
void
BringTheBallForward() {
var
supplier =
new
Xavi();
messi.Ball = supplier.PassTheBall();
messi.Dribble();
}
}
Ta có thể thấy Messi phụ thuộc hoàn toàn vào Xavi. Có thể sửa lại như sau:1234567class
Messi {
public
IMidfielder Midfielder {}
public
void
BringTheBallForward() {
messi.Ball = Midfielder.PassTheBall();
messi.Dribble();
}
}
Như vậy program có thể điều chỉnh và inject Midfielder vào, giảm sự lệ thuộc giữa các class với nhau.
Tóm lại, các principle trên có tác dụng chính là giúp chúng ta nắm được và dự đoán được các vấn đề tiềm ẩn của program thông qua các dấu hiệu vi phạm 5 principle này để giải quyết chúng 1 cách triệt để.
Không có nhận xét nào:
Đăng nhận xét