💯 Day 02 - SOLID - JS - TypeScript - ReactJS

💯 Day 02 - SOLID - JS - TypeScript - ReactJS

date
Mar 26, 2023
slug
day-02-solid-js-reactjs
status
Published
tags
Chia sẻ
Study
TypeScript
JavaScript
Sưu tầm
summary
SOLID là 1 trong những nguyên tắc triển khai code rất quan trọng trong phát triển phần mềm. Không chỉ với JS - TypeScript - React mà nó được ứng dụng trong mọi loại ngôn ngữ lập trình OOP khác, anh em nên tuân thủ chúng để có thể viết code dễ bảo trì, dễ testing, dễ đàng đọc hiểu 💯
type
Post

S - Single Responsibility Principle 1️⃣

💫
👀 A class should have a single responsibility Một Class chỉ nên có duy nhất 1 trách nhiệm/nhiệm vụ mà thôi.
Nếu được phân bổ quá nhiều nhiệm vụ, chúng có thể gia tăng khả năng phát sinh bug hơn bình thường. Khi anh em thay đổi logic nào đó trong 1 function thực thi 1 trong nhiều nhiệm vụ riêng biệt, rất có thể anh em sẽ làm ảnh hưởng tới cả những chỗ khác mà anh em không hề hay biết 😂
Nguyên tắc này tập trung vào mục tiêu làm sao phân tách các hành vi, logic để nếu có phát sinh lỗi do sự thay đổi của anh em, sẽ không ảnh hưởng đến các hành vi, logic ko liên quan khác.
Chúng ta có ví dụ bên dưới, đây là 1 component có nhiệm vụ lấy về danh sách sản phẩm, cùng với đó là nghiệp vụ lọc sản phẩm theo rating. ⭐
import axios from "axios"; import { useEffect, useState } from "react"; import { Rating } from "react-simple-star-rating"; export function ProductFilter() { const [products, setProducts] = useState([]); const [filterRate, setFilterRate] = useState(1); const fetchProducts = async () => { const response = await axios.get( "https://fakestoreapi.com/products" ); if (response && response.data) setProducts(response.data); }; useEffect(() => { fetchProducts(); }, []); const handleRating = (rate: number) => { setFilterRate(rate); }; const filteredProducts = useMemo( () => products.filter( (product: any) => product.rating.rate > filterRate ), [products, filterRate] ); return ( <div className="flex flex-col h-full"> <div className="flex flex-col justify-center items-center"> <span className="font-semibold">Minimum Rating </span> <Rating initialValue={filterRate} SVGclassName="inline-block" onClick={handleRating} /> </div> <div className="h-full flex flex-wrap justify-center"> {filteredProducts.map((product: any) => ( <div className="w-56 flex flex-col items-center m-2 max-w-sm"> <a href="#"> <img className="p-8 rounded-t-lg h-48" src={product.image} alt="product image" /> </a> <div className="flex flex-col px-5 pb-5"> <a href="#"> <h5 className="text-lg"> {product.title} </h5> </a> <div className="flex items-center mt-2.5 mb-5 flex-1"> <span className="mr-2 px-2.5 py-0.5"> {parseInt(product.rating.rate)} </span> </div> <div className="flex flex-col items-between justify-around"> <span className="text-2xl font-bold text-gray-900 dark:text-white"> ${product.price} </span> </div> </div> </div> ))} </div> </div> ); }
Anh em có thể thấy, tất cả đều cho vào cùng 1 file component duy nhất, vừa dài, vừa khó đọc đúng không nào? 😒
Thử tưởng tượng trong dự án thực tế, code của anh em đến cả ngàn dòng, thực hiện chằng chịt nghiệp vụ, call Api lằng nhằng bên trong. Thật sự rất hại não ☹️☹️
Áp dụng nguyên tắc đầu tiên của chúng ta như nào 🤔❓
Đơn giản là phân tách code thành nhiều phần nhỏ hơn. Chúng ta có Sản phẩm phải không? File product.tsx trông sẽ như này:
interface IProduct { id: string; title: string; price: number; rating: { rate: number }; image: string; } interface IProductProps { product: IProduct; } export function Product(props: IProductProps) { const { product } = props; const { id, title, price, rating, image } = product; return ( <div className="w-56 flex flex-col items-center m-2 max-w-sm "> <a href="#"> <img className="p-8 rounded-t-lg h-48" src={image} alt="product image" /> </a> <div className="flex flex-col px-5 pb-5"> <a href="#"> <h5 className="text-lg"> {title} </h5> </a> <div className="flex items-center mt-2.5 mb-5 flex-1"> <span className="text-xs font-semibold mr-2 px-2.5 py-0.5 ml-3"> {Math.trunc(rating.rate)} </span> </div> <div className="flex flex-col items-between justify-around"> <span className="text-2xl"> ${price} </span> </div> </div> </div> ); }
Trên đây là component Product dùng để hiển thị thông tin của từng sản phẩm, chúng ta định nghĩa rõ ràng 1 sản phẩm bao gồm những gì thông qua interface IProduct
Chúng ta cấu trúc HTML cho từng line sản phẩm đó luôn. Toẹt zời 👍
Tiếp đến phần thể hiện bộ lọc Rating, như anh em thấy, ta có filter.tsx bên dưới.
import { useState } from "react"; import { Rating } from "react-simple-star-rating"; export function filterProducts(products: any[], rate: number) { return products.filter( (product: any) => product.rating.rate > rate ); } interface IFilterProps { filterRate: number; handleRating: (rate: number) => void; } export function Filter(props: IFilterProps) { const { filterRate, handleRating } = props; return ( <div className="flex flex-col justify-center items-center mb-4"> <span className="font-semibold">Minimum Rating </span> <Rating initialValue={filterRate} SVGclassName="inline-block" onClick={handleRating} /> </div> ); }
Anh em có function filterProducts sử dụng để lọc sản phẩm theo điều kiện đầu vào: rate ở mức bao nhiêu nhé.
Ok! Giờ đến phần xử lý Api lấy dữ liệu nào, Tạo custom hook useProduct như sau:
import axios from "axios"; import { useEffect, useState } from "react"; export const useProducts = () => { const [products, setProducts] = useState<any[]>([]); const fetchProducts = async () => { const response = await axios.get( "https://fakestoreapi.com/products" ); if (response && response.data) setProducts(response.data); }; useEffect(() => { fetchProducts(); }, []); return { products }; };
Kế đến là custom hook useRateFilter nha
import { useState } from "react"; export function useRateFilter() { const [filterRate, setFilterRate] = useState(1); const handleRating = (rate: number) => { setFilterRate(rate); }; return { filterRate, handleRating }; }
Và đến tiết mục quan trọng nhất, kết hợp tất cả những thứ vừa làm lại với nhau:
import { useEffect, useState } from "react"; import { Product } from "./product"; import { Rating } from "react-simple-star-rating"; import { Filter, filterProducts } from "./filter"; import { useProducts } from "./hooks/useProducts"; import { useRateFilter } from "./hooks/useRateFilter"; export function ProductFilter() { const { products } = useProducts(); const { filterRate, handleRating } = useRateFilter(); return ( <div className="flex flex-col h-full"> <Filter filterRate={filterRate as number} handleRating={handleRating} /> <div className="h-full flex flex-wrap justify-center"> {filterProducts(products, filterRate).map((product: any) => ( <Product product={product} /> ))} </div> </div> ); }
Component đã gọn gàng hơn rất nhiều rồi đúng không? 😊😊
product.tsx chỉ có duy nhất 1 vài trò là hiển thị dữ liệu của từng line Sản phẩm
filter.tsx có nhiệm vụ hiển thị chỗ chọn rating ⭐ và nghiệp vụ lọc sản phẩm
useProduct hook có nhiệm vụ lấy dữ liệu sản phẩm từ Api về
useRateFilter hook có nhiệm vụ xử lý việc chọn rating ⭐để lọc sản phẩm
Cuối cùng là ProductFilter component thay vì nhồi nhét tất cả vào 1, giờ đây nó gọi các thành phần khác để xử lý công việc. 😁
Vây là anh em đã tuân thủ được nguyên tắc đầu tiên rồi nhé 💕, phân tách code thành các thành phần nhỏ hơn, có lỗi ở đâu chỉ cần check đúng ở đó.

O - Open-Closed Principle ⭕

💫
👀 Classes should be open for extension, but closed for modification Các class nên sẵn sàng và ưu tiên cho việc mở rộng thay vì chỉnh sửa/thay đổi các tính năng sẵn có.
Hạn chế chỉnh sửa vì sẽ làm ảnh hưởng tới tính đúng đắn.
Khi cần bổ sung tính năng/logic mới, chúng ta nên kế thừa và mở rộng tạo thành các Class con. Vừa đảm bảo chúng có đặc tính của Class cha vừa được bổ sung tính năng mới.
Bình thường khi có yêu cầu bổ sung tính năng hoặc thay đổi nghiệp vụ, anh em sẽ sửa code đã có trước đó. Tuy nhiên việc làm này ẩn chứa rủi ro sai lệch cho ứng dụng của chúng ta và vi phạm nguyên tắc Open-Closed.
Ta có VD bên dưới:
import { HiOutlineArrowNarrowRight, HiOutlineArrowNarrowLeft, } from "react-icons/hi"; interface IButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { text: string; role?: "back" | "forward" | "main" | "not-found"; } export function Button(props: IButtonProps) { const { text, role} = props; return ( <button className="flex items-center outline-none pt-4 pb-4 pl-8 pr-8 rounded-xl" {...props} > {text} <div className="m-2"> {role === "forward" && <HiOutlineArrowNarrowRight />} {role === "back" && <HiOutlineArrowNarrowLeft />} </div> </button> ); }
Anh em có thể thấy đây đoạn code mô tả Component Button với việc hiển thị các icon tương ứng với từng role khác nhau.
Nếu chúng ta thêm hàng tá role, rõ ràng là anh em phải sửa lại component, bổ sung điều kiện để hiển thị đúng icon.
Thay vào đó anh em có thể sửa lại Component như sau:
interface IButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { text: string; icon?: React.ReactNode; } export function Button(props: IButtonProps) { const { text, icon } = props; return ( <button className="flex items-center outline-none pt-4 pb-4 pl-8 pr-8 rounded-xl" {...props} > {text} <div className="m-2"> {icon} </div> </button> ); }
Khi sử dụng anh em chỉ cần thay đổi icon phù hợp là được
import { Button } from "./button"; import { HiOutlineArrowNarrowRight, HiOutlineArrowNarrowLeft, } from "react-icons/hi"; export function Index() { return ( <div className="flex space-x-10"> <Button text="Go Home" icon={<HiOutlineArrowNarrowRight />} /> <Button text="Go Back" icon={<HiOutlineArrowNarrowLeft />} /> </div> ); }

L - Liskov Subsitution Principle 🚉

💫
👀 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. Nếu B là kiểu con của A, thì object thuộc kiểu A trong một chương trình P có thể được thay thế bởi object thuộc kiểu B mà không làm thay đổi bất kỳ hành vi nào của P được định nghĩa bởi kiểu A.
Khi Class con không thể thực hiện các hành động giống với Class cha, điều này có thể phát sinh nhiều bug.
Nếu anh em có một Class và anh em tạo một Class khác kế thừa nó, thì Class mới này tốt hơn hết hãy để cho nó có thể làm được những gì mà Class cha đã định nghĩa.
Class con có thể thực hiện các hành vi, xử lý được các yêu cầu và trả về kết quả tương tự hoặc chí ít là trả về kết quả có cùng type với Class cha.
Mục tiêu của nguyên tắc này là thực thi tính nhất quán đảm bảo việc các Class cha và con có thể sử dụng thay thế cho nhau mà không gây ra lỗi.
Chúng ta có Button component như sau
import React from 'react'; const Button = ({ text, onClick }) => ( <button onClick={onClick}> {text} </button> ); export default Button;
Component này thực hiện rất tốt nhiệm vụ của mình đó là hiển thị tiêu đề và handle click event.
Anh em có thêm LinkButton, vẫn thực hiện được các tác vụ y hệt Button nhưng được bổ sung thêm thẻ a
import React from 'react'; import Button from './Button'; const LinkButton = ({ text, url, onClick }) => ( <Button onClick={onClick}> <a href={url}>{text}</a> </Button> ); export default LinkButton;
Như vậy LinkButton vẫn đảm bảo rằng tittle và event click được xử lý, kèm vào đó bổ sung thêm thẻ a với thuộc tính href
import React from 'react'; import Button from './Button'; import LinkButton from './LinkButton'; const MyComponent = () => ( <div> <Button text="Click me!" onClick={() => console.log('Button clicked')} /> <LinkButton text="Google" url="https://www.google.com" onClick={() => console.log('LinkButton clicked')} /> </div> );

I - Interface Segregation Principle 🥹

💫
👀 Clients should not be forced to depend on methods that they do not use. Các thành phần trong một chương trình không nên bị bó buộc vào các phương thức mà chúng không sử dụng
Khi một Class được yêu câu phải thực hiện các hành động không không mang lại lợi ích nào, điều này là phí phạm và chúng có thể gây ra nhưng lỗi không mong muốn nếu Class đó không có khả năng thực hiện những hành động đó.
Một Class chỉ nên thực hiện những hành động mà chúng cần để hoàn thành vai trò của mình. Tất các hành động còn lại nên được loại bỏ hoàn toàn hoặc chuyển chúng qua đâu đó, nơi mà chúng có thể được sử dụng trong tương lai.
Mục tiêu của nguyên tắc này là chia nhỏ một tập hợp các hành động thành các tập hợp nhỏ hơn, điều này giúp cho các Class chỉ phải thực thi đúng những thứ nó được yêu cầu.
VD sau đây để anh em dễ hình dung hơn:
import { Thumbnail } from "./thumbnail"; export interface IProduct { id: string; title: string; price: number; rating: { rate: number }; image: string; } interface IProductProps { product: IProduct; } export function Product(props: IProductProps) { const { product } = props; return ( <div className="w-56 flex flex-col items-center m-2 max-w-sm"> <span> {Math.trunc(rating.rate)} </span> <a href="#"> <Thumbnail product={product} /> </a> </div> <div className="flex flex-col px-5 pb-5"> <a href="#"> <h5 className="text-lg"> {title} </h5> </a> <span className="text-2xl"> ${price} </span> </div> ); }
import { IProduct } from "./product"; interface IThumbnailProps { product: IProduct; } export function Thumbnail(props: IThumbnailProps) { const { product } = props; return ( <img className="p-8 rounded-t-lg h-48" src={product.image} alt="product image" /> ); }
Rõ ràng Thumbnail chỉ cần IThumbnailProps chứa imageURL để hiên thị hình ảnh mà thôi, nhưng lại phải gánh thêm tất cả các thuộc tính của Interface IProduct, chúng ta phải truyền cả object profuct vào chỉ để lấy image, điều này dẫn tới dư thừa không đáng có
Thay vào đó anh em điều chỉnh lại như sau:
interface IThumbnailProps { imageUrl: string } export function Thumbnail(props: IThumbnailProps) { const { imageUrl } = props; return ( <img className="p-8 rounded-t-lg h-48" src={imageUrl} alt="product image" /> ); }
Lúc này IThumbnailProps chỉ cần duy nhất imageUrl không bị bó buộc vào IProduct nữa và không vi phạm nguyên tắc ISP

D - Dependency Inversion Principle 🧩

💫
👀 High-level modules should not depend on low-level modules. Both should depend on the abstraction. 👀 Abstractions should not depend on details. Details should depend on abstractions. Các module cấp cao hơn không nên phụ thuộc vào module cấp thấp hơn. Cả 2 nên phụ thuộc vào Abstraction. Abstraction không nên phụ thuộc vào các thể hiện của mình, các thể hiện nên phụ thuộc vào Abstraction.
High-level Module(or Class): Một Class có thể thực thi nhiệm vụ với công cụ nào đó.
Low-level Module (or Class): Công cụ cần có để thực thi nhiệm vụ.
Abstraction: Thể hiện qua một interface là cấu nối cho 2 Class.
Details: Cách mà công cụ đó làm việc.
Nguyên tắc này phát biểu rằng: Một Class không nên hợp nhất với công cụ dùng để thực thi một hành động, Thay vào đó nó nên được hợp nhất với Interface cho phép công cụ đó liên kết với Class.
Mục tiêu chính là để giảm sự phụ thuộc của high-level Class on the low-level Class bằng cách introducing an interface
Vd thể hiện với React như sau:
import React, { useState, useEffect } from 'react'; import TodoService from './TodoService'; function TodoList() { const [todos, setTodos] = useState([]); useEffect(() => { async function fetchTodos() { try { const todos = await TodoService.getTodos(); setTodos(todos); } catch (error) { console.error(error); } } fetchTodos(); }, []); return ( <ul> {todos.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); } export default TodoList;
Rõ ràng là TodoList đã bị gắn chặt vào TodoService Nếu có thay đổi gì nghiệp vụ getTodos chúng ta phải vào hẳn TodoService để sửa logic. Vậy nếu ngày đẹp trời nào đó, KH thay đổi liên tục, lúc muốn dùng logic cũ, mãi lại muốn về logic mới?
Để giải quyết điều này, anh em truyền TodoService như một prop đầu vào cho TodoList. Lúc này component không phải import trực tiếp TodoService vào nữa.
import React, { useState, useEffect } from 'react'; function TodoList({ todoService }) { const [todos, setTodos] = useState([]); useEffect(() => { async function fetchTodos() { try { const todos = await todoService.getTodos(); setTodos(todos); } catch (error) { console.error(error); } } fetchTodos(); }, [todoService]); return ( <ul> {todos.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); } export default TodoList;
Cách này giúp chúng ta có thể thay đổi thể hiện của TodoService bằng cách truyền một thể hiện của một Service khác cho TodoList component một cách dễ dàng
import React from 'react'; import TodoService from './TodoService'; import TodoServiceV2 from './TodoServiceV2'; import TodoList from './TodoList'; function App() { // return <TodoList todoService={TodoService} />; return <TodoList todoService={TodoServiceV2} />; } export default App;