Verwendung von Control Inversion in JavaScript und Reactjs zur Vereinfachung der Codebehandlung

Verwendung von Control Inversion in JavaScript und Reactjs zur Vereinfachung der Codebehandlung


Inversion of Control ist ein ziemlich einfach zu verstehendes Programmierprinzip, das gleichzeitig Ihren Code erheblich verbessern kann. Dieser Artikel beschreibt, wie Sie Control Inversion in JavaScript und in Reactjs anwenden.


Wenn Sie bereits Code geschrieben haben, der an mehreren Stellen verwendet wird, kennen Sie diese Situation:


  1. Sie erstellen einen wiederverwendbaren Code (es kann sich um eine Funktion, eine React-Komponente, einen React-Hook usw. handeln) und geben ihn frei (für die Zusammenarbeit oder die Veröffentlichung in Open Source).
  2. Jemand bittet Sie, neue Funktionen hinzuzufügen. Ihr Code unterstützt die vorgeschlagene Funktionalität nicht, es könnte jedoch sein, dass Sie eine kleine Änderung vorgenommen haben.
  3. Sie fügen Ihrem Code und der zugehörigen Logik ein neues Argument / eine neue Eigenschaft / Option hinzu, damit diese neue Funktion weiterhin funktioniert.
  4. Wiederholen Sie die Schritte 2 und 3 mehrmals (oder mehrmals).
  5. Jetzt ist Ihr wiederverwendbarer Code schwer zu verwenden und zu warten.

Was genau macht Code zu einem Albtraum? Es gibt verschiedene Aspekte, die Ihren Code problematisch machen können:


  1. Paketgröße und / oder Leistung: Nur mehr Code zum Ausführen auf Geräten kann zu einer schlechten Leistung führen. Manchmal kann dies dazu führen, dass Leute sich einfach weigern, Ihren Code zu verwenden.
  2. Schwierig zu warten: Früher hatte Ihr wiederverwendbarer Code nur wenige Optionen und konzentrierte sich darauf, eine Sache gut zu machen. Jetzt kann er eine Reihe verschiedener Dinge tun, und Sie müssen alles dokumentieren. Darüber hinaus werden Ihnen Fragen zur Verwendung Ihres Codes für bestimmte Anwendungsfälle gestellt, die möglicherweise mit Anwendungsfällen vergleichbar sind, für die Sie bereits Unterstützung hinzugefügt haben. Möglicherweise gibt es sogar zwei fast identische Anwendungsfälle, die sich geringfügig unterscheiden. Sie müssen daher Fragen dazu beantworten, was in einer bestimmten Situation am besten ist.
  3. Komplexität der Implementierung : Jedes Mal, if es sich nicht nur um eine if , besteht jeder Zweig der Logik Ihres Codes gleichzeitig mit den vorhandenen Logikzweigen. Tatsächlich sind Situationen möglich, in denen Sie versuchen, eine Kombination von Argumenten / Optionen / Requisiten beizubehalten, die noch niemand verwendet. Sie müssen jedoch alle möglichen Optionen in Betracht ziehen, da Sie nicht genau wissen, ob diese Kombinationen von jemandem verwendet werden oder nicht.
  4. Anspruchsvolle API : Jedes neue Argument / jede neue Option / jedes neue Requisit, das Sie zu Ihrem wiederverwendbaren Code hinzufügen, erschwert die Verwendung, da Sie jetzt über eine umfangreiche README-Datei oder eine Website verfügen, auf der alle verfügbaren Funktionen dokumentiert sind Ihr Code. Die Verwendung ist nicht bequem, da die Komplexität Ihrer API den Code des Entwicklers durchdringt, der sie verwendet, was den Code kompliziert.

Infolgedessen leidet jeder. Es ist erwähnenswert, dass die Umsetzung des endgültigen Programms ein wesentlicher Bestandteil der Entwicklung ist. Aber es wäre großartig, wenn wir uns mehr Gedanken über die Implementierung unserer Abstraktionen machen würden (siehe "AHA-Programmierung" ). Gibt es eine Möglichkeit, die Probleme mit wiederverwendbarem Code zu reduzieren und dennoch die Vorteile der Verwendung von Abstraktionen zu nutzen?


Kontrollieren Sie die Inversion


Die Umkehrung der Kontrolle ist ein Prinzip, das die Erstellung und Verwendung von Abstraktionen wirklich vereinfacht. Das sagt Wikipedia dazu:


... bei der herkömmlichen Programmierung wird Benutzercode, der den Zweck des Programms ausdrückt, in wiederverwendbaren Bibliotheken aufgerufen, um häufige Probleme zu lösen. Bei der Steuerungsumkehrung wird jedoch benutzerdefinierter oder aufgabenspezifischer Code aufgerufen.

Stellen Sie es sich folgendermaßen vor: "Reduzieren Sie die Funktionalität Ihrer Abstraktion und sorgen Sie dafür, dass Ihre Benutzer die von ihnen benötigte Funktionalität implementieren können." Das mag völlig absurd erscheinen, aber wir verwenden Abstraktionen, um komplexe und sich wiederholende Aufgaben zu verbergen und dadurch unseren Code „sauberer“ und „ordentlicher“ zu machen. Wie wir oben gesehen haben, vereinfachen traditionelle Abstraktionen den Code jedoch nicht immer.


Was ist die Inversion von Management im Code?


Als erstes ist hier ein sehr ausgeklügeltes Beispiel:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

Spielen wir nun einen typischen "Abstraktionslebenszyklus", indem wir dieser Abstraktion neue Anwendungsfälle hinzufügen und sie "sinnlos verbessern", um diese neuen Anwendungsfälle zu unterstützen:


 //   Array.prototype.filter   function filter( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if ( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ) { continue } newArray[newArray.length] = element } return newArray } filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterNull: false}) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterUndefined: false}) // [0, 1, 2, undefined, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterZero: true}) // [1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterEmptyString: true}) // [0, 1, 2, 3, 'four'] 

Unser Programm arbeitet also mit nur sechs Anwendungsfällen, aber tatsächlich unterstützen wir jede mögliche Kombination von Funktionen, und es gibt bis zu 25 solcher Kombinationen (wenn ich richtig gerechnet habe).


Im Allgemeinen ist dies eine ziemlich einfache Abstraktion. Aber es kann vereinfacht werden. Es kommt häufig vor, dass die Abstraktion, zu der neue Funktionen hinzugefügt wurden, für die tatsächlich unterstützten Anwendungsfälle erheblich vereinfacht werden kann. Sobald die Abstraktion etwas unterstützt (z. B. { filterZero: true, filterUndefined: false } ausführen), haben wir { filterZero: true, filterUndefined: false } Angst, diese Funktionalität zu entfernen, da der darauf basierende Code möglicherweise { filterZero: true, filterUndefined: false } .


Wir schreiben sogar Tests für Anwendungsfälle, die wir tatsächlich nicht haben, einfach weil unsere Abstraktion diese Szenarien unterstützt, und wir müssen dies möglicherweise in Zukunft tun. Und wenn diese oder andere Anwendungsfälle für uns unnötig werden, entfernen wir ihre Unterstützung nicht, da wir sie einfach vergessen oder glauben, dass sie für uns in Zukunft nützlich sein könnte oder wir einfach Angst haben, etwas zu zerbrechen.


Okay, schreiben wir nun eine ausführlichere Abstraktion für diese Funktion und wenden die Kontrollinversionsmethode an, um alle Anwendungsfälle zu unterstützen, die wir benötigen:


 //   Array.prototype.filter   function filter(array, filterFn) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (filterFn(element)) { newArray[newArray.length] = element } } return newArray } filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null) // [0, 1, 2, undefined, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== 0, ) // [1, 2, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== '', ) // [0, 1, 2, 3, 'four'] 

Großartig! Es ist viel einfacher geworden. Wir haben gerade die Kontrolle über die Funktion übertragen und die Verantwortung für die Entscheidung, welches Element in das neue Array fällt, von der filter auf die Funktion übertragen, die die Filterfunktion aufruft. Beachten Sie, dass die filter sich immer noch eine nützliche Abstraktion ist, aber jetzt viel flexibler.


Aber war die vorherige Version dieser Abstraktion so schlecht? Wahrscheinlich nicht. Aber seit wir die Kontrolle übernommen haben, können wir jetzt viel mehr einzigartige Anwendungsfälle unterstützen:


 filter( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], animal => animal.legs === 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

Stellen Sie sich vor, Sie müssten Unterstützung für diesen Anwendungsfall hinzufügen, ohne eine Steuerungsumkehrung anzuwenden. Ja, das wäre einfach lächerlich.


Schlechte API?


Eine der häufigsten Beschwerden, die ich von Leuten in Bezug auf APIs höre, die eine Inversion der Steuerung verwenden, ist: "Ja, aber jetzt ist es schwieriger zu verwenden als zuvor." Nehmen Sie dieses Beispiel:


 //  filter([0, 1, undefined, 2, null, 3, 'four', '']) //  filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) 

Ja, eine der Optionen ist eindeutig einfacher zu bedienen als die andere. Einer der Vorteile der Steuerelementinversion ist jedoch, dass Sie eine API verwenden können, die die Steuerelementinversion verwendet, um Ihre alte API erneut zu implementieren. Das ist normalerweise ziemlich einfach. Z.B:


 function filterWithOptions( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { return filter( array, element => !( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ), ) } 

Cool, was? Auf diese Weise können wir Abstraktionen über der API erstellen, in der die Steuerelementinversion angewendet wird, und dadurch eine einfachere API erstellen. Und wenn es in unserer "einfacheren" API nicht genügend Anwendungsfälle gibt, können unsere Benutzer dieselben Bausteine ​​verwenden, mit denen wir unsere High-Level-API erstellt haben, um Lösungen für komplexere Aufgaben zu entwickeln. Sie müssen uns nicht bitten, filterWithOptions eine neue Funktion filterWithOptions und auf die Implementierung zu warten. Sie verfügen bereits über Tools, mit denen sie die benötigten Zusatzfunktionen eigenständig entwickeln können.


Und nur für den Fan:


 function filterByLegCount(array, legCount) { return filter(array, animal => animal.legs === legCount) } filterByLegCount( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

Sie können spezielle Funktionen für jede Situation erstellen, die bei Ihnen häufig auftritt.


Beispiele aus der Praxis


Das funktioniert also in einfachen Fällen, aber ist dieses Konzept für das wirkliche Leben geeignet? Höchstwahrscheinlich verwenden Sie ständig die Kontrollinversion. Beispielsweise wendet die Funktion Array.prototype.filter die umgekehrte Steuerung an. Wie die Funktion Array.prototype.map .


Es gibt verschiedene Muster, mit denen Sie möglicherweise bereits vertraut sind und die nur eine Form der Kontrollinversion darstellen.


Hier sind zwei meiner Lieblingsmuster, die diese zeigen: "Compound Components" und "State Reducers" . Nachfolgend finden Sie kurze Beispiele, wie diese Muster angewendet werden können.


Zusammengesetzte Komponenten


Angenommen, Sie möchten eine Menu mit einer Schaltfläche zum Öffnen eines Menüs und einer Liste von Menüelementen erstellen, die angezeigt werden, wenn Sie auf die Schaltfläche klicken. Wenn das Objekt ausgewählt ist, führt es eine Aktion aus. Um dies zu implementieren, erstellen sie normalerweise einfach Requisiten:


 function App() { return ( <Menu buttonContents={ <> Actions <span aria-hidden></span> </> } items={[ {contents: 'Download', onSelect: () => alert('Download')}, {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')}, {contents: 'Delete', onSelect: () => alert('Delete')}, ]} /> ) } 

Dies ermöglicht es uns, viele Dinge in den Menüpunkten zu konfigurieren. Was aber, wenn wir vor dem Menüpunkt Löschen eine Zeile einfügen wollen? Sollten wir Objekten, die sich auf Gegenstände beziehen, eine spezielle Option hinzufügen? Nun, ich weiß es nicht, zum Beispiel: precedeWithLine ? So lala Idee.


Erstellen Sie möglicherweise einen speziellen Menüeintrag, z. B. {contents: <hr />} . Ich denke, es würde funktionieren, aber dann müssten wir Fälle behandeln, in denen es kein onSelect . Und um ehrlich zu sein, ist dies eine sehr umständliche API.


Wenn Sie überlegen, wie Sie eine gute API für Benutzer erstellen, die versuchen, etwas anderes zu tun, anstatt nach der if Anweisung zu greifen, versuchen Sie, das Steuerelement zu invertieren. Was ist, wenn wir die Verantwortung für die Menüvisualisierung auf den Benutzer übertragen? Wir nutzen eine der Stärken der Reaktion:


 function App() { return ( <Menu> <MenuButton> Actions <span aria-hidden></span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert('Download')}>Download</MenuItem> <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem> <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem> </MenuList> </Menu> ) } 

Ein wichtiger Punkt, auf den Sie achten sollten, ist, dass für den Benutzer kein Zustand der Komponenten sichtbar ist. Der Status wird implizit zwischen diesen Komponenten geteilt. Dies ist der Kernwert des Komponentenmusters. Bei dieser Gelegenheit haben wir dem Benutzer unserer Komponenten eine gewisse Kontrolle über das Rendern gegeben. Jetzt ist das Hinzufügen einer zusätzlichen Zeile (oder etwas anderem) eine einfache und intuitive Aktion. Keine zusätzliche Dokumentation, keine zusätzlichen Funktionen, kein zusätzlicher Code oder Tests. Jeder gewinnt.


Sie können hier mehr über dieses Muster lesen. Vielen Dank an Ryan Florence , der mir das beigebracht hat.


Zustandsreduzierer


Ich habe mir dieses Muster ausgedacht, um das Problem der Einstellung der Komponentenlogik zu lösen. Sie können mehr über diese Situation in meinem Blog , The State Reducer Pattern , lesen, aber der wichtigste Punkt ist, dass ich eine Such- / Autovervollständigungs- / Typ-In-Bibliothek namens Downshift und einer der Benutzer der Bibliothek eine Multi-Choice-Komponentenversion entwickelte Aus diesem Grund wollte er, dass das Menü auch nach Auswahl eines Elements geöffnet bleibt.


Die Logik hinter dem Downshift schlug vor, dass das Menü geschlossen werden sollte, nachdem die Auswahl getroffen wurde. Ein Bibliotheksbenutzer, der seine Funktionalität ändern musste, schlug vor, prop closeOnSelection . Ich lehnte dieses Angebot ab, da ich den Weg zum Propalaps bereits gegangen war und es vermeiden wollte.


Stattdessen habe ich die API so gestaltet, dass Benutzer selbst steuern können, wie Statusänderungen auftreten. Stellen Sie sich die Statusreduzierung als den Status einer Funktion vor, die jedes Mal aufgerufen wird, wenn sich der Status der Komponente ändert, und dem Anwendungsentwickler die Möglichkeit gibt, die bevorstehende Statusänderung zu beeinflussen.


Ein Beispiel für die Verwendung der Downshift Bibliothek, damit das Menü nicht geschlossen wird, nachdem der Benutzer auf das ausgewählte Element geklickt hat:


 function stateReducer(state, changes) { switch (changes.type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes, //     Downshift   //       isOpen  highlightedIndex //      isOpen: state.isOpen, highlightedIndex: state.highlightedIndex, } default: return changes } } // ,   // <Downshift stateReducer={stateReducer} {...restOfTheProps} /> 

Nachdem wir diese Requisite hinzugefügt hatten, erhielten wir VIEL weniger Anfragen zum Hinzufügen neuer Einstellungen für diese Komponente. Die Komponente ist flexibler geworden und es ist für Entwickler einfacher geworden, sie nach Bedarf zu konfigurieren.


Requisiten rendern


Erwähnenswert ist das Muster "Render Requisiten" . Dieses Muster ist ein ideales Beispiel für die Verwendung der Umkehrung der Kontrolle, aber wir brauchen es insbesondere nicht. Lesen Sie hier mehr darüber: Warum brauchen wir nicht mehr so ​​viele Render-Requisiten?


Warnung


Die Umkehrung der Kontrolle ist eine großartige Möglichkeit, um das Problem der falschen Vorstellung darüber, wie Ihr Code in Zukunft verwendet wird, zu umgehen. Aber bevor ich fertig bin, möchte ich dir einen Rat geben.


Kommen wir zu unserem weit hergeholten Beispiel zurück:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

Was ist, wenn dies alles ist, was wir von der filter benötigen? Und wir waren nie mit einer Situation konfrontiert, in der wir irgendetwas außer null und undefined filtern mussten? In diesem Fall würde das Hinzufügen einer Steuerelementinversion für einen einzelnen Anwendungsfall den Code einfach komplizieren und nicht viel Nutzen bringen.


Wenden Sie wie bei jeder Abstraktion das Prinzip der AHA-Programmierung an und vermeiden Sie voreilige Abstraktionen!


Schlussfolgerungen


Ich hoffe, der Artikel hat Ihnen weitergeholfen. Ich habe gezeigt, wie Sie das Konzept der Kontrollinversion in einer Reaktion anwenden können. Dieses Konzept gilt natürlich nicht nur für React (wie wir mit der filter ). if Sie das nächste Mal feststellen, dass Sie der coreBusinessLogic Funktion Ihrer Anwendung eine weitere if hinzufügen, coreBusinessLogic Sie, wie Sie das Steuerelement invertieren und die Logik dahin übertragen können, wo sie verwendet wird Sonderfall).


Wenn Sie möchten, können Sie mit einem Beispiel aus einem Artikel in CodeSandbox spielen .


Viel Glück und vielen Dank für Ihre Aufmerksamkeit!


PS. Wenn dir dieser Artikel gefallen hat, könnte dir dieser Vortrag gefallen : youtube Kent C Dodds - Simply React

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


All Articles