React 中的 SOLID 原則

React 中的 SOLID 原則

翻譯自 Mastering SOLID Principles in Just 8 Minutes!

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 組件只接收需要的 imageURLalt 參數,遵循了 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 並處理表單提交。這種方式能使我們更輕鬆地交換或擴展表單行為,而無須修改父組件。

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *