Khi viết code, điều quan trọng là đảm bảo mã của bạn dễ đọc và dễ hiểu. Điều này đặc biệt quan trọng khi làm việc trên các dự án lớn với nhiều phần khác nhau. Một cách để đảm bảo code của bạn dễ hiểu là tuân thủ các nguyên tắc SOLID.

SOLID viết tắt của năm nguyên tắc khác nhau:

  • Nguyên tắc Single Responsibility (SRP)

  • Nguyên tắc Open-Closed (OCP)

  • Nguyên tắc Liskov Substitution (LSP)

  • Nguyên tắc Interface Segregation (ISP)

  • Nguyên tắc Dependency Inversion (DIP)

Hãy xem xét từng nguyên tắc này một cách cụ thể và xem làm thế nào chúng có thể giúp cải thiện code của bạn hay không nhé.

Giải thích

Nguyên tắc Single Responsibility (SRP)

Nguyên tắc Single Responsibility (SRP) nói rằng mỗi phần code của bạn chỉ nên có một nhiệm vụ duy nhất. Điều này có nghĩa là nếu bạn có một đoạn code thực hiện nhiều công việc, bạn nên chia nó thành các phần nhỏ hơn, mỗi phần với nhiệm vụ riêng của nó.

Ví dụ, hãy tưởng tượng bạn có một đoạn code lưu số lần nhấp vào một nút và cũng hiển thị số lần nhấp trên màn hình. Theo SRP, mã này nên được chia thành hai phần riêng biệt: một phần là lưu số lần nhấp và một phần hiển thị số lần nhấp trên màn hình.

// Incorrect way of doing it

let clickCount = 0;

function handleButtonClick() {

    clickCount += 1;

    document.getElementById("click-count").innerHTML =    clickCount;

}

// Correct way of doing it

let clickCount = 0;

function handleButtonClick() {

    clickCount += 1;

}

function updateClickCount() {

    document.getElementById("click-count").innerHTML = clickCount;

}

 

Nguyên tắc Open-Closed (OCP)

Nguyên tắc Open-Closed (OCP) nói rằng mã của bạn nên mở rộng được nhưng cần đóng gói lại để sửa đổi. Điều này có nghĩa là bạn nên có thể thêm tính năng mới vào code của bạn mà không cần thay đổi mã hiện có.

Ví dụ, hãy tưởng tượng bạn có một chương trình có thể cộng hai số lại với nhau. Theo OCP, bạn nên có thể thêm một tính năng mới (như trừ hai số) mà không cần thay đổi mã hiện có.

// Incorrect way of doing it

function add(a, b) {

    return a + b;

}

// It is not a good idea to change the `add` function to a `subtract` function.

function subtract(a, b) {

    return a - b;

}

 

// Correct way of doing it

class Calculator {

    static add(a, b) {

        return a + b;

    }

    static subtract(a, b) {

        return a - b;

    }

}

 

Nguyên tắc Liskov Substitution (LSP)

Nguyên tắc Liskov Substitution (LSP) nói rằng bạn nên sử dụng một lớp con ở bất kỳ nơi nào thay vì bạn sử dụng lớp cha. Điều này có nghĩa là lớp con nên là một phiên bản "tốt hơn" của lớp cha và không nên làm hỏng bất kỳ code nào sử dụng lớp cha.

Ví dụ, hãy tưởng tượng bạn có một lớp cha gọi là "Animal" và một lớp con gọi là "Dogs". Theo LSP, bạn nên có thể sử dụng một đối tượng "Dog" ở bất kì nơi nào mà bạn sử dụng đối tượng "Animal" và code của bạn vẫn hoạt động đúng.

class Animals {

    constructor(name) {

        this.name = name;

    }

    speak() {

        return "Animals make noise";

    }

}

 

class Dogs extends Animals {

    speak() {

        return "Woof";

    }

}

 

const animal = new Animals("Animals");

console.log(animal.speak()); // prints "Animals make noise"

 

const dog = new Dogs("Dog");

console.log(dog.speak()); // prints "Woof"

console.log(dog instanceof Animals); // prints "true"

 

Nguyên tắc Interface Segregation (ISP)

Nguyên tắc Interface Segregation (ISP) nói rằng bạn không nên ép buộc khách hàng phải triển khai các interface mà họ không sử dụng. Điều này có nghĩa là bạn nên tạo ra các interface nhỏ hơn cho các nhóm chức năng liên quan.

Ví dụ, hãy tưởng tượng bạn có một interface gọi là "Automobile" có các chức năng “drive" và “flying”. Theo ISP, nếu bạn có một interface Drivable chỉ có “drive", nó không nên bị ép buộc triển khai các chức năng “flying” từ interface "Automobile". Tương tự với interface Flyable 

// Incorrect way of doing it

interface Automobile {

    drive(): void;

    fly(): void;

}

 

class Car implements Automobile {

    drive(): void {

        // code for driving

    }

 

    fly(): void {

        // code for flying (not applicable for cars)

    }

}

// Correct way of doing it

interface Drivable {

    drive(): void;

}

 

interface Flyable {

    fly(): void;

}

 

class Car implements Drivable {

    drive(): void {

        // code for driving

    }

}

 

Nguyên tắc Dependency Inversion (DIP)

Nguyên tắc Dependency Inversion (DIP) nói rằng các module cấp cao không nên phụ thuộc vào các module cấp thấp, mà cả hai nên phụ thuộc vào các trừu tượng (abstract). Điều này có nghĩa là code của bạn không nên phụ thuộc vào các Class hoặc Function cụ thể, mà thay vào đó là các khái niệm trừu tượng.

Ví dụ, hãy tưởng tượng bạn có một class gọi là "Car" phụ thuộc vào một class gọi là "Engine". Theo DIP, class "Car" không nên phụ thuộc vào lớp "Engine" cụ thể, mà nên phụ thuộc vào một abstract Engine.
 

// Incorrect way of doing it

class Engine {

    start(): void {

        // code for starting the engine

    }

}

 

class Car {

    private engine: Engine;

    constructor() {

        this.engine = new Engine();

    }

    start(): void {

        this.engine.start();

    }

}

 

// Correct way of doing it

interface Engine {

    start(): void;

}

 

class RealEngine implements Engine {

    start(): void {

        // code for starting the engine

    }

}

 

class Car {

    private engine: Engine;

    constructor(engine: Engine) {

        this.engine = engine;

    }

    start(): void {

        this.engine.start();

    }

}

const car = new Car(new RealEngine());

 

Use Case

Quy trình thanh toán trên trang web thương mại điện tử - Payment

Giả sử chúng ta có một trang web thương mại điện tử nơi khách hàng có thể thêm các mặt hàng vào giỏ hàng và sau đó tiếp tục đến trang thanh toán. Quy trình thanh toán bao gồm việc tính toán tổng chi phí của các mặt hàng, áp dụng bất kỳ giảm giá hoặc khuyến mãi nào, sau đó tiến hành xử lý thanh toán.

Theo Nguyên tắc SRP, chúng ta nên chia quy trình thanh toán thành các class khác nhau, mỗi class có trách nhiệm cụ thể của riêng mình. Ví dụ, chúng ta có thể có một class Giỏ hàng (Cart) để theo dõi các mặt hàng trong giỏ hàng, một class Giảm giá (Discounts) để áp dụng bất kỳ giảm giá hoặc khuyến mãi nào, và một class Thanh toán (Payment) để xử lý việc thanh toán.

class Cart {

    items = [];

    addItem(item) {

        this.items.push(item);

    }

    getTotal() {

        return this.items.reduce((total, item) => total + item.price, 0);

    }

}

 

class Discounts {

    applyDiscount(total) {

        return total * 0.9; // 10% off

    }

}

 

class Payment {

    processPayment(total) {

        // code for processing the payment

    }

}

 

class Checkout {

    cart;

    discounts;

    payment;

 

    constructor(cart, discounts, payment) {

        this.cart = cart;

        this.discounts = discounts;

        this.payment = payment;

    }

 

    processCheckout() {

        const total = this.discounts.applyDiscount(this.cart.getTotal());

        this.payment.processPayment(total);

    }

}

 

const cart = new Cart();

cart.addItem({ name: "item1", price: 20 });

cart.addItem({ name: "item2", price: 30 });

 

const checkout = new Checkout(cart, new Discounts(), new Payment());

checkout.processCheckout();

Trong ví dụ này, mỗi class có một trách nhiệm duy nhất: class Giỏ hàng (Cart) theo dõi các mặt hàng trong giỏ hàng, class Giảm giá (Discounts) áp dụng các khuyến mãi, class Xử lý thanh toán (Payment) xử lý việc thanh toán, và class Thanh toán (Checkout) điều phối quy trình. Điều này làm cho code dễ bảo trì và dễ hiểu hơn.

 

Ứng dụng thời tiết - Weather App

Giả sử chúng ta có một ứng dụng thời tiết hiển thị nhiệt độ hiện tại, độ ẩm và áp suất cho một địa điểm cụ thể. Chúng ta muốn thêm một tính năng mới hiển thị tốc độ gió và hướng gió.

Theo Nguyên tắc OCP, chúng ta nên có thể thêm tính năng mới này mà không cần sửa đổi code hiện tại.

class WeatherData {

  constructor(temperature, humidity, pressure) {

    this.temperature = temperature;

    this.humidity = humidity;

    this.pressure = pressure;

  }

}

 

class WeatherDisplay {

  display(weatherData) {

    console.log(`Temperature: ${weatherData.temperature}`);

    console.log(`Humidity: ${weatherData.humidity}`);

    console.log(`Pressure: ${weatherData.pressure}`);

  }

}

 

class WindDisplay {

  display(weatherData) {

    console.log(`Wind speed: ${weatherData.windSpeed}`);

 

    console.log(`Wind direction: ${(weatherData, windDirection)}`);

  }

}

 

class WeatherApp {

  weatherData;

  weatherDisplay;

  windDisplay;

  constructor(weatherData) {

    this.weatherData = weatherData;

    this.weatherDisplay = new WeatherDisplay();

    this.windDisplay = new WindDisplay();

  }

 

  displayWeather() {

    this.weatherDisplay.display(this.weatherData);

    this.windDisplay.display(this.weatherData);

  }

}

 

const weatherData = new WeatherData(72, 50, 1013);

weatherData.windSpeed = 5;

weatherData.windDirection = "NW";

const weatherApp = new WeatherApp(weatherData);

weatherApp.displayWeather();

Trong ví dụ này, class WeatherApp có thể mở rộng bằng cách thêm class WindDisplay mới mà không cần sửa đổi class WeatherDisplay hiện tại. Điều này cho phép chúng ta thêm tính năng mới vào ứng dụng mà không ảnh hưởng đến mã nguồn hiện tại.

Nhân vật Game - Game Character

Giả sử chúng ta có một trò chơi với các loại nhân vật khác nhau, mỗi loại có khả năng độc đáo của riêng nó. Chúng ta muốn thêm một loại nhân vật mới mà không làm hỏng cơ chế trò chơi hiện tại.

Theo Nguyên tắc LSP, chúng ta nên sử dụng class nhân vật mới ở bất kỳ đâu chúng ta sử dụng lớp nhân vật cha, và trò chơi vẫn hoạt động đúng.

class Character {

    move() {

        console.log("Character moved");

    }

}

 

class Warrior extends Character {

    attack() {

        console.log("Warrior attacked");

    }

}

 

class Mage extends Character {

    castSpell() {

        console.log("Mage cast a spell");

    }

}

 

class Paladin extends Warrior {

    heal() {

        console.log("Paladin healed");

    }

}

 

const characters = [new Warrior(), new Mage(), new Paladin()];

for (let character of characters) {

    character.move();

    if (character instanceof Warrior) {

        character.attack();

    }

    if (character instanceof Mage) {

        character.castSpell();

    }

    if (character instanceof Paladin) {

        character.heal();

    }

}

Trong ví dụ này, class Paladin là một class con của lớp Warrior và nó có khả năng điều trị độc (heal) của riêng mình, nhưng vẫn đúng cách thực hiện phương thức move từ class cha, do đó nó có thể được sử dụng ở bất kỳ đâu mà đối tượng Character được sử dụng. Điều này cho phép chúng ta thêm các loại nhân vật mới mà không làm hỏng cơ chế trò chơi hiện tại.

Ứng dụng trò chuyện - Chat App

Giả sử chúng ta có một ứng dụng trò chuyện cho phép người dùng gửi tin nhắn và tệp tin. Chúng ta muốn tách riêng chức năng gửi tin nhắn và gửi tệp tin để người dùng chỉ cần triển khai chức năng mà họ cần mà không cần triển khai chức năng khác.

Theo Nguyên tắc ISP, chúng ta nên tạo hai interface riêng biệt, một cho việc gửi tin nhắn và một cho việc gửi tệp tin.

interface MessageSender {

  sendMessage(message: string): void;

}

interface FileSender {

  sendFile(file: File): void;

}

class ChatClient implements MessageSender {

  sendMessage(message: string): void {

    // code for sending a message

  }

}

class FileTransferClient implements FileSender {

  sendFile(file: File): void {

    // code for sending a file

  }

}

class AdvancedChatClient implements MessageSender, FileSender {

  sendMessage(message: string): void {

    // code for sending a message

  }

  sendFile(file: File): void {

    // code for sending a file

  }

}

 

const chatClient = new ChatClient();

chatClient.sendMessage("Hello!");

 

const fileTransferClient = new FileTransferClient();

fileTransferClient.sendFile(new File("file.txt"));

 

const advancedChatClient = new AdvancedChatClient();

advancedChatClient.sendMessage("Hello!");

advancedChatClient.sendFile(new File("file.txt"));

Trong ví dụ này, class ChatClient chỉ triển khai interface MessageSender và không cần triển khai interface FileSender, và class FileTransferClient chỉ triển khai interface FileSender và không cần triển khai interface MessageSender. Điều này cho phép các khách hàng chỉ triển khai chức năng mà họ cần, trong khi vẫn giữ mã nguồn rõ ràng và dễ hiểu.

Nền tảng mạng xã hội - Social media platform

Giả sử chúng ta có một nền tảng mạng xã hội cho phép người dùng đăng bài cập nhật bằng văn bản và hình ảnh. Chúng ta muốn thêm một tính năng mới cho phép người dùng đăng bài cập nhật video mà không thay đổi code hiện tại.

Theo Nguyên tắc DIP, chúng ta nên đảm bảo rằng code xử lý cập nhật không phụ thuộc vào các class hoặc function cụ thể, mà thay vào đó phụ thuộc vào một khái niệm trừu tượng khác (abstract).

interface Update {

  display(): void;

}

 

class TextUpdate implements Update {

  text: string;

  constructor(text: string) {

    this.text = text;

  }

  display(): void {

    console.log(`Text Update: ${this.text}`);

  }

}

 

class ImageUpdate implements Update {

  imageUrl: string;

  constructor(imageUrl: string) {

    this.imageUrl = imageUrl;

  }

 

  display(): void {

    console.log(Image Update: ${ this.imageUrl });

  }

}

 

class VideoUpdate implements Update {

  videoUrl: string;

  constructor(videoUrl: string) {

    this.videoUrl = videoUrl;

  }

  display(): void {

    console.log(Video Update: ${ this.videoUrl });

  }

}

 

class SocialMediaApp {

  updates: Update[];

  constructor() {

    this.updates = [];

  }

  addUpdate(update: Update) {

    this.updates.push(update);

  }

 

  displayUpdates() {

    this.updates.forEach(update => update.display());

  }

}

 

const socialMediaApp = new SocialMediaApp();

socialMediaApp.addUpdate(new TextUpdate("Hello, world!"));

socialMediaApp.addUpdate(new ImageUpdate("image.jpg"));

socialMediaApp.addUpdate(new VideoUpdate("video.mp4"));

socialMediaApp.displayUpdates();

Trong ví dụ này, class SocialMediaApp không phụ thuộc vào các class cụ thể để xử lý bài cập nhật văn bản, hình ảnh hoặc video, mà phụ thuộc vào abstract của interface Update. Điều này cho phép chúng ta thêm các loại bài cập nhật mới (như bài cập nhật video) mà không cần thay đổi mã nguồn hiện có.

 

Kết luận

Nguyên tắc SOLID là một tập hợp các hướng dẫn giúp nhà phát triển viết mã nguồn sạch, dễ bảo trì và dễ hiểu. Bằng cách tuân thủ các nguyên tắc này, nhà phát triển có thể đảm bảo rằng mã nguồn của họ dễ làm việc và có thể được mở rộng hoặc sửa đổi dễ dàng trong tương lai. 

Mỗi nguyên tắc có cách giải thích riêng, ưu điểm riêng và cách tổ chức cũng khác nhau. Đáng chú ý là nguyên tắc SOLID không phải là các quy tắc cứng nhắc, mà chỉ là hướng dẫn tổng quát có thể được áp dụng theo nhiều cách khác nhau, phụ thuộc vào dự án hoặc ứng dụng cụ thể.

Reference: viblo, stackoverflow, mudgen




Các thành viên đã like bài viết: