📜 ⬆️ ⬇️

Application of SOLID principles in the development of React-applications

We recently published material on the SOLID methodology. Today we bring to your attention the translation of an article that is devoted to applying the SOLID principles in developing applications using the popular React library.

image

The author of the article says that here, for the sake of brevity, he does not show the full implementation of some components.

The principle of sole responsibility (S)


The Single Responsibility Principle tells us that the module should have one and only one reason for change.
')
Imagine that we are developing an application that displays a list of users in a table. Here is the App component code:

 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),       })   } } 

We have a component in which the list of users is stored. We download this list via HTTP from some server, the list is editable. Our component violates the principle of sole responsibility, as it has more than one reason for change.

In particular, I can see four reasons for changing a component. Namely, the component changes in the following cases:


How to solve these problems? It is necessary, after the reasons for changing a component are identified, to try to eliminate them, to derive them from the source component, creating suitable abstractions (components or functions) for each such reason.

We will solve the problems of our App component by refactoring it. Its code, after splitting it into several components, will look like this:

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

Now, if we need to change the title, we change the Header component, and if we need to add a new component to the application, we change the App component. Here we have solved problems # 1 (changing the header of the application) and problem # 2 (adding a new component to the application). This is done by moving the corresponding logic from the App component to the new components.

Now we are UserList solve problems №3 and №4, creating the class UserList . Here is his code:

 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 is our new container component. Thanks to him, we solved problem number 3 (changing the user loading mechanism) by creating the function-properties fetchUser and saveUser . As a result, now, when we need to change the link used to load the list of users, we turn to the corresponding function and make changes to it.

The last problem, which we have at number 4 (changing the table that displays the list of users), has been solved by introducing the presentation component UserTable , which encapsulates the formation of HTML-code and styling the table with users.

The principle of openness-closure (O)


The Open Closed Principle states that software entities (classes, modules, functions) must be open for expansion, but not for modification.

If you look at the UserList component described above, you will notice that if you need to display a list of users in a different format, we will have to modify the render method of this component. This is a violation of the principle of openness-closeness.

To bring the program into compliance with this principle, you can use the composition of components .

Take a look at the code for the UserList component that has been refactored:

 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) => {       //         }; } 

The UserList component, as a result of the modification, turned out to be open for expansion, since it displays the child components, which makes it easier to change its behavior. This component is closed for modification, as all changes are performed in separate components. We can even deploy these components independently.

Now let's look at how, using the new component, a list of users is displayed.

 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>       );   } } 

Here we expand the behavior of the UserList component, creating a new component that knows how to display a list of users. We can even download more detailed information about each of the users in this new component, without touching the UserList component, and this is precisely the purpose of refactoring this component.

Barbara Liskov substitution principle (L)


The substitution principle of Barbara Liskov (Liskov Substitution Principle) indicates that the objects in the programs must be replaced by instances of their subtypes without disrupting the correctness of the program.

If this definition seems to you to be too loosely formulated - that’s a stricter version of it.


Barbara Liskov's substitution principle: if something looks like a duck and quacks like a duck, but needs batteries - the wrong abstraction is probably chosen

Take a look at the following example:

 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); 

We have a class User , whose constructor assumes user roles. Based on this class, we create the AdminUser class. After that, we created a simple showUserRoles function that takes a User object as a parameter and displays to the console all the roles assigned to the user.

We call this function, passing it the objects ordinaryUser and adminUser , and then we encounter an error.


Mistake

What happened? The AdminUser class AdminUser is similar to the User class object. It definitely "quacks" as User , since it has the same methods as User . The problem is the "battery". The fact is that by creating the adminUser object, we passed it a couple of objects, not an array.

The substitution principle is violated here, since the showUserRoles function must work correctly with objects of the User class and with objects created on the basis of the heir classes of this class.

Fixing this problem is easy - just pass the array to the AdminUser constructor instead of the objects:

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

Interface separation principle (I)


Interface Segregation Principle indicates that programs should not depend on what they do not need.

This principle is especially relevant in languages ​​with static typing, in which dependencies are explicitly given by interfaces.

Consider an example:

 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>       )   } } 

The UserTable component UserRow component, passing to it, in properties, an object with complete information about the user. If we analyze the code of the UserRow component, it turns out that it depends on the object containing all the information about the user, but all he needs is the id and name properties.

If you write a test for this component and at the same time use TypeScript or Flow, you will have to create a simulation for the user object with all its properties, otherwise the compiler will generate an error.

At first glance, this does not seem to be a problem if you use pure JavaScript, but if TypeScript ever lodges in your code, this will suddenly lead to test failure due to the need to assign all interface properties, even if only some of them are used.

Whatever it was, but the program, which satisfies the principle of interface separation, turns out to be more understandable.

 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>       )   } } 

Remember that this principle applies not only to the types of properties passed to the components.

Principle of dependency inversion (D)


The principle of dependency inversion (Dependency Inversion Principle) tells us that the object of the dependency must be an abstraction, and not something concrete.

Consider the following example:

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

If we analyze this code, it becomes clear that the App component depends on the global function fetch . If we describe the relationship of these entities in the UML language, we get the following diagram.


Component and function relationships

A high-level module should not depend on low-level concrete implementations of anything. It must depend on abstraction.

The App component does not need to know how to load user information. In order to solve this problem, we need to invert the dependencies between the App component and the fetch function. Below is a UML diagram illustrating this.


Inversion of dependencies

Here is the implementation of this mechanism.

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

Now we can say that the component is distinguished by its weak connectivity, since it does not have information about which specific protocol we use - HTTP, SOAP, or some other. The component does not care at all.

Adherence to the principle of dependency inversion extends our ability to work with code, since we can very easily change the data loading mechanism, and the App component will not change at all.

In addition, it simplifies testing, as it is easy to create a function that simulates the function of loading data.

Results


By investing time in writing quality code, you will earn the gratitude of your colleagues and yourself when, in the future, you will have to come across this code again. Implementing SOLID principles in the development of React applications is a worthwhile investment of time.

Dear readers! Do you use the principles of SOLID when developing React-applications?

Source: https://habr.com/ru/post/428079/


All Articles