SOLID là gì ?

SOLID là từ viết tắt dùng để chỉ 5 nguyên tắc thiết kế giúp cho code của ae dev dễ tái sử dụng hơn, dễ maintain hơn, dễ mở rộng hơn. Những nguyên tắc này bao gồm:

  • Single-responsibility principle
  • Open-Closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Tôi sẽ lấy ReactJs làm ví dụ, nhưng các bạn ko cần phải lo lắng về tính áp dụng, vì bản chất cốt lõi của 5 nguyên tắc trên, nếu bạn hiểu, thì đều có thể áp dụng ở tất cả những ngôn ngữ lập trình khác.

Single-responsibility Principle

“A module should be responsible to one, and only one, actor.” — Wikipedia.

Nguyên tắc này chỉ ra răng một component chỉ nên được sử dụng cho một mục đích hoặc một trách nhiệm rõ ràng. Nghĩa là, component đó chỉ nên tập trung vào một chức năng hoặc 1 hành động cụ thể, tránh thực hiện những công việc không liên quan.

Việc tuân theo SRP giúp cho những component chúng ta phát triển ra, sẽ trở nên tập trung hơn, dễ hiểu và dễ dàng sửa đổi hơn. Hãy cũng xem ví dụ bên dưới.

// ❌ Bad Practice: Product component chứa nhiều nhiệm vụ như sau:
// - Render danh sách product
// - Render UI cho từng product

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <div key={product?.id} className="product">
                    <h3>{product?.name}</h3>
                    <p>${product?.price}</p>
                </div>
            ))}
        </div>
    );
};

Ở ví dụ trên, Products component vi phạt nguyên tắc SRP vì nó đang nhận nhiều nhiệm vụ. Lẽ ra nó chỉ nên quản lý việc render danh sách product, tuy nhiên, nó lại render UI cho từng product cụ thể. Điều này có thể làm component trở lên khó hiểu hơn trong tương lai

Thay vào đó, ta có thể áp dụng SRP như sau

// ✅ Good Practice: Tách nhỏ nhiệm vụ của Products component ra thành nhiều phần khác nhau

import Product from './Product';
import products from '../../data/products.json';

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <Product key={product?.id} product={product} />
            ))}
        </div>
    );
};

// Product.js
// Tách ra 1 component riêng chịu trách nhiệm render UI của từng product
const Product = ({ product }) => {
    return (
        <div className="product">
            <h3>{product?.name}</h3>
            <p>${product?.price}</p>
        </div>
    );
};

Việc tách nhỏ này đảm bảo mỗi component có một nhiệm vụ duy nhất, làm chúng trở nên dễ hiểu, test và dễ maintain hơn.

Open-Closed principle

“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Wikipedia.

Nguyên tắc này nhấn mạnh rằng component nên open cho việc mở rộng (VD: có thể bổ sung thêm chức năng hoặc những hành động khác) những close với những hành động chỉnh sửa (nghĩa là, đoạn code hiện tại ko nên bị thay đổi). OCP khuyển khích việc tạo ra những đoạn code linh hoạt, dễ maintain. Giờ hãy cùng xem cách triển khai thực tế nhé.

// ❌ Bad Practice: Vi phạm nguyên tắc Open-Closed Principle

// Button.js
// Button component hiện tại như sau
const Button = ({ text, onClick }) => {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

// Button.js
// Button component bị chỉnh sửa bằng cách bổ sung trực tiếp thuộc tính icon
const Button = ({ text, onClick, icon }) => {
  return (
    <button onClick={onClick}>
      <i className={icon} />
      <span>{text}</span>
    </button>
  );
}

// Home.js
const Home = () => {
  const handleClick= () => {};

  return (
    <div>
      {/* ❌ Tránh việc chỉnh sửa trực tiếp bên trong component như sau */}
      <Button text="Submit" onClick={handleClick} icon="fas fa-arrow-right" /> 
    </div>
  );
}

Ở ví dụ trên, chúng ta chỉnh sửa component Button hiện tại bằng cách thêm mới 1 thuộc tính là icon. Việc thay đổi một thành phân hiện có để đáp ứng các yêu cầu mới là vi phạm nguyên tắc OCP.

Những thay đổi này có khả năng sẽ gây ra những rủi ro ngoài ý muốn, nếu component đó đã và đang được sử dụng ở những nơi khác nhau trong source code.

Thay vào đó, hãy làm như sau

// ✅ Good Practice: Open-Closed Principle

// Button.js
// Existing Button functional component
const Button = ({ text, onClick }) => {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

// IconButton.js
// IconButton component
// ✅ Good: You have not modified anything here.
const IconButton = ({ text, icon, onClick }) => {
  return (
    <button onClick={onClick}>
      <i className={icon} />
      <span>{text}</span>
    </button>
  );
}

const Home = () => {
  const handleClick = () => {
    // Handle button click event
  }

  return (
    <div>
      <Button text="Submit" onClick={handleClick} />
      {/* 
      <IconButton text="Submit" icon="fas fa-heart" onClick={handleClick} />
    </div>
  );
}

Ở ví dụ trên, ta tạo ra một component mới gọi là IconButton. Component này chỉ làm một nhiệm vụ, đó là render button cùng với 1 icon, nó sẽ ko làm ảnh hưởng tới component Button đang được sử dụng. Cách này tuân thủ OCP bằng cách mở rộng chức năng thông qua việc tạo ra một đổi tượng mới thay vì chỉnh sửa đổi tượng hiện có.

Lưu ý, ví dụ về cách sử dụng OCP ở trên sẽ chỉ đúng với trường hợp Button component đã được sử dụng ở nhiều nơi trong source code. Nếu component đang trong quá trình phát triển thì việc áp dụng OCP ko cần thiết.

Liskov substitution principle

“Subtype objects should be substitutable for supertype objects” — Wikipedia.

Nguyên tắc Liskov nhấn mạnh về việc chỉnh sửa hay tùy chỉnh một đối tượng hay một component. Nếu chỉ nói về React component, thì LSP nói tới ý tưởng về việc tạo ra những custom component và những component có thể dễ dàng thay thế cho những component gốc mà không ảnh hưởng tới tính đúng đắn hoặc hành vi của ứng dụng. Ví dụ

// ⚠️ Bad Practice
// This approach violates the Liskov Substitution Principle as it modifies 
// the behavior of the derived component, potentially resulting in unforeseen 
// problems when substituting it for the base Select component.
const BadCustomSelect = ({ value, iconClassName, handleChange }) => {
  return (
    <div>
      <i className={iconClassName}></i>
      <select value={value} onChange={handleChange}>
        <options value={1}>One</options>
        <options value={2}>Two</options>
        <options value={3}>Three</options>
      </select>
    </div>
  );
};

const LiskovSubstitutionPrinciple = () => {
  const [value, setValue] = useState(1);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div>
      {/** ❌ Avoid this */}
      {/** Below Custom Select doesn't have the characteristics of base `select` element */}
      <BadCustomSelect value={value} handleChange={handleChange} />
    </div>
  );

Ví dụ trên, ta có 1 component là BadCustomSelect - là một component thay thế cho select input trong React. Tuy nhiên, nó vi phạm LSP vì nó hạn chế hành vi của select element. VD: nếu ta truyền thêm thuộc tính disabled hoặc readOnly thì sẽ ko có hiện tượng gì xảy ra cả

Thay vào đó, làm như sau

// ✅ Good Practice
// This component follows the Liskov Substitution Principle and allows the use of select's characteristics.

const CustomSelect = ({ value, iconClassName, handleChange, ...props }) => {
  return (
    <div>
      <i className={iconClassName}></i>
      <select value={value} onChange={handleChange} {...props}>
        <options value={1}>One</options>
        <options value={2}>Two</options>
        <options value={3}>Three</options>
      </select>
    </div>
  );
};

const LiskovSubstitutionPrinciple = () => {
  const [value, setValue] = useState(1);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div>
      {/* ✅ This CustomSelect component follows the Liskov Substitution Principle */}
      <CustomSelect
        value={value}
        handleChange={handleChange}
        defaultValue={1}
      />
    </div>
  );
};

Đoạn code trên, ta tạo ra component CustomSelect hướng tới việc mở rộng chức năng của select element. Component này nhận vào những props khác như disabled, readOnly, ... . Bằng cách đó, CustomSelect đã tuân thủ LSP

Interface segregation principle

“No code should be forced to depend on methods it does not use.” — Wikipedia.

Nguyên tắc ISP đề xuất rằng ta chỉ nên cung cấp cho component những thông tin cần thiết, nằm trong yêu cầu về chức năng và tránh cung cấp những thông tin dư thừa. Ví dụ

// ❌ Avoid: Cung cấp cho `ProductThumbnailURL ` toàn bộ product data
const ProductThumbnailURL = ({ product }) => {
  return (
    <div>
      <img src={product.imageURL} alt={product.name} />
    </div>
  );
};

// ❌ Bad Practice
const Products = ({ product }) => {
  return (
    <div>
      <ProductThumbnailURL product={product} />
      <h4>{product?.name}</h4>
      <p>{product?.description}</p>
      <p>{product?.price}</p>
    </div>
  );
};

const Products = () => {
  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
}

Ở ví dụ trên, ta truyền toàn bộ thông tin của thông tin product cho ProductThumbnailURL component, mặc dù nó chỉ yêu cầu 2 thông tin duy nhất là imageURLname.

Thay vào đó, làm như sau

// ✅ Good: chỉ nhận những thông tin cần thiết, bỏ những thông tin dư thừa
const ProductThumbnailURL = ({ imageURL, alt }) => {
  return (
    <div>
      <img src={imageURL} alt={alt} />
    </div>
  );
};

// ✅ Good Practice
const Products = ({ product }) => {
  return (
    <div>
      <ProductThumbnailURL imageURL={product.imageURL} alt={product.name} />
      <h4>{product?.name}</h4>
      <p>{product?.description}</p>
      <p>{product?.price}</p>
    </div>
  );
};

const Products = () => {
  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
};

Trong đoạn code sửa đổi trên, ProductThumbnailURL component chỉ nhận vào những thông tin cần thiết. Nó ngăn chặn được những rủi ro không cần thiết và tuân thủ nguyên tắc ISP.

Dependency inversion principle

“No component or function should care about how a particular thing is done” — Mohammad Faisal.

Về mặt lập trình hướng đối tượng, ý tưởng chính đằng sau nguyên tắc này là luôn luôn sử dụng abstraction thay vì implement cụ thể.

Vẫn khó hiểu đúng ko 😂, vậy thì ae đi vào ví dụ cho dễ hình dung

import React from "react";

const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users'

export const Users = () => {
  
    const [users , setUsers] = useState([])

    useEffect(() => {
      
      fetch(URL)
        .then(response => response.json())
        .then(json => setUsers(json))
        
    },[])

    return <>
        <div> Users List</div>
        {filteredUsers.map(user => <div>{user.name}</div>)}
    </>
}

Đoạn code trên, tôi gọi một api enpoint lấy về 1 vài thông tin và render ra dữ liệu. Tuy nhiên, mục đích chính của Users component là render dữ liệu. Nó không nên quan tâm về việc dữ liệu được lấy về ra sao hoặc lấy về từ đâu.

Vấn đề chính là ở đây, component này cần phải biết quá nhiều thứ.

Giả sử bạn có 10 component khác nhau và tất cả những component này đều có đoạn code lấy về dữ liệu của chúng. Giờ, Team lead của bạn yêu cầu sử dụng axios thay vì fetch. Xong! bạn sẽ phải vào từng file và refacctor lại toàn bộ logic code liên quan đến fetch

Thay vì vậy, sử dụng như sau

Tách đoạn xử lý fetch dữ liệu ra một hook api khác gọi là useFetch

// ./useFetch.ts
import {useState} from "react";

export const useFetch = (URL) => {

    const [data , setData] = useState([])

    useEffect(() => {
      
        fetch(URL)
            .then(response => response.json())
            .then(json => setData(json))
            
    },[])

    return data;
}

Tại Users component, ta chỉ cần import hook api đó vào và sử dụng


// ./Users.tsx 

import React from "react";
import useFetch from './useFetch'

const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users'

export const Users = () => {
  
    const users = useFetch(REMOTE_URL)

    return <>
        <div> Users List</div>
        {filteredUsers.map(user => <div>{user.name}</div>)}
    </>
}

Giờ thì, Users component chỉ cần quan tâm đến mỗi việc render dữ liệu và gọi useFetch . Nó không cần quan tâm đến những logic về việc dữ liệu trả về thì làm sao, hay thư viện nào đang được sử dụng nữa.

Kết

Đến đây, có thể các bạn cũng đã nắm được cách áp dụng của 5 nguyên tắc SOLID trong Reactjs. Hi vọng bài viết này sẽ giúp các bạn viết code tốt hơn.

Link tham khảo: