我们最近发布了有关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) {
UserList
是我们的新容器组件。 多亏了他,我们通过创建
fetchUser
和
saveUser
属性
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
传递
adminUser
和
adminUser
对象来调用此函数,此后我们遇到错误。
失误发生什么事了
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
组件的代码,事实证明它取决于包含有关用户的所有信息的对象,但是他只需要
id
和
name
属性。
如果为此组件编写测试并使用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:
如果我们分析此代码,很明显
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原理?
