
この記事の著者は、ここでは簡潔にするために、一部のコンポーネントの完全な実装を示していないと述べています。
唯一の責任の原則(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原則を使用していますか?
