Reactアプリケーション開発へのSOLID原則の適用

最近、SOLID方法論に関する資料を公​​開しました。 本日は、人気のあるReactライブラリを使用したアプリケーション開発におけるSOLID原則の適用に関する記事の翻訳に注目してください。



画像



この記事の著者は、ここでは簡潔にするために、一部のコンポーネントの完全な実装を示していないと述べています。



唯一の責任の原則(S)



単一責任の原則は、モジュールには変更の理由が1つだけあるべきだと言っています。



テーブルにユーザーのリストを表示するアプリケーションを開発していると想像してください。 App



コンポーネントのコードは次のとおりです。



 class App extends Component {   state = {       users: [{name: 'Jim', surname: 'Smith', age: 33}]   };   componentDidMount() {       this.fetchUsers();   }   async fetchUsers() {       const response = await fetch('http://totallyhardcodedurl.com/users');       const users = await response.json();       this.setState({users});   }   render() {       return (           <div className="App">               <header className="App-header">                 //                     </header>               <table>                   <thead>                       <tr>                           <th>First name</th>                           <th>Last name</th>                           <th>Age</th>                       </tr>                   </thead>                   <tbody>                       {this.state.users.map((user, index) => (                           <tr key={index}>                               <td><input value={user.name} onChange={/* update name in the state */}/></td>                               <td><input value={user.surname} onChange={/* update surname in the state*/}/></td>                               <td><input value={user.age} onChange={/* update age in the state */}/></td>                           </tr>                       ))}                   </tbody>               </table>               <button onClick={() => this.saveUsersOnTheBackend()}>Save</button>           </div>       );   }   saveUsersOnTheBackend(row) {       fetch('http://totallyhardcodedurl.com/users', {           method: "POST",           body: JSON.stringify(this.state.users),       })   } }
      
      





ユーザーのリストが保存されている状態のコンポーネントがあります。 特定のサーバーからHTTP経由でこのリストをダウンロードします;リストは編集可能です。 変更の理由は複数あるため、このコンポーネントは単独責任の原則に違反しています。



特に、コンポーネントを変更する4つの理由がわかります。 つまり、次の場合にコンポーネントが変更されます。





これらの問題を解決するには? コンポーネントを変更する理由を特定した後、それらを削除し、元のコンポーネントから推測し、そのような各理由に適した抽象化(コンポーネントまたは関数)を作成する必要があります。



App



コンポーネントの問題をリファクタリングして解決します。 そのコードは、いくつかのコンポーネントに分割した後、次のようになります。



 class App extends Component {   render() {       return (           <div className="App">               <Header/>               <UserList/>           </div>       );   } }
      
      





ここで、タイトルを変更する必要がある場合、 Header



コンポーネントを変更し、アプリケーションに新しいコンポーネントを追加する必要がある場合、 App



コンポーネントを変更します。 ここでは、問題1(アプリケーションのヘッダーを変更する)と問題2(アプリケーションに新しいコンポーネントを追加する)を解決しました。 これは、対応するロジックをApp



コンポーネントから新しいコンポーネントに移動することにより行われます。



次に、 UserList



クラスを作成して、No。3とNo. 4の問題を解決します。 彼のコードは次のとおりです。



 class UserList extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   state = {       users: [{name: 'Jim', surname: 'Smith', age: 33}]   };   componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   render() {       return (           <div>               <UserTable users={this.state.users} onUserChange={(user) => this.updateUser(user)}/>               <button onClick={() => this.saveUsers()}>Save</button>           </div>       );   }   updateUser(user) {     //         }   saveUsers(row) {       this.props.saveUsers(this.state.users);   } }
      
      





UserList



は新しいコンテナコンポーネントです。 彼のおかげで、関数プロパティfetchUser



saveUser



作成することで、問題3(ユーザーのロードメカニズムを変更する)を解決しました。 その結果、ユーザーのリストをロードするために使用するリンクを変更する必要があるので、対応する機能に変更を加えます。



4番目にある最後の問題(ユーザーのリストを表示するテーブルの変更)は、 UserTable



プレゼンテーションコンポーネントをプロジェクトに導入することで解決しました。これは、HTMLコードの形成をカプセル化し、ユーザーにテーブルをスタイリングします。



開放性閉鎖の原理(O)



Open Closed Principleでは、プログラムエンティティ(クラス、モジュール、関数)は拡張のために開かれている必要がありますが、修正のためには開かれていないと述べています。



上記のUserList



コンポーネントを見ると、ユーザーのリストを別の形式で表示する必要がある場合、このコンポーネントのrender



メソッドを変更する必要があることに気付くでしょう。 これは、開放性と閉鎖性の原則に違反しています。



コンポーネント構成を使用して、この原則に沿ったプログラムを作成できます



リファクタリングされたUserList



コンポーネントのコードを見てください。



 export class UserList extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   state = {       users: [{id: 1, name: 'Jim', surname: 'Smith', age: 33}]   };   componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   render() {       return (           <div>               {this.props.children({                   users: this.state.users,                   saveUsers: this.saveUsers,                   onUserChange: this.onUserChange               })}           </div>       );   }   saveUsers = () => {       this.props.saveUsers(this.state.users);   };   onUserChange = (user) => {       //         }; }
      
      





UserList



コンポーネントは、変更の結果、子コンポーネントを表示し、動作の変更を容易にするため、拡張用に開かれていることが判明しました。 すべての変更は個別のコンポーネントで実行されるため、このコンポーネントは変更のために閉じられます。 これらのコンポーネントを個別に展開することもできます。



次に、新しいコンポーネントを使用して、ユーザーのリストがどのように表示されるかを見てみましょう。



 export class PopulatedUserList extends Component {   render() {       return (           <div>               <UserList>{                   ({users}) => {                       return <ul>                           {users.map((user, index) => <li key={index}>{user.id}: {user.name} {user.surname}</li>)}                       </ul>                   }               }               </UserList>           </div>       );   } }
      
      





ここでは、ユーザーをリストする方法を知っている新しいコンポーネントを作成して、 UserList



コンポーネントの動作を拡張します。 UserList



コンポーネントに触れることなく、この新しいコンポーネントの各ユーザーに関する詳細情報をダウンロードすることもできます。これはまさにこのコンポーネントをリファクタリングする目的です。



バーバラリスク代替原理(L)



Barbara Liskov(Liskov Substitution Principle)の置換原理は、プログラム内のオブジェクトを、プログラムの正しい操作に違反することなく、サブタイプのインスタンスに置き換える必要があることを示しています。



この定義があまりにも自由に定式化されているように思える場合、より厳密なバージョンを以下に示します。









バーバラ・リスコフを代用する原則:何かがカモのように見え、カモはカモのように見えるが、バッテリーが必要な場合-間違った抽象化がおそらく選択される



次の例を見てください。



 class User { constructor(roles) {   this.roles = roles; } getRoles() {   return this.roles; } } class AdminUser extends User {} const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser({role: 'moderator'},{role: 'admin'}); function showUserRoles(user) { const roles = user.getRoles(); roles.forEach((role) => console.log(role)); } showUserRoles(ordinaryUser); showUserRoles(adminUser);
      
      





コンストラクターがユーザーロールを受け入れるUser



クラスがあります。 このクラスに基づいて、 AdminUser



クラスを作成します。 その後、単純なshowUserRoles



関数を作成しましたshowUserRoles



関数は、 User



型のオブジェクトをパラメーターとして受け取り、ユーザーに割り当てられたすべてのロールをコンソールに表示します。



ordinaryUser



adminUser



オブジェクトとadminUser



オブジェクトを渡すことでこの関数を呼び出した後、エラーが発生します。









エラー



どうした AdminUser



クラスのオブジェクトは、 User



クラスのオブジェクトに似ています。 User



と同じメソッドを持っているので、それは間違いなくUser



として「鳴ります」。 問題は「バッテリー」です。 実際には、 adminUser



オブジェクトを作成するときに、配列ではなくいくつかのオブジェクトを渡しています。



ここでは、 showUserRoles



関数がUser



クラスのオブジェクトおよびこのクラスの子孫クラスに基づいて作成されたオブジェクトで正しく機能するため、置換の原則に違反しています。



この問題をAdminUser



ことは難しくありません-オブジェクトの代わりに配列をAdminUser



コンストラクターに渡すだけです:



 const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']);
      
      





インターフェース分離の原理(I)



インターフェイスの分離の原則(インターフェイス分離の原則)は、プログラムが必要のないものに依存してはならないことを示しています。



この原則は、依存関係がインターフェイスによって明示的に定義されている静的型付けの言語に特に関連しています。



例を考えてみましょう:



 class UserTable extends Component {   ...     render() {       const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33};       return (           <div>               ...                 <UserRow user={user}/>               ...           </div>       );   }    ... } class UserRow extends Component {   static propTypes = {       user: PropTypes.object.isRequired,   };   render() {       return (           <tr>               <td>Id: {this.props.user.id}</td>               <td>Name: {this.props.user.name}</td>           </tr>       )   } }
      
      





UserTable



コンポーネントUserRow



UserTable



コンポーネントをUserRow



プロパティで、完全なユーザー情報を持つオブジェクトを渡します。 UserRow



コンポーネントのコードを分析すると、ユーザーに関するすべての情報を含むオブジェクトに依存していることがUserRow



ますが、必要なのはid



プロパティとname



プロパティのみです。



このコンポーネントのテストを作成し、TypeScriptまたはFlowを使用する場合、すべてのプロパティをuser



してuser



オブジェクトの模倣を作成する必要があります。作成しないと、コンパイラーはエラーをスローします。



一見、純粋なJavaScriptを使用している場合、これは問題のようには見えませんが、TypeScriptがコード内に落ち着くと、一部のインターフェイスのみが使用されている場合でも、インターフェイスのすべてのプロパティを割り当てる必要があるため、突然テストが失敗します。



インターフェースの分離の原則を満たすプログラムの方がわかりやすいでしょう。



 class UserTable extends Component {   ...     render() {       const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33};       return (           <div>               ...                 <UserRow id={user.id} name={user.name}/>               ...           </div>       );   }    ... } class UserRow extends Component {   static propTypes = {       id: PropTypes.number.isRequired,       name: PropTypes.string.isRequired,   };   render() {       return (           <tr>               <td>Id: {this.props.id}</td>               <td>Name: {this.props.name}</td>           </tr>       )   } }
      
      





この原則は、コンポーネントに渡されるプロパティタイプだけに適用されるわけではありません。



依存関係反転の原理(D)



依存関係の逆転の原則は、依存関係のオブジェクトは特定のものではなく抽象化する必要があることを示しています。



次の例を考えてみましょう。



 class App extends Component { ... async fetchUsers() {   const users = await fetch('http://totallyhardcodedurl.com/stupid');   this.setState({users}); } ... }
      
      





このコードを分析すると、 App



コンポーネントがグローバルfetch



関数に依存していることが明らかになります。 これらのエンティティの関係をUML言語で記述すると、次の図が得られます。









コンポーネントと機能の関係



高レベルのモジュールは、何かの低レベルの具体的な実装に依存すべきではありません。 抽象化に依存する必要があります。



App



コンポーネントは、ユーザー情報をダウンロードする方法を知る必要はありません。 この問題を解決するには、 App



コンポーネントとfetch



関数の間の依存関係を逆にする必要があります。 以下は、これを説明するUML図です。









依存関係の反転



このメカニズムの実装は次のとおりです。



 class App extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   ...     componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   ... }
      
      





ここで、使用する特定のプロトコル(HTTP、SOAP、またはその他)に関する情報がないため、コンポーネントは適切に接続されていないと言えます。 コンポーネントはまったく気にしません。



依存関係の反転の原則に準拠すると、コードを操作する可能性が広がります。これは、データの読み込みメカニズムを非常に簡単に変更でき、 App



コンポーネントがまったく変更されないためです。



さらに、データをロードする機能をシミュレートする機能を簡単に作成できるため、テストが簡単になります。



まとめ



高品質のコードを書くことに時間をかけることにより、将来、このコードに再び直面しなければならないときに、同僚とあなた自身の感謝を得ることができます。 Reactアプリケーション開発にSOLID原則を組み込むことは価値のある投資です。



親愛なる読者! Reactアプリケーションを開発するときに、SOLID原則を使用していますか?






All Articles