Các phần trước:
Chúng tôi đang tiếp tục xem xét các phương pháp để làm cho mã của chúng tôi sạch sẽ, linh hoạt và có thể bảo trì được. Và bây giờ, hãy bắt đầu tìm hiểu các giải pháp kiến trúc sạch sẽ.
Các nguyên tắc SOLID được Robert C. Martin giới thiệu và được nhiều người coi là những phương pháp thực hành tốt nhất trong lập trình hướng đối tượng và thiết kế phần mềm.
Trong bài viết này, chúng ta sẽ xem xét ba nguyên tắc RẮN đầu tiên: Nguyên tắc trách nhiệm duy nhất, Nguyên tắc mở/đóng và Nguyên tắc thay thế Liskov trên các ví dụ thực tế.
Một lớp chỉ nên có một lý do để thay đổi
Nói cách khác, một lớp nên có một trách nhiệm hoặc công việc duy nhất. Điều này quan trọng vì một lớp có một trách nhiệm duy nhất sẽ dễ hiểu, sửa đổi và bảo trì hơn. Những thay đổi trong một vùng mã sẽ không ảnh hưởng đến toàn bộ hệ thống, giảm nguy cơ phát sinh lỗi.
Chúng ta hãy xem một ví dụ thực tế và cách tuân theo nguyên tắc trách nhiệm duy nhất:
// Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }
Trong ví dụ này, lớp của chúng tôi thực hiện các hành động theo các hướng khác nhau: nó thiết lập ngữ cảnh, thay đổi và xác thực nó.
Để tuân theo SRP, chúng ta cần phân chia các trách nhiệm khác nhau này.
// Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }
SRP nhằm mục đích nâng cao tính mô đun mã, giảm thiểu các thách thức bắt nguồn từ sự phụ thuộc lẫn nhau. Bằng cách tổ chức mã thành các hàm, các lớp riêng biệt và thúc đẩy tính mô đun hóa, mã sẽ trở nên dễ sử dụng hơn, tiết kiệm thời gian lẽ ra phải dùng để mã hóa lại chức năng hiện có.
Các thực thể phần mềm (lớp, mô-đun, chức năng) phải mở để mở rộng nhưng đóng để sửa đổi
Bạn sẽ có thể thêm chức năng mới mà không cần thay đổi mã hiện có. Điều này quan trọng vì bằng cách tuân thủ nguyên tắc này, bạn có thể giới thiệu các tính năng hoặc thành phần mới cho hệ thống của mình mà không gây rủi ro cho tính ổn định của mã hiện có.
Nó thúc đẩy khả năng sử dụng lại mã và giảm nhu cầu thay đổi sâu rộng khi mở rộng chức năng.
// Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }
Trong ví dụ này, vấn đề nằm ở lớp HttpRequestCost
, trong phương thức getDeliveryCost
chứa các điều kiện để tính toán các loại sản phẩm khác nhau và chúng tôi sử dụng các phương thức riêng biệt cho từng loại sản phẩm. Vì vậy, nếu cần thêm một loại sản phẩm mới, chúng ta nên sửa đổi lớp HttpRequestCost
và nó không an toàn; chúng ta có thể nhận được kết quả bất ngờ.
Để tránh điều đó, chúng ta nên tạo một request
phương thức trừu tượng trong lớp Product
mà không cần thực hiện. Sự nhận thức cụ thể sẽ kế thừa các lớp: Ananas và Banana. Họ sẽ nhận ra yêu cầu cho chính mình.
HttpRequestCost
sẽ lấy tham số product
theo giao diện lớp Product
và khi chúng ta chuyển các phần phụ thuộc cụ thể vào HttpRequestCost
, nó sẽ tự nhận ra phương thức request
.
// Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }
Tuân theo nguyên tắc này, bạn sẽ giảm việc ghép mã và cứu hệ thống khỏi những hành vi không thể đoán trước.
Các đối tượng của siêu lớp có thể được thay thế bằng các đối tượng của lớp con của nó mà không làm hỏng ứng dụng.
Để hiểu nguyên tắc này, chúng ta hãy xem ví dụ này:
// Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }
Trong ví dụ này, chúng tôi gặp vấn đề với lớp Contractor
. Designer
, Programmer
và Seller
đều là Công nhân và họ được kế thừa từ lớp cha Worker
. Nhưng đồng thời, Nhà thiết kế không được tiếp cận khu vực khép kín vì họ là Nhà thầu chứ không phải Nhân viên. Và chúng tôi đã ghi đè phương thức access
và phá vỡ Nguyên tắc thay thế Liskov.
Nguyên tắc này cho chúng ta biết rằng nếu chúng ta thay thế siêu lớp Worker
bằng lớp con của nó, chẳng hạn như lớp Designer
, thì chức năng sẽ không bị hỏng. Nhưng nếu chúng ta làm như vậy thì chức năng của lớp Programmer
sẽ bị hỏng - phương thức access
sẽ có những hiện thực không mong muốn từ lớp Designer
.
Theo Nguyên tắc thay thế Liskov, chúng ta không nên viết lại các cách thực hiện trong các lớp con mà cần tạo ra các lớp trừu tượng mới trong đó chúng ta xác định các cách thực hiện cụ thể cho từng loại trừu tượng.
Hãy sửa nó:
// Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }
Chúng tôi đã tạo các lớp trừu tượng mới Employee
và Contractor
, đồng thời chuyển phương thức access
sang lớp Employee
và xác định cách thực hiện cụ thể. Nếu chúng ta thay thế lớp Worker
bằng lớp con Contractor
, chức năng Worker
sẽ không bị hỏng.
Nếu bạn tuân thủ LSP, bạn có thể thay thế bất kỳ phiên bản lớp con nào ở bất kỳ nơi nào cần có phiên bản lớp cơ sở và chương trình vẫn hoạt động như dự kiến. Điều này thúc đẩy việc tái sử dụng mã, tính mô-đun và làm cho mã của bạn linh hoạt hơn trước các thay đổi.
Trong bài viết tiếp theo, chúng ta sẽ xem xét các nguyên tắc SOLID Phân chia giao diện và đảo ngược phụ thuộc.