Mein Rechen: von Lumpen zu Reichtum

Hintergrund


Ich arbeite jetzt seit einem Jahr als Front-End-Entwickler. Mein erstes Projekt war ein "feindliches" Backend. Es kommt vor, dass dies kein großes Problem ist, wenn die Kommunikation hergestellt wird.


Aber in unserem Fall war es nicht so.


Wir haben den Code entwickelt, der darauf beruhte, dass das Backend uns bestimmte Daten, eine bestimmte Struktur und ein bestimmtes Format sendet. Während das Backend es für normal hielt, den Inhalt der Antworten zu ändern - ohne Vorwarnung. Infolgedessen haben wir Stunden gebraucht, um festzustellen, warum ein bestimmter Teil der Website nicht mehr funktioniert.


Wir haben festgestellt, dass wir überprüfen müssen, was das Backend zurückgibt, bevor wir uns auf die Daten verlassen können, die es an uns gesendet hat. Wir haben eine Forschungsaufgabe zum Thema Datenvalidierung vom Frontend aus erstellt.


Diese Studie wurde mir in Auftrag gegeben.


Ich habe eine Liste erstellt, was ich in dem Tool sein möchte, das ich für die Datenvalidierung verwenden möchte.


Die wichtigsten Auswahlpunkte waren folgende Punkte:


  • deklarative Beschreibung (Schema) der Validierung, die in eine Validierungsfunktion umgewandelt wird, die true / false zurückgibt (gültig, ungültig)
  • niedrige Eintrittsschwelle;
  • Ähnlichkeit validierter Daten mit einer Beschreibung der Validierung;
  • einfache Integration von benutzerdefinierten Validierungen;
  • Einfache Integration von benutzerdefinierten Fehlermeldungen.

Als Ergebnis fand ich viele Validierungsbibliotheken, nachdem ich das TOP-5 (ajv, joi, roi ...) überprüft hatte. Sie sind alle sehr gut. Aber es schien mir, dass zur Lösung von 5% der komplexen Fälle 95% der häufigsten Fälle dazu verurteilt waren, ziemlich ausführlich und umständlich zu sein.


Deshalb dachte ich: Warum nicht selbst etwas entwickeln, das zu mir passt?
Vier Monate später erschien die siebte Version meiner Quartett- Validierungsbibliothek.
Es war eine stabile Version, vollständig getestet, 11k Downloads auf npm. Wir haben es drei Monate lang für drei Projekte in einer Kampagne verwendet.


Diese drei Monate haben eine sehr nützliche Rolle gespielt. Quartett hat alle seine Vorteile gezeigt. Es gibt keine Datenprobleme vom Backend. Jedes Mal, wenn sie die Antwort änderten, warfen wir sofort einen Fehler. Die Zeit, die für die Suche nach den Ursachen von Fehlern aufgewendet wurde, hat sich dramatisch verringert. Es gibt praktisch keine Datenfehler mehr.


Es wurden aber auch Mängel festgestellt.


Aus diesem Grund habe ich beschlossen, sie zu analysieren und eine neue Version mit Korrekturen aller Fehler zu veröffentlichen, die während der Entwicklung gemacht wurden.
Ich werde im Folgenden auf diese Architekturfehler und ihre Lösungen eingehen.


Architektonischer Rechen


"Stroko" - eine typische Sprache des Schemas


Ich werde ein Beispiel der alten Version des Schemas für das Objekt der Person geben.


const personSchema = { name: 'string', age: 'number', linkedin: ['string', 'null'] } 

Dieses Schema überprüft ein Objekt mit drei Eigenschaften: Name - muss eine Zeichenfolge sein, Alter - muss eine Zahl sein, eine Verknüpfung zu einem Konto in LinkedIn - muss entweder null (wenn kein Konto vorhanden ist) oder Zeichenfolge (wenn ein Konto vorhanden ist) sein.


Dieses Schema erfüllt meine Anforderungen an Lesbarkeit und Ähnlichkeit mit validierten Daten, und ich denke, dass die Schwelle für den Einstieg in das Erlernen des Schreibens solcher Schemata nicht hoch ist. Darüber hinaus kann ein solches Schema leicht mit einer Typdefinition in Typoskript geschrieben werden:


 type Person = { name: string age: number linkedin: string | null } 

(Wie Sie sehen können - die Änderungen sind eher kosmetischer Natur)


Wenn ich eine Entscheidung getroffen habe, was für die häufigsten Validierungsoptionen verwendet werden soll (zum Beispiel die oben verwendeten). Ich entschied mich für - Strings, sozusagen die Namen von Validatoren.


Das Problem mit Zeichenfolgen ist jedoch, dass sie dem Compiler oder Fehleranalysator nicht zur Verfügung stehen. Die Zeichenfolge 'Nummer' unterscheidet sich nicht wesentlich von 'Nummer'.


Lösung


Die neue Version des Quartetts 8.0.0. Ich beschloss, aus dem Quartett zu entfernen - die Verwendung von Strings als Namen von Validatoren innerhalb des Schemas.


Das Diagramm sieht jetzt so aus:


 const personSchema = { name: v.string age: v.number, linkedin: [v.string, null] } 

Diese Änderung hat zwei große Vorteile:


  • Compiler oder Fehleranalysatoren - können erkennen, dass der Methodenname mit einem Fehler geschrieben wurde.
  • Linien - werden nicht mehr als Schemaelement verwendet. Dies bedeutet, dass Sie für sie neue Funktionen in der Bibliothek auswählen können, die im Folgenden beschrieben werden.

TypeScript-Unterstützung


Im Allgemeinen wurden die ersten sieben Versionen in reinem Javascript entwickelt. Beim Wechsel zu einem Projekt mit Typescript musste die Bibliothek irgendwie angepasst werden. Daher wurden Typdeklarationen für die Bibliothek geschrieben.


Dies war jedoch ein Minus - beim Hinzufügen von Funktionen oder beim Ändern einiger Elemente der Bibliothek war es immer leicht zu vergessen, die Typdeklarationen zu aktualisieren.


Es gab auch nur geringfügige Unannehmlichkeiten dieser Art:


 const checkPerson = v(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

Als wir den Objektvalidator in Zeile (0) erstellt haben. Wir möchten nach Überprüfung der tatsächlichen Antwort aus dem Backend in Zeile (1) und Behandlung des Fehlers. In Zeile (2), damit diese person Typ Person ist. Dies ist jedoch nicht geschehen. Leider war eine solche Überprüfung kein Typwächter.


Lösung


Ich beschloss, die gesamte Quartettbibliothek in Typescript umzuschreiben, damit der Compiler die Korrespondenz der Bibliothek mit den Typen überprüfen konnte. Unterwegs fügen wir der Funktion, die den kompilierten Validator zurückgibt, einen Typparameter hinzu, der bestimmen würde, welcher Typschutz dieser Validatortyp ist.


Ein Beispiel sieht so aus:


 const checkPerson = v<Person>(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

Jetzt ist in Zeile (2) person vom Typ Person .


Lesbarkeit


Es gab auch zwei Fälle, in denen der Code schlecht gelesen wurde: Überprüfung der Einhaltung eines bestimmten Wertesatzes (Überprüfung von Aufzählungen) und Überprüfung anderer Eigenschaften des Objekts.


a) Überprüfen Sie die Aufzählungen
Anfangs gab es eine Idee, meiner Meinung nach eine gute. Wir werden es demonstrieren, indem wir unserem Objekt das Feld "Geschlecht" hinzufügen.
Die alte Version der Schaltung sah folgendermaßen aus:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum('male', 'female') } 

Die Option ist sehr gut lesbar. Aber wie immer lief alles ein wenig vom Plan ab.
Mit einer deklarierten Aufzählung im Programm, zum Beispiel:


 enum Sex { Male = 'male', Female = 'female' } 

Natürlich möchte ich es innerhalb der Schaltung verwenden. Wenn Sie also einen der Werte ändern (z. B. 'männlich' -> 'm', 'weiblich' -> 'f'), sollte sich auch das Validierungsschema ändern.


Daher wurde die Enum-Validierung fast immer folgendermaßen geschrieben:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)) } 

Welches sieht ziemlich sperrig aus.


b) Validierung der Resteigenschaften des Objekts


Angenommen, wir fügen unserem Objekt ein solches Merkmal hinzu - es kann zusätzliche Felder enthalten, aber alle müssen Links zu sozialen Netzwerken sein - das heißt, sie müssen entweder null oder eine Zeichenfolge sein.


Das alte Schema würde so aussehen:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)), ...v.rest(['null', 'string']) // Rest props are string | null } 

In diesem Eintrag wurden die verbleibenden Eigenschaften hervorgehoben - von den bereits aufgelisteten. Die Verwendung eines Spread-Operators verwirrt eher eine Person, die dieses Schema verstehen möchte.


Lösung


Wie oben beschrieben, sind Zeichenfolgen nicht mehr Teil von Validierungsschemata. Nur drei Arten von Javascript-Werten blieben Validierungsschema. Objekt - um das Validierungsschema des Objekts zu beschreiben. Array zur Beschreibung - verschiedene Optionen für die Gültigkeit. Funktion (Bibliothek generiert oder benutzerdefiniert) - für alle anderen Validierungsoptionen.


Diese Bestimmung ermöglichte das Hinzufügen von Funktionen, wodurch die Lesbarkeit der Schaltung um ein Vielfaches erhöht werden konnte.


Was ist, wenn wir den Wert mit der Zeichenfolge 'male' vergleichen möchten? Müssen wir wirklich etwas anderes wissen als den Wert selbst und die Zeichenfolge 'männlich'?


Daher wurde beschlossen, die Werte primitiver Typen als Element der Schaltung hinzuzufügen. Wenn Sie also einen primitiven Wert im Schema treffen, bedeutet dies, dass dies der gültige Wert ist, den der nach diesem Schema erstellte Validator überprüfen sollte. Ich gebe besser ein Beispiel:


Wenn wir die Zahl auf Gleichheit überprüfen müssen 42-mind. Dann schreiben wir es so:


 const check42 = v(42) check42(42) // => true check42(41) // => false check42(43) // => false check42('42') // => false 

Mal sehen, wie sich dies auf das Personenschema auswirkt (ohne zusätzliche Eigenschaften zu berücksichtigen):


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], // null is primitive value sex: ['male', 'female'] // 'male', 'female' are primitive values } 

Mit vordefinierten Aufzählungen können wir es folgendermaßen umschreiben:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex) // same as ['male', 'female'] } 

In diesem Fall wurde unnötige Zeremonialität in Form der Verwendung der Enum-Methode und der Verwendung des Spread-Operators entfernt, um gültige Werte aus dem Objekt als Parameter in diese Methode einzufügen.


Was als primitiver Wert angesehen wird: Zahlen, Zeichenfolgen, Zeichen, true , false , null und undefined .


Das heißt, wenn wir den Wert mit ihnen vergleichen müssen, verwenden wir diese Werte einfach selbst. Und eine Validierungsbibliothek - sie erstellt einen Validator, der den Wert streng mit den im Schema angegebenen vergleicht.


Um verbleibende Eigenschaften zu validieren, wurde ausgewählt, eine spezielle Eigenschaft für alle anderen Felder des Objekts zu verwenden:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex), [v.rest]: [null, v.string] } 

Somit sieht die Schaltung besser lesbar aus. Und eher wie Typescript-Anzeigen.


Der Validator bezieht sich auf die Funktion, die ihn erstellt hat


In älteren Versionen waren Fehlererklärungen nicht Teil des Validators. Sie wurden einem Array innerhalb der v Funktion hinzugefügt.


Zuvor mussten Sie einen Validator bei sich haben (um dies zu überprüfen) und v (um eine Erklärung der Invalidität zu erhalten), um eine Erklärung für Validierungsfehler zu erhalten. All dies sah wie folgt aus:

a) Wir fügen dem Diagramm Erklärungen hinzu


 const checkPerson = v({ name: v('string', 'wrong name') age: v('number', 'wrong age'), linkedin: v(['null', 'string'], 'wrong linkedin'), sex: v( v.enum(...Object.values(Sex)), 'wrong sex value' ), ...v.rest( v( ['null', 'string'], 'wrong social networks link' ) ) // Rest props are string | null }) 

Zu jedem Element der Schaltung können Sie mithilfe des zweiten Arguments der v-Compiler-Funktion eine Erklärung des Fehlers hinzufügen.


b) Löschen Sie das Erklärungsarray


Vor der Validierung musste dieses globale Array gelöscht werden, in dem alle Erklärungen während der Validierung aufgezeichnet wurden.


 v.clearContext() // same as v.explanations = [] 

c) Validieren


 const isPersonValid = checkPerson(person) 

Wenn bei dieser Überprüfung eine Gültigkeit gefunden wurde und in der Phase der Erstellung der Schaltung eine Erklärung gegeben wurde, wird diese Erklärung in das globale Array v.explanation .


d) Fehlerbehandlung


 if (!isPersonValid) { throw new TypeError('Invalid person response: ' + v.explanation.join('; ')) } // ex. Throws 'Invalid person response: wrong name; wrong age' 

Wie Sie hier sehen können, gibt es ein großes Problem. Denn wenn wir den Validator nicht anstelle seiner Erstellung verwenden wollen. Wir müssen es nicht nur an die Parameter übergeben, sondern auch an die Funktion, die es erstellt hat. Weil sich darin das Array befindet, in dem die Erklärungen hinzugefügt werden.


Lösung


Dieses Problem wurde wie folgt gelöst: Erklärungen wurden Teil der Validierungsfunktion selbst. Was kann aus seiner Art verstanden werden:
Typ Validator = (Wert: any, Erklärungen ?: any []) => boolean


Wenn Sie nun eine Erklärung des Fehlers benötigen, übergeben Sie das Array, in das Sie die Erklärung einfügen möchten.


Somit wird der Validator zu einer unabhängigen Einheit. Es wurde auch eine Methode hinzugefügt, die die Validierungsfunktion in eine Funktion umwandeln kann, die null zurückgibt, wenn der Wert gültig ist, und eine Reihe von Erklärungen zurückgibt, wenn der Wert ungültig ist.


Nun sieht die Validierung mit Erklärungen folgendermaßen aus:


 const checkPerson = v<Person>({ name: v(v.string, 'wrong name'), age: v(v.number, 'wrong age'), linkedin: v([null, v.string], 'wrong linkedin') sex: v(Object.values(Sex), 'wrong sex') [v.rest]: v([null, v.string], 'wrong social network') }) // ... const explanations = [] if (!checkPerson(person, explanation)) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } // OR const getExplanation = v.explain(checkPerson) const explanations = getExplanation(person) if (explanations) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } 

Nachwort


Ich habe drei Prämissen hervorgehoben, aufgrund derer ich alles neu schreiben musste:


  • Die Hoffnung, dass sich Menschen beim Schreiben von Zeilen nicht irren
  • Verwenden globaler Variablen (in diesem Fall v.explanation-Array)
  • Tests mit kleinen Beispielen während der Entwicklung - zeigten nicht die Probleme, die bei der Verwendung in wirklich großen Fällen auftreten.

Ich bin jedoch froh, dass ich diese Probleme analysiert habe und die veröffentlichte Version bereits in unserem Projekt verwendet wird. Und ich hoffe, es wird uns nicht weniger nützlich sein als das vorherige.


Vielen Dank für das Lesen, ich hoffe, meine Erfahrung wird Ihnen nützlich sein.

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


All Articles