SOLID 原則是五個設計原則,幫助我們保持應用的可重複使用性、可維護性、可擴展性和鬆散耦合性,這五個設計原則為:
- Single-responsibility principle 單一職責原則
- Open-Closed principle 開閉原則
- Liskov substitution principle 里氏替換原則
- Interface segregation principle 介面隔離原則
- Dependency inversion principle 依賴倒置原則
下面將以 React 為例解釋這五個設計原則
Single-responsibility Principle
“A module should be responsible to one, and only one, actor.” — Wikipedia.
單一職責原則規定組件應該有明確的目的或職責。它應該專注於特定的功能或行為,並避免重擔不相關的任務。遵循 SRP 使組件更加集中、模組化並且易於理解和修改。
舉個簡單的例子:
// ❌ Bad Practice: Component with Multiple Responsibilities
const Products = () => {
const [products, setProducts] = useState([])
// ...
return (
<div className="products">
{products.map((product) => (
<div key={product?.id} className="product">
<h3>{product?.name}</h3>
<p>${product?.price}</p>
</div>
))}
</div>
);
};
上面的例子中,Products
組件承擔多重職責,它管理 products 的迭代並處理每個 product 的 UI 渲染,違反了 SRP。
// ✅ Good Practice: Separating Responsibilities into Smaller Components
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
// Separate component responsible for rendering the product details
const Product = ({ product }) => {
return (
<div className="product">
<h3>{product?.name}</h3>
<p>${product?.price}</p>
</div>
);
};
從別處獲取資料並且將 product 的 UI 組件拆成單個組件可以確保 Products
組件符合 SRP,使組件更易於理解、測試和維護。
Open-Closed Principle
“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Wikipedia.
OCP 強調組件應該要可以添加新的行為或功能,但不應該修改原有的程式碼。
舉個例子:
// ❌ Bad Practice: Violating the Open-Closed Principle
// Button.js
// Existing Button component
const Button = ({ text, onClick }) => {
return (
<button onClick={onClick}>
{text}
</button>
);
}
// Button.js
// Modified Existing Button component with additional icon prop (modification)
const Button = ({ text, onClick, icon }) => {
return (
<button onClick={onClick}>
<i className={icon} />
<span>{text}</span>
</button>
);
}
// Home.js
// 👇 Avoid: Modified existing component prop
const Home = () => {
const handleClick= () => {};
return (
<div>
{/* ❌ Avoid this */}
<Button text="Submit" onClick={handleClick} icon="fas fa-arrow-right" />
</div>
);
}
為了讓 Button 顯示 icon,在現有的 Button 組件中做了修改,因此違反了 OCP。這個改動會使組件在不同情境中使用時有產生副作用的風險。
// ✅ 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>
);
}
正確的做法是建立一個單獨的 IconButton
組件,可以使用帶有 icon 的 Button 又不需要動到原本的 Button 組件,遵循了 OCP。
Liskov Substitution Principle
“Subtype objects should be substitutable for supertype objects” — Wikipedia.
LSP 是物件導向的基本原則,強調層次結構中物件可替換性的需要。在 React 組件的上下文中,LSP 提倡:派生組件應該能夠取代其基礎組件,而不影響應用的正確性或行為。
舉個例子:
// ⚠️ 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>
);
};
上面的例子違反了 LSP,因為 select 本身還有其他很多屬性,但是BadCustomSelect
限制了 select 只能有 value
, onChange
兩個屬性。
// ✅ 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>
);
};
在修改後的程式碼中,CustomSelect
的 select 還允許接受 select 的其他 props,遵循了 LSP。
Interface Segregation Principle
“No code should be forced to depend on methods it does not use.” — Wikipedia.
這邊的 interface 跟 TS 的 interface 沒什麼關係。
大部分文章中對 ISP 的解釋都太過複雜,其實就只是在說「不要傳入組件不需要的props」。
舉個例子:
// ❌ Avoid: disclose unnecessary information for this component
// This introduces unnecessary dependencies and complexity for the component
const ProductThumbnailURL = ({ product }) => {
return (
<div>
<img src={product.imageURL} alt={product.name} />
</div>
);
};
// ❌ Bad Practice
const Product = ({ 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>
);
}
上面的例子中 ProductThumbnailURL
組件只需要 imageURL
, name
兩個資訊,但卻把整個 product 物件都傳入,為組件增加了不必要的風險和複雜性,因此違背了 ISP。
// ✅ Good: reducing unnecessary dependencies and making
// the codebase more maintainable and scalable.
const ProductThumbnailURL = ({ imageURL, alt }) => {
return (
<div>
<img src={imageURL} alt={alt} />
</div>
);
};
// ✅ Good Practice
const Product = ({ 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>
);
};
修改後的 ProductThumbnailURL
組件只接收需要的 imageURL
和 alt
參數,遵循了 ISP。
Dependency Inversion Principle
“One entity should depend upon abstractions, not concretions” — Wikipedia.
DIP 強調父組件不應該依賴子組件。這項原則促進了鬆散耦合和模組化,並有助於更輕鬆地維護系統。
舉個例子:
// ❌ Bad Practice
// This component follows concretion instead of abstraction and
// breaks Dependency Inversion Principle
const CustomForm = ({ children }) => {
const handleSubmit = () => {
// submit operations
};
return <form onSubmit={handleSubmit}>{children}</form>;
};
const DependencyInversionPrinciple = () => {
const [email, setEmail] = useState();
const handleChange = (event) => {
setEmail(event.target.value);
};
const handleFormSubmit = (event) => {
// submit business logic here
};
return (
<div>
{/** ❌ Avoid: tightly coupled and hard to change */}
<CustomForm>
<input
type="email"
value={email}
onChange={handleChange}
name="email"
/>
</CustomForm>
</div>
);
};
上面的例子中 CustomForm
組件與其子組件 form 緊密耦合,從而阻礙了組件的靈活性,病使更改或擴展其行為變得困難。
// ✅ Good Practice
// This component follows abstraction and promotes Dependency Inversion Principle
const AbstractForm = ({ children, onSubmit }) => {
const handleSubmit = (event) => {
event.preventDefault();
onSubmit();
};
return <form onSubmit={handleSubmit}>{children}</form>;
};
const DependencyInversionPrinciple = () => {
const [email, setEmail] = useState();
const handleChange = (event) => {
setEmail(event.target.value);
};
const handleFormSubmit = () => {
// submit business logic here
};
return (
<div>
{/** ✅ Use the abstraction instead */}
<AbstractForm onSubmit={handleFormSubmit}>
<input
type="email"
value={email}
onChange={handleChange}
name="email"
/>
<button type="submit">Submit</button>
</AbstractForm>
</div>
);
};
修改後的程式碼引入了 AbstractForm
組件,它充當了 form 的抽象化,接收 onSubmit
方法作為 prop 並處理表單提交。這種方式能使我們更輕鬆地交換或擴展表單行為,而無須修改父組件。