+3

Nguyên tắc thiết kế SOLID là gì?

Giới thiệu về "Uncle Bob"

Robert C. Martin, thường được biết đến với biệt danh "Uncle Bob", là một trong những nhân vật có tầm ảnh hưởng lớn trong lĩnh vực kỹ thuật phần mềm. Với hơn 50 năm kinh nghiệm, ông đã trở thành biểu tượng cho việc thúc đẩy Clean code và là người tiên phong trong việc thúc đẩy các nguyên tắc giúp xây dựng phần mềm chất lượng, dễ bảo trì.

Ông nổi tiếng nhất với khái niệm "Clean Code", một triết lý về cách viết mã nguồn dễ đọc, dễ hiểu và dễ bảo trì. Theo Uncle Bob, mã nguồn không chỉ cần hoạt động tốt mà còn phải rõ ràng, có cấu trúc hợp lý để các lập trình viên khác có thể tiếp tục phát triển mà không gặp khó khăn. Uncle Bob là tác giả của nhiều cuốn sách bán chạy về kỹ thuật phần mềm, trong đó nổi bật nhất là:

  1. Clean Code: A Handbook of Agile Software Craftsmanship
  2. Clean Architecture: A Craftsman's Guide to Software Structure and Design
  3. The Clean Coder: A Code of Conduct for Professional Programmers

Các tác phẩm này đã trở thành cẩm nang cho nhiều thế hệ lập trình viên, cung cấp những nguyên tắc nền tảng giúp tạo nên phần mềm chất lượng cao, dễ mở rộng và bảo trì. Những cuốn sách này không chỉ tập trung vào khía cạnh kỹ thuật mà còn nhấn mạnh tầm quan trọng của đạo đức nghề nghiệp, kỷ luật và trách nhiệm trong lập trình. Với tầm nhìn xa và những đóng góp của mình, Robert C. Martin đã truyền cảm hứng cho hàng triệu lập trình viên trên toàn thế giới và đóng góp không nhỏ vào việc nâng cao tiêu chuẩn trong phát triển phần mềm hiện đại.

Nguyên tắc thiết kế SOLID là gì?

SOLID là một tập hợp năm nguyên tắc thiết kế phần mềm quan trọng trong lập trình hướng đối tượng (OOP), giúp xây dựng hệ thống dễ bảo trì, mở rộng và tránh lỗi. Dưới đây là định nghĩa ngắn gọn cho từng nguyên tắc:

  • S - Single Responsibility Principle (SRP): Mỗi lớp chỉ nên có một trách nhiệm duy nhất, và lý do để thay đổi lớp đó phải liên quan đến trách nhiệm này.
  • O - Open-Closed Principle (OCP): Các lớp nên được mở rộng nhưng không được phép sửa đổi. Điều này có nghĩa là có thể mở rộng chức năng của lớp mà không cần thay đổi mã hiện tại.
  • L - Liskov Substitution Principle (LSP): Các đối tượng của lớp con có thể thay thế cho đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình.
  • I - Interface Segregation Principle (ISP): Không nên ép buộc các lớp phụ thuộc vào những giao diện mà chúng không sử dụng; các giao diện nên nhỏ và cụ thể thay vì lớn và đa năng.
  • D - Dependency Inversion Principle (DIP): Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp, mà cả hai nên phụ thuộc vào các trừu tượng (interface hoặc abstract class).

Single Responsibility Principle (SRP)

"A class should have only one reason to change." -- Robert C. Martin

class User {
    name: string;
    email: string;
    constructor(name: string, email: string) {
        this.name = name;
        this.email = email;
    }
}

class UserAuthentication {
    user: User;
    constructor(user: User) {
        this.user = user;
    }
    
    authenticate(password: string): boolean {
        // Implement authentication logic here.
    }
}

Trong ví dụ trên, chúng ta đã tách lớp User thành hai phần riêng biệt: User class sẽ chỉ quản lý thông tin người dùng, còn Authentication class sẽ chịu trách nhiệm xử lý việc xác thực thông tin người dùng. Bằng cách này, mỗi class chỉ có một lý do duy nhất để thay đổi: User class thay đổi khi có thay đổi về thông tin người dùng, và Authentication class thay đổi khi có thay đổi liên quan đến xác thực. Đây chính là việc áp dụng nguyên tắc SRP (Single Responsibility Principle).

Ưu điểm khi sử dụng SRP:

  1. Dễ dàng bảo trì
  2. Dễ đọc, dễ hiểu
  3. Dễ dàng trong việc kiểm thử
  4. Giảm sự phụ thuộc
  5. Tăng việc tái sử dụng code

Open-Closed Principle (OCP)

"The Open-Closed Principle states that "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." -- Robert C. Martin

interface Customer {
    giveDiscount(): number;
}

class RegularCustomer implements Customer {
    giveDiscount(): number {
        return 5;
    }
}

class PremiumCustomer implements Customer {
    giveDiscount(): number {
        return 10;
    }
}

class Discount {
  giveDiscount(customer: Customer): number {
    return customer.giveDiscount();
  }
}

Trong ví dụ trên, chúng ta đã định nghĩa Customer interface với 1 method giveDiscount. 2 class RegularCustomerPremiumCustomer implement Customer interface. class Discount đã được đóng lại và nó không cần phải sửa đổi mỗi khi chúng ta có thêm loại khách hàng mới. Đây là việc áp dụng nguyên tác OCP

Ưu điểm khi sử dụng OCP:

  1. Giảm nguy cơ gặp lỗi
  2. Tăng khả năng tái sử dụng code

Liskov Substitution Principle (LSP)

"If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program."

abstract class Shape {
  abstract calculateArea(): number;
}

class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super();
  }

  public calculateArea(): number {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(public side: number) {
    super();
  }

  public calculateArea(): number {
    return this.side * this.side;
  }
}
// ====== Client Code
function area(shape: Shape) {
  return shape.calculateArea();
}

let rectangle = new Rectangle(4, 8);
let square = new Square(5);

area(rectangle); // 24
area(square); // 25

Trong ví dụ này, SquareRectangle đều là các lớp con của Shape. Mỗi lớp triển khai phương thức calculateArea theo các đặc tính hình học riêng của chúng. Hàm area được thiết kế để hoạt động với bất kỳ đối tượng nào thuộc loại Shape. Nó sử dụng phương thức calculateArea và các setter cụ thể để thay đổi kích thước của shape.

Ưu điểm:

  1. Giảm sự trùng lặp
  2. Tăng cường tính linh hoạt
  3. Giảm chi phí bảo trì
  4. Tăng tính module

Interface Segregation Principle (ISP)

"No client should be forced to depend on interfaces they do not use." -- Robert C. Martin

interface PostCreator {
  createPost(post: Post): void;
}

interface CommentCreator {
  commentOnPost(comment: Comment): void;
}

interface PostSharer {
  sharePost(post: Post): void;
}
// ------------------ Implement ------------------
class Admin implements PostCreator, CommentCreator, PostSharer {
  createPost(post: Post): void {
    // Actual implementation
  }

  commentOnPost(comment: Comment): void {
    // Actual implementation
  }

  sharePost(post: Post): void {
    // Actual implementation
  }
}

class RegularUser implements CommentCreator, PostSharer {
  commentOnPost(comment: Comment): void {
    // Actual implementation
  }

  sharePost(post: Post): void {
    // Actual implementation
  }
}

Mỗi client sẽ chỉ phụ thuộc vào interface mà chúng sử dụng. Ví dụ admin có thể truy cập được hết interface trong khi RegularUser chỉ có thể comment và share bài post.

Ưu điểm:

  1. Cải thiện khả năng bảo trì
  2. Giảm thiểu tác động khi thay đổi
  3. Tăng tính đóng gói
  4. Kiểm thử dễ dàng

Dependency Inversion Principle (DIP)

"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." -- Robert C. Martin

Với nguyên tắc này chúng ta cần tách nhỏ chúng ra thành 2 phần để hiểu rõ hơn:

  • 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.
// không sử dụng DIP
class MySQLDatabase {
  save(data: string): void {
    // logic to save data to a MySQL database
  }
}

class HighLevelModule {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase();
  }

  execute(data: string): void {
    // high-level logic
    this.database.save(data);
  }
}

Với ví dụ này, HighLevelModule là hight-level module và nó phụ thuộc vào low-level module MySQLDatabase. Điều đó có nghĩa nếu chúng ta quyết định thay đổi db từ MySQL sang MongoDB, chúng ta sẽ phải sửa HighLevelModule. Điều này thực sự không tốt.

Ví dụ sau, chúng ta sẽ sửa lại một chút bằng cách cho HighLevelModule phụ thuộc vào IDatabse, nó chỉ sử dụng lại IDatabase với method save mà không cần quan tâm bên dưới đó là MySQL hay MongoDB. THiết kế này sẽ cho chúng ta thay đổi database mà không phải sửa đổi HighLevelModule.

// Sử dụng DIP
interface IDatabase {
  save(data: string): void;
}

class MySQLDatabase implements IDatabase {
  save(data: string): void {
    // logic to save data to a MySQL database
  }
}

class MongoDBDatabase implements IDatabase {
  save(data: string): void {
    // logic to save data to a MongoDB database
  }
}

class HighLevelModule {
  private database: IDatabase;

  constructor(database: IDatabase) {
    this.database = database;
  }

  execute(data: string): void {
    // high-level logic
    this.database.save(data);
  }
}

Ưu điểm:

  1. Tách biệt được code
  2. Dễ dàng sửa đổi và mở rộng
  3. Dễ dàng kiểm thử
  4. Dễ dàng tái sử dụng code, đọc hiểu code

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí