将SOLID原理应用于React应用程序开发

我们最近发布了有关SOLID方法的材料 。 今天,我们引起您注意的是一篇文章的翻译,该文章使用流行的React库解决了SOLID原理在应用程序开发中的应用。

图片

文章的作者说,为了简洁起见,在这里他没有显示某些组件的完整实现。

唯一责任原则(S)


单一责任原则告诉我们,一个模块应该只有一个改变理由。

假设我们正在开发一个在表中显示用户列表的应用程序。 这是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从某个服务器下载此列表;该列表是可编辑的。 我们的组件违反唯一责任原则,因为它有多个更改原因。

特别是,我看到了更改组件的四个原因。 即,在以下情况下组件发生变化:

  • 每次您需要更改应用程序标题。
  • 每次您需要向应用程序中添加新组件时(例如,页脚)。
  • 每次您需要更改用于加载用户数据的机制时,例如服务器地址或协议。
  • 每次您需要更改表时(例如,更改列的格式或执行类似的其他操作)。

如何解决这些问题? 在确定了更改组件的原因之后,有必要尝试消除它们,从原始组件中推断出原因,并针对每种此类原因创建合适的抽象(组件或功能)。

我们将通过重构来解决App组件的问题。 将其分为几个部分后,其代码将如下所示:

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

现在,如果您需要更改标题,我们将更改Header组件,并且如果您需要向应用程序中添加新组件,那么我们将更改App组件。 在这里,我们解决了问题1(更改应用程序的标头)和问题2(向应用程序添加新组件)。 这是通过将相应的逻辑从App组件移动到新组件来完成的。

现在,我们将通过创建UserList类来解决第3号和第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是我们的新容器组件。 多亏了他,我们通过创建fetchUsersaveUser属性saveUser解决了问题3(更改了用户加载机制)。 结果,现在我们需要更改用于加载用户列表的链接,我们转到相应的功能并对其进行更改。

通过将UserTable表示组件引入到项目中,我们解决了第4个问题(更改显示用户列表的表),该组件封装了HTML代码的形成并为用户设置了样式。

开闭原理(O)


开放封闭原则指出,程序实体(类,模块,函数)应开放以进行扩展,而不能进行修改。

如果查看上面描述的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组件,而这正是重构该组件的目的。

芭芭拉·里斯克(Barbara Lisk)换人原则(L)


Barbara Liskov的替换原理(Liskov替换原理)表明,程序中的对象应使用其子类型的实例替换,而不会破坏程序的正确操作。

如果您觉得这个定义过于随意,这里是一个更严格的定义。


替换芭芭拉·里斯科夫(Barbara Liskov)的原理:如果某物看起来像鸭子而鹌鹑像鸭子,但需要电池,则可能选择了错误的抽象

看下面的例子:

 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函数,该函数将类型为User的对象作为参数,并在控制台中显示分配给该用户的所有角色。

我们通过adminUser传递adminUseradminUser对象来调用此函数,此后我们遇到错误。


失误

发生什么事了 AdminUser类的对象类似于User类的对象。 由于它具有与User相同的方法,因此它肯定是User “庸医”。 问题是“电池”。 事实是,在创建adminUser对象时,我们adminUser传递了几个对象,而不是数组。

这里违反了替换原理,因为showUserRoles函数应该与User类的对象以及基于该类的后代类创建的对象showUserRoles正常工作。

AdminUser这个问题并不难-只需将AdminUser数组而不是对象传递给AdminUser构造函数即可:

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

接口分离原理(一)


接口隔离原则表明程序不应依赖于它们不需要的内容。

该原理在具有静态类型的语言中特别重要,在静态类型中,依赖项由接口明确定义。

考虑一个例子:

 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组件,并在属性中传递具有完整用户信息的对象。 如果我们分析UserRow组件的代码,事实证明它取决于包含有关用户的所有信息的对象,但是他只需要idname属性。

如果为此组件编写测试并使用TypeScript或Flow,则必须为其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组件则完全不会更改。

此外,由于可以轻松创建一个模拟加载数据功能的功能,因此可以简化测试。

总结


花时间编写高质量的代码,当您将来不得不再次面对此代码时,您将赢得同事和您自己的感激。 将SOLID原理集成到React应用程序开发中是一项值得的投资。

亲爱的读者们! 开发React应用程序时是否使用SOLID原理?

Source: https://habr.com/ru/post/zh-CN428079/


All Articles