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à imageURL
và name
.
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: