このブログでは、React アプリケーションでの SOLID 原則の実装を示します。この記事を読み終えるまでに、SOLID の原則を完全に理解できるようになります。始める前に、これらの原則について簡単に説明しましょう。
SOLID 原則は、アプリケーションの再利用性、保守性、拡張性、疎結合を維持するのに役立つ 5 つの設計原則です。 SOLID の原則は次のとおりです。
さて、これらの原則をそれぞれ個別に調べてみましょう。ここでは例として React を使用しますが、中心となる概念は他のプログラミング言語やフレームワークと似ています。
「モジュールは 1 つのアクターに対してのみ責任を負うべきです。」 — ウィキペディア。
単一責任の原則では、コンポーネントには 1 つの明確な目的または責任がある必要があると規定されています。特定の機能や動作に焦点を当て、無関係なタスクを引き受けないようにする必要があります。 SRP に従うと、コンポーネントがより焦点が絞られ、モジュール化され、理解しやすく、変更しやすくなります。実際の実装を見てみましょう。
// ❌ Bad Practice: Component with Multiple Responsibilities 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> ); };
上記の例では、 Products
コンポーネントは複数の責任を引き受けることで単一責任の原則に違反しています。製品の反復を管理し、各製品の UI レンダリングを処理します。これにより、将来的にコンポーネントの理解、保守、テストが困難になる可能性があります。
// ✅ 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> ); };
この分離により、各コンポーネントが単一の責任を負うことが保証され、理解、テスト、保守が容易になります。
「ソフトウェア エンティティ (クラス、モジュール、関数など) は拡張に対してオープンである必要がありますが、変更に対してはクローズされている必要があります。」 — ウィキペディア。
オープンクローズ原則では、コンポーネントは拡張に対してはオープンであるべき (新しい動作や機能を追加できる) が、変更に対してはクローズである必要がある (既存のコードは変更されないままであるべきである) ことが強調されています。この原則により、変更に強く、モジュール化され、保守が容易なコードの作成が促進されます。実際の実装を見てみましょう。
// ❌ 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> ); }
上の例では、 icon
プロップを追加することで既存のButton
コンポーネントを変更します。新しい要件に対応するために既存のコンポーネントを変更することは、オープンクローズの原則に違反します。これらの変更により、コンポーネントがより脆弱になり、さまざまな状況で使用した場合に予期しない副作用が発生するリスクが生じます。
// ✅ 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
機能コンポーネントを作成します。 IconButton
コンポーネントは、既存のButton
コンポーネントを変更せずに、アイコン ボタンのレンダリングをカプセル化します。変更ではなく合成によって機能を拡張することにより、オープンクローズの原則に準拠しています。
「サブタイプ オブジェクトはスーパータイプ オブジェクトの代わりに使用できる必要があります。」 — 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> );
上の例には、React のカスタム選択入力として機能することを目的としたBadCustomSelect
コンポーネントがあります。ただし、基本select
要素の動作を制限するため、リスコフ置換原則 (LSP) に違反します。
// ✅ 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> ); };
改訂されたコードには、React の標準select
要素の機能を拡張することを目的としたCustomSelect
コンポーネントがあります。このコンポーネントは、 value
、 iconClassName
、 handleChange
の props と、スプレッド演算子...props
を使用した追加の props を受け入れます。 select
要素の特性の使用を許可し、追加の小道具を受け入れることにより、 CustomSelect
コンポーネントは Liskov Substitution Principle (LSP) に従います。
「いかなるコードも、使用しないメソッドに強制的に依存すべきではありません。」 — ウィキペディア。
インターフェイス分離原則 (ISP) は、インターフェイスが広すぎてクライアントに不必要な機能の実装を強制するのではなく、特定のクライアントの要件に焦点を合わせて調整する必要があることを示唆しています。実際の実装を見てみましょう。
// ❌ 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 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> ); }
上記の例では、必須ではないにもかかわらず、製品の詳細全体をProductThumbnailURL
コンポーネントに渡します。これはコンポーネントに不必要なリスクと複雑さを追加し、インターフェイス分離原則 (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 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> ); };
改訂されたコードでは、 ProductThumbnailURL
コンポーネントは製品の詳細全体ではなく、必要な情報のみを受け取ります。これにより、不必要なリスクが防止され、インターフェイス分離原則 (ISP) が促進されます。
「1 つのエンティティは、具体化ではなく抽象化に依存する必要があります。」 — ウィキペディア。
依存性反転原則 (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 */} <BadCustomForm> <input type="email" value={email} onChange={handleChange} name="email" /> </BadCustomForm> </div> ); };
CustomForm
コンポーネントはその子と密接に結合しているため、柔軟性が損なわれ、その動作を変更または拡張することが困難になります。
// ✅ 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
コンポーネントが導入されています。 onSubmit
関数を prop として受け取り、フォームの送信を処理します。このアプローチにより、上位レベルのコンポーネントを変更することなく、フォームの動作を簡単に交換または拡張できます。
SOLID 原則は、開発者が適切に設計され、保守可能で、拡張可能なソフトウェア ソリューションを作成できるようにするガイドラインを提供します。これらの原則に従うことで、開発者はモジュール性、コードの再利用性、柔軟性を実現し、コードの複雑さを軽減できます。
このブログが貴重な洞察を提供し、既存または次の React プロジェクトにこれらの原則を適用するきっかけになったことを願っています。
好奇心を持ち続けてください。コーディングを続けてください!
参照:
ここでも公開されています。