Anwendung von SOLID-Prinzipien zur Reaktion auf die Anwendungsentwicklung

Wir haben kürzlich Material zur SOLID-Methodik veröffentlicht. Heute machen wir Sie auf eine Übersetzung eines Artikels aufmerksam, der sich mit der Anwendung von SOLID-Prinzipien in der Anwendungsentwicklung unter Verwendung der beliebten React-Bibliothek befasst.

Bild

Der Autor des Artikels sagt, dass er hier der Kürze halber nicht die vollständige Implementierung einiger Komponenten zeigt.

Prinzip der alleinigen Verantwortung (S)


Das Prinzip der Einzelverantwortung besagt, dass ein Modul nur einen Grund für Änderungen haben sollte.

Stellen Sie sich vor, wir entwickeln eine Anwendung, die eine Liste der Benutzer in einer Tabelle anzeigt. Hier ist der Code für die App Komponente:

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

Wir haben eine Komponente, in deren Status die Benutzerliste gespeichert ist. Wir laden diese Liste über HTTP von einem bestimmten Server herunter, die Liste kann bearbeitet werden. Unsere Komponente verstößt gegen das Prinzip der alleinigen Verantwortung, da sie mehr als einen Grund für Änderungen hat.

Insbesondere sehe ich vier Gründe für das Ändern einer Komponente. Die Komponente ändert sich nämlich in den folgenden Fällen:

  • Jedes Mal, wenn Sie den Anwendungstitel ändern müssen.
  • Jedes Mal, wenn Sie der Anwendung eine neue Komponente hinzufügen müssen (z. B. Seitenfuß).
  • Jedes Mal, wenn Sie den Mechanismus zum Laden von Benutzerdaten ändern müssen, z. B. Serveradresse oder Protokoll.
  • Jedes Mal, wenn Sie die Tabelle ändern müssen (z. B. die Formatierung von Spalten ändern oder andere Aktionen wie diese ausführen).

Wie können diese Probleme gelöst werden? Nachdem Sie die Gründe für die Änderung der Komponente ermittelt haben, müssen Sie versuchen, diese zu beseitigen, aus der ursprünglichen Komponente abzuleiten und für jeden dieser Gründe geeignete Abstraktionen (Komponenten oder Funktionen) zu erstellen.

Wir werden die Probleme unserer App Komponente lösen, indem wir sie umgestalten. Der Code sieht nach der Aufteilung in mehrere Komponenten folgendermaßen aus:

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

Wenn Sie nun den Titel ändern müssen, ändern wir die Header Komponente. Wenn Sie der Anwendung eine neue Komponente hinzufügen müssen, ändern wir die App Komponente. Hier haben wir die Probleme Nr. 1 (Ändern des Headers der Anwendung) und Problem Nr. 2 (Hinzufügen einer neuen Komponente zur Anwendung) gelöst. Dazu wird die entsprechende Logik von der App Komponente auf die neuen Komponenten verschoben.

Wir werden nun die Probleme Nr. 3 und Nr. 4 lösen, indem UserList die UserList Klasse UserList . Hier ist sein 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 ist unsere neue Containerkomponente. Dank ihm haben wir das Problem Nr. 3 (Ändern des Benutzerlademechanismus) gelöst, fetchUser saveUser Eigenschaftsfunktionen fetchUser und saveUser . Nachdem wir nun den Link zum Laden der Benutzerliste ändern müssen, wenden wir uns der entsprechenden Funktion zu und nehmen Änderungen daran vor.

Das letzte Problem, das wir bei Nummer 4 haben (Ändern der Tabelle, in der die Liste der Benutzer UserTable ), wurde gelöst, indem die UserTable Präsentationskomponente in das Projekt eingeführt wurde, die die Bildung von HTML-Code kapselt und die Tabelle mit Benutzern gestaltet.

Das Prinzip der Offenheit-Schließung (O)


Das Open Closed-Prinzip besagt, dass Programmentitäten (Klassen, Module, Funktionen) für Erweiterungen geöffnet sein sollten, jedoch nicht für Änderungen.

Wenn Sie sich die UserList beschriebene UserList Komponente UserList , werden Sie feststellen, dass wir die UserList Komponente UserList müssen, wenn Sie die Liste der Benutzer in einem anderen Format anzeigen müssen. Dies ist eine Verletzung des Prinzips der Offenheit-Nähe.

Sie können das Programm anhand der Zusammensetzung der Komponenten an dieses Prinzip anpassen .

Sehen Sie sich den Code der UserList Komponente an, die UserList wurde:

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

Die UserList Komponente UserList als Ergebnis der Änderung als offen für Erweiterungen, da UserList Komponenten angezeigt werden, was eine Änderung ihres Verhaltens erleichtert. Diese Komponente wird wegen Änderungen geschlossen, da alle Änderungen in separaten Komponenten durchgeführt werden. Wir können diese Komponenten sogar unabhängig voneinander bereitstellen.

Schauen wir uns nun an, wie mithilfe der neuen Komponente eine Liste der Benutzer angezeigt wird.

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

Hier erweitern wir das Verhalten der UserList Komponente, indem wir eine neue Komponente erstellen, die weiß, wie Benutzer UserList werden. Wir können sogar detailliertere Informationen zu jedem Benutzer in dieser neuen Komponente UserList , ohne die UserList Komponente zu berühren, und genau dies ist der Zweck der Überarbeitung dieser Komponente.

Das Barbara-Lisk-Substitutionsprinzip (L)


Das Substitutionsprinzip von Barbara Liskov (Liskov-Substitutionsprinzip) gibt an, dass Objekte in Programmen durch Instanzen ihrer Subtypen ersetzt werden sollten, ohne die korrekte Funktionsweise des Programms zu verletzen.

Wenn Ihnen diese Definition zu frei formuliert erscheint, finden Sie hier eine strengere Version davon.


Substitutionsprinzip von Barbara Liskov: Wenn etwas wie eine Ente aussieht und wie eine Ente quakt, aber Batterien benötigt, wird wahrscheinlich die falsche Abstraktion gewählt

Schauen Sie sich das folgende Beispiel an:

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

Wir haben eine User , deren Konstruktor Benutzerrollen akzeptiert. Basierend auf dieser Klasse erstellen wir die AdminUser Klasse. Danach haben wir eine einfache showUserRoles Funktion erstellt, die ein Objekt vom Typ User als Parameter verwendet und alle dem Benutzer zugewiesenen Rollen in der Konsole anzeigt.

Wir rufen diese Funktion auf, indem wir ordinaryUser adminUser und adminUser an sie übergeben. adminUser ein Fehler auf.


Fehler

Was ist passiert? Das Objekt der AdminUser Klasse ähnelt dem Objekt der User Klasse. Es "quakt" definitiv als User , da es die gleichen Methoden wie User . Das Problem sind die "Batterien". Tatsache ist, dass wir beim Erstellen des adminUser Objekts ein paar Objekte übergeben haben, kein Array.

Hier wird das Substitutionsprinzip verletzt, da die Funktion showUserRoles mit Objekten der User und mit Objekten, die basierend auf den untergeordneten Klassen dieser Klasse erstellt wurden, korrekt funktionieren sollte.

Es ist nicht schwierig, dieses Problem zu AdminUser Sie einfach AdminUser Array anstelle von Objekten an den AdminUser Konstruktor:

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

Prinzip der Schnittstellentrennung (I)


Das Prinzip der Schnittstellentrennung gibt an, dass Programme nicht davon abhängen sollten, was sie nicht benötigen.

Dieses Prinzip ist besonders relevant in Sprachen mit statischer Typisierung, in denen Abhängigkeiten explizit durch Schnittstellen definiert werden.

Betrachten Sie ein Beispiel:

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

Die UserTable Komponente UserRow Komponente und übergibt sie in den Eigenschaften an ein Objekt mit vollständigen Benutzerinformationen. Wenn wir den Code der UserRow Komponente analysieren, stellt sich heraus, dass er von dem Objekt abhängt, das alle Informationen über den Benutzer enthält, aber nur die Eigenschaften id und name .

Wenn Sie einen Test für diese Komponente schreiben und TypeScript oder Flow verwenden, müssen Sie eine Nachahmung für das user mit all seinen Eigenschaften erstellen. Andernfalls gibt der Compiler einen Fehler aus.

Auf den ersten Blick scheint dies kein Problem zu sein, wenn Sie reines JavaScript verwenden. Wenn sich TypeScript jedoch jemals in Ihrem Code festsetzt, führt dies plötzlich zu Testfehlern, da alle Eigenschaften der Schnittstellen zugewiesen werden müssen, auch wenn nur einige davon verwendet werden.

Wie dem auch sei, ein Programm, das das Prinzip der Trennung der Schnittstelle erfüllt, ist verständlicher.

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

Beachten Sie, dass dieses Prinzip nicht nur für Eigenschaftstypen gilt, die an Komponenten übergeben werden.

Prinzip der Abhängigkeitsinversion (D)


Das Prinzip der Abhängigkeitsinversion besagt, dass das Objekt der Abhängigkeit eine Abstraktion sein sollte, nicht etwas Spezifisches.

Betrachten Sie das folgende Beispiel:

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

Wenn wir diesen Code analysieren, wird klar, dass die App Komponente von der globalen fetch abhängt. Wenn Sie die Beziehung dieser Entitäten in UML beschreiben, erhalten Sie das folgende Diagramm.


Beziehung zwischen Komponente und Funktion

Ein High-Level-Modul sollte nicht von konkreten Implementierungen von etwas auf niedriger Ebene abhängen. Es sollte von der Abstraktion abhängen.

Die App Komponente muss nicht wissen, wie Benutzerinformationen heruntergeladen werden. Um dieses Problem zu lösen, müssen wir die Abhängigkeiten zwischen der App Komponente und der fetch invertieren. Unten sehen Sie ein UML-Diagramm, das dies veranschaulicht.


Abhängigkeitsinversion

Hier ist die Implementierung dieses Mechanismus.

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

Jetzt können wir sagen, dass die Komponente nicht sehr verbunden ist, da sie keine Informationen darüber enthält, welches Protokoll wir verwenden - HTTP, SOAP oder ein anderes. Die Komponente kümmert sich überhaupt nicht.

Die Einhaltung des Prinzips der Abhängigkeitsinversion erweitert unsere Möglichkeiten für die Arbeit mit Code, da wir den Mechanismus zum Laden von Daten sehr einfach ändern können und sich die App Komponente überhaupt nicht ändert.

Darüber hinaus vereinfacht dies das Testen, da es einfach ist, eine Funktion zu erstellen, die die Funktion des Ladens von Daten simuliert.

Zusammenfassung


Wenn Sie Zeit in das Schreiben von qualitativ hochwertigem Code investieren, werden Sie die Dankbarkeit Ihrer Kollegen und sich selbst verdienen, wenn Sie sich in Zukunft erneut mit diesem Code auseinandersetzen müssen. Die Integration von SOLID-Prinzipien in die Entwicklung von React-Anwendungen ist eine lohnende Investition.

Liebe Leser! Verwenden Sie SOLID-Prinzipien bei der Entwicklung von React-Anwendungen?

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


All Articles