Formentwicklung reagieren. KISS, YAGNI, DRY Prinzipien in der Praxis

Hallo, in diesem Tutorial werden wir uns ansehen, wie man in React eine sehr einfache, aber kontrollierte Form entwickelt, die sich auf die Qualität des Codes konzentriert.

Bei der Entwicklung unserer Form folgen wir den Prinzipien von „KISS“, „YAGNI“, „DRY“. Um dieses Tutorial erfolgreich abzuschließen, müssen Sie diese Prinzipien nicht kennen, ich werde sie auf dem Weg erklären. Ich glaube jedoch, dass Sie das moderne Javascript gut beherrschen und in React denken können .



Tutorial Struktur:







Zur Sache! Schreiben eines einfachen Formulars mit KISS und YAGNI


Stellen wir uns also vor, wir haben die Aufgabe, ein Autorisierungsformular zu implementieren:
Code kopieren
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



Wir beginnen unsere Entwicklung mit der Analyse der Prinzipien von KISS und YAGNI, wobei wir den Rest der Prinzipien vorübergehend vergessen.

KISS - "Lass den Code einfach und dumm." Ich denke, Sie kennen das Konzept des einfachen Codes. Aber was bedeutet "dummer" Code? Nach meinem Verständnis ist dies Code, der das Problem mit der minimalen Anzahl von Abstraktionen löst, während die Verschachtelung dieser Abstraktionen ineinander ebenfalls minimal ist.

YAGNI - "Du wirst es nicht brauchen." Der Code sollte nur das tun können, wofür er geschrieben wurde. Wir erstellen keine Funktionen, die später benötigt werden oder die die Anwendung unserer Meinung nach verbessern. Wir tun nur das, was speziell für die Umsetzung der Aufgabe benötigt wird.

Befolgen wir diese Grundsätze genau, aber berücksichtigen wir auch:

  • initialDataund onSubmitfür LogInFormkommt von oben (dies ist eine nützliche Technik, insbesondere wenn das Formular verarbeitet werden kann createund updategleichzeitig)
  • muss für jedes Feld haben label

Lassen Sie uns außerdem die Stilisierung verpassen, da sie für uns nicht interessant und gültig ist, da dies ein Thema für ein separates Tutorial ist.

Bitte implementieren Sie das Formular selbst nach den oben beschriebenen Grundsätzen.

Meine Formularimplementierung
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Enter your nickname
        <input
          value={logInData.nickname}
          onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
        />
      </label>
      <label>
        Enter your email
        <input
          type="email"
          value={logInData.email}
          onChange={e => setLogInData({ ...logInData, email: e.target.value })}
        />
      </label>
      <label>
        Enter your password
        <input
          type="password"
          value={logInData.password}
          onChange={e => setLogInData({ ...logInData, password: e.target.value })}
        />
      </label>
      <button>Submit</button>
    </form>
  );
};
    






Ihre Entscheidung ist höchstwahrscheinlich etwas anders, weil jeder Entwickler anders denkt. Sie können für jedes Feld einen eigenen Status erstellen oder Handlerfunktionen in einer separaten Variablen platzieren. Dies ist in jedem Fall nicht wichtig.

Wenn Sie jedoch sofort eine Handlerfunktion für alle Felder erstellt haben, ist dies nicht mehr die „dümmste“ Lösung für das Problem. In dieser Phase ist es unser Ziel, den einfachsten und "dümmsten" Code zu erstellen, damit wir später das gesamte Bild betrachten und die beste Abstraktion auswählen können.

Wenn Sie genau den gleichen Code haben, ist das cool und bedeutet, dass unser Denken konvergiert!

Weiter werden wir mit diesem Code arbeiten. Es ist einfach, aber alles andere als ideal.




Refactoring und TROCKEN


Es ist Zeit, sich mit dem DRY-Prinzip zu befassen.

DRY, vereinfachte Formulierung - "Duplizieren Sie Ihren Code nicht." Das Prinzip scheint einfach zu sein, hat aber einen Haken: Um die Codeduplizierung zu beseitigen, müssen Sie Abstraktionen erstellen. Wenn diese Abstraktionen nicht gut genug sind, werden wir das KISS-Prinzip verletzen.

Es ist auch wichtig zu verstehen, dass DRY nicht benötigt wird, um Code schneller zu schreiben. Ihre Aufgabe ist es, das Lesen und die Unterstützung unserer Lösung zu vereinfachen. Beeilen Sie sich daher nicht, sofort Abstraktionen zu erstellen. Es ist besser, einen Teil des Codes einfach zu implementieren und dann zu analysieren, welche Abstraktionen Sie erstellen müssen, um das Lesen des Codes zu vereinfachen und die Anzahl der Stellen für Änderungen zu verringern, wenn sie benötigt werden.

Checkliste der korrekten Abstraktion:
  • Der Name der Abstraktion entspricht voll und ganz ihrem Zweck
  • Die Abstraktion erfüllt eine bestimmte, verständliche Aufgabe
  • Das Lesen des Codes, aus dem die Abstraktion extrahiert wurde, wurde verbessert

Kommen wir also zum Refactoring.
Und wir haben doppelten Code ausgesprochen:
Code kopieren
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Dieser Code dupliziert eine Zusammensetzung von 2 Elementen: label, input. Lassen Sie uns sie zu einer neuen Abstraktion zusammenführen InputField:
Code kopieren
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Jetzt LogInFormsieht unsere so aus:
Code kopieren
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={e => setLogInData({ ...logInData, email: e.target.value })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={e => setLogInData({ ...logInData, password: e.target.value })}
      />
      <button>Submit</button>
    </form>
  );
};



Das Lesen ist einfacher geworden. Der Name der Abstraktion entspricht der Aufgabe, die sie löst. Der Zweck der Komponente liegt auf der Hand. Der Code ist weniger geworden. Wir gehen also in die richtige Richtung!

Jetzt ist klar, dass die InputField.onChangeLogik dupliziert ist.
Was dort passiert, kann in zwei Phasen unterteilt werden:

Code kopieren
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



Die erste Funktion beschreibt die Details zum Abrufen des Werts aus dem Ereignis input. Wir haben die Wahl zwischen zwei Abstraktionen, in denen wir diese Logik speichern können: InputFieldund LogInForm.

Um richtig zu bestimmen, auf welche der Abstraktionen wir unseren Code verweisen müssen, müssen wir uns der vollständigen Formulierung des DRY-Prinzips zuwenden: „Jeder Teil des Wissens sollte eine eindeutige, konsistente und maßgebliche Darstellung innerhalb des Systems haben.“

Ein Teil des Wissens im konkreten Beispiel besteht darin, zu wissen, wie der Wert des Ereignisses in ermittelt werden kann input. Wenn wir dieses Wissen in unserem speichern LogInForm, dann ist es offensichtlich, dass bei der Verwendung unsererInputFieldIn einer anderen Form müssen wir unser Wissen duplizieren oder in eine separate Abstraktion bringen und von dort aus verwenden. Und basierend auf dem KISS-Prinzip sollten wir die minimal mögliche Anzahl von Abstraktionen haben. In der Tat, warum müssen wir eine weitere Abstraktion erstellen, wenn wir diese Logik einfach in unsere InputFieldeinfügen können und der externe Code nichts darüber weiß, wie die Eingabe im Inneren funktioniert InputField. Es wird einfach eine vorgefertigte Bedeutung annehmen, genauso wie es nach innen geht.

Wenn Sie sich nicht sicher sind, ob Sie in Zukunft eine Veranstaltung benötigen, denken Sie an das YAGNI-Prinzip. Sie können onChangeEventunserer Komponente jederzeit eine zusätzliche Requisite hinzufügen InputField.

Bis dahin wird es InputFieldso aussehen:
Code kopieren
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



Somit wird eine Einheitlichkeit des Typs während der Eingabe und Ausgabe in die Komponente beobachtet und die wahre Natur dessen, was für den externen Code geschieht, wird verborgen. Wenn wir in Zukunft eine andere UI-Komponente benötigen, z. B. ein Kontrollkästchen oder eine Auswahl, werden wir auch die Typgleichmäßigkeit bei der Eingabe / Ausgabe beibehalten.

Dieser Ansatz bietet uns zusätzliche Flexibilität, da unser Formular mit allen Eingabe- / Ausgabequellen arbeiten kann, ohne dass für jede Quelle zusätzliche eindeutige Handler erstellt werden müssen.

Dieser heuristische Trick ist in vielen Frameworks standardmäßig integriert. Zum Beispiel ist dies die Hauptidee v-modelin Vuedem viele Menschen für seine einfache Liebe mit Arbeitsformen.

Kehren wir zum Geschäft zurück und aktualisieren Sie unsere Komponente LogInFormentsprechend den Änderungen in InputField:
Code kopieren
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={nickname => setLogInData({ ...logInData, nickname })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={email => setLogInData({ ...logInData, email })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={password => setLogInData({ ...logInData, password })}
      />
      <button>Submit</button>
    </form>
  );
};



Es sieht schon ziemlich gut aus, aber wir können es noch besser machen!

Callbackdas, was übergeben wird, macht onChangeimmer das gleiche. Darin wird nur der Schlüssel geändert: Passwort, E-Mail, Spitzname. Wir können es also durch einen Aufruf der Funktion ersetzen : handleChange('password').

Lassen Sie uns diese Funktion implementieren:
Code kopieren
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



Wie Sie sehen können, empfängt die Funktion das Argument und speichert es beim Schließen des Handlers, und der Handler kehrt sofort zum externen Code zurück. Funktionen, die nacheinander Argumente für einen Aufruf annehmen, werden auch als Curry bezeichnet.

Schauen wir uns den resultierenden Code an:
Code kopieren
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    const handleChange = fieldName => fieldValue => {
      setLogInData({
        ...logInData,
        [fieldName]: fieldValue,
      });
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };
  
  // InputField.js
  const InputField = ({ type, label, value, onChange }) => (
    <label>
      {label}
      <input type={type} value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );



Dieser Code ist eine kurze, präzise und mäßig deklarative Implementierung der Aufgabe. Ein weiteres Refactoring im Rahmen der Aufgabe ist meiner Meinung nach nicht sinnvoll.




Was kann man noch tun?



Wenn das Projekt viele Formulare enthält, können Sie die handleChange-Berechnung in einen separaten Hook einfügen useFieldChange:
Code kopieren
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



Da dies eine reine Funktion ist (dh beim ersten Aufruf, da immer dieselbe Funktion zurückgegeben wird), muss es sich nicht um einen Hook handeln. Aber der Haken sieht konzeptionell nach einer korrekteren und natürlicheren Lösung für React aus.

Sie können auch hinzufügen , callbackin-Place - Unterstützung fieldValuevollständig das übliche Verhalten nachzubilden setStateaus React:
Code kopieren
  const isFunc = val => typeof val === "function";

  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: isFunc(fieldValue) ? fieldValue(state[fieldName]) : fieldValue,
    }));
  };



Ein Beispiel für die Verwendung mit unserem Formular:
Code kopieren
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
    const handleChange = useFieldChange(setLogInData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };



Wenn Ihre Bewerbung jedoch nur ein Formular enthält, müssen Sie dies nicht alles tun! Weil es dem YAGNI-Prinzip widerspricht, wonach wir nicht das tun sollten, was wir nicht brauchen, um ein bestimmtes Problem zu lösen. Wenn Sie nur eine Form haben, reichen die tatsächlichen Vorteile solcher Bewegungen nicht aus. Immerhin haben wir den Code unserer Komponente nur um 3 Zeilen reduziert, aber eine zusätzliche Abstraktion eingeführt, die bestimmte Logik der Form verbirgt, die besser auf der Oberfläche zu halten ist.




Oh nein! Bitte nicht!



Geformte Konfigurationsformulare


Gesperrte Konfigurationen sind wie eine Webpack-Konfiguration, nur für Formulare.

Schauen Sie sich diesen Code mit einem Beispiel an:
Code kopieren
  const Form = () => (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={state.nickname}
        onChange={handleChange('nickname')}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={state.email}
        onChange={handleChange('email')}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={state.password}
        onChange={handleChange('password')}
      />
      <button>Submit</button>
    </form>
  );



Einige scheinen der Meinung zu sein, dass der Code hier dupliziert wird, da wir dieselbe InputField-Komponente aufrufen und dort dieselben Label-, Wert- und onChange-Parameter übergeben. Und sie fangen an, ihren eigenen Code zu trocknen, um imaginäre Duplikate zu vermeiden.
Oft machen sie das so:
Code kopieren
const fields = [
  {
    name: 'nickname',
    label: 'Enter your nickname',
  },
  {
    type: 'email',
    name: 'email',
    label: 'Enter your email',
  },
  {
    type: 'password',
    name: 'password',
    label: 'Enter your password',
  },
];

const Form = () => (
  <form onSubmit={handleSubmit}>
    {fields.map(({ type, name, label }) => (
      <InputField
        type={type}
        label={label}
        value={state[name]}
        onChange={handleChange(name)}
      />
    ))}
    <button>Submit</button>
  </form>
);



Als Ergebnis erhalten wir mit 17 Zeilen jsx-Code 16 Zeilen der Konfiguration. Bravo! Das verstehe ich DRY. Wenn wir hier 100 dieser Eingänge haben, erhalten wir 605 bzw. 506 Zeilen.

Infolgedessen haben wir jedoch komplexeren Code erhalten, der gegen das KISS-Prinzip verstößt. Immerhin besteht es jetzt aus 2 Abstraktionen: Feldern und einem Algorithmus (ja, ein Algorithmus ist auch eine Abstraktion), der ihn in einen Baum von React-Elementen verwandelt. Wenn wir diesen Code lesen, müssen wir ständig zwischen diesen Abstraktionen wechseln.

Das größte Problem bei diesem Code ist jedoch die Unterstützung und Erweiterung.
Stellen Sie sich vor, was mit ihm passieren wird, wenn:
  • Einige weitere Feldtypen mit unterschiedlichen möglichen Eigenschaften werden hinzugefügt
  • Felder werden basierend auf der Eingabe anderer Felder gerendert oder nicht gerendert
  • Einige Felder erfordern beim Ändern eine zusätzliche Verarbeitung
  • Werte in Auswahlen hängen von vorherigen Auswahlen ab

Diese Liste kann für eine lange Zeit fortgesetzt werden ...

Um dies zu implementieren, müssen Sie immer einige Funktionen in Ihrer Konfiguration speichern, und der Code, der die Konfiguration rendert, verwandelt sich allmählich in Frankenstein, nimmt eine Reihe verschiedener Requisiten und wirft sie über verschiedene Komponenten.

Der zuvor geschriebene Code, der auf der Zusammensetzung der Komponenten basiert, wird leise erweitert, ändert sich nach Belieben und wird aufgrund der fehlenden Zwischenabstraktion nicht sehr kompliziert - ein Algorithmus, der einen Baum von React-Elementen aus unserer Konfiguration erstellt. Dieser Code ist nicht doppelt vorhanden. Es folgt nur einem bestimmten Entwurfsmuster und sieht daher "gemustert" aus.

Nutzlose Optimierung


Nachdem in React Hooks aufgetaucht waren, bestand die Tendenz, alle Handler und Komponenten wahllos in useCallbackund einzuwickeln memo. Bitte tue das nicht! Diese Hooks wurden von React-Entwicklern nicht bereitgestellt, da React langsam ist und alles optimiert werden muss. Sie bieten Raum für die Optimierung Ihrer Anwendung, falls Sie auf Leistungsprobleme stoßen. Und selbst wenn Sie auf solche Probleme stoßen, müssen Sie nicht das gesamte Projekt in memound einbinden useCallback. Verwenden Sie den Profiler , um Probleme zu identifizieren und erst dann an der richtigen Stelle zu speichern.

Durch das Auswendiglernen wird Ihr Code immer komplizierter, aber nicht immer produktiver.

Schauen wir uns den Code eines realen Projekts an. Vergleichen Sie, wie die Funktion mit useCallbackund ohne aussieht :
Code kopieren
  const applyFilters = useCallback(() => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  }, [selectedMetricsStatus, onApplyFilterClick]);

  const applyFilters = () => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  };



Die Lesbarkeit des Codes wurde nach dem Entfernen des Wrappers deutlich erhöht, da der ideale Code sein Fehlen ist.

Die Leistung dieses Codes wurde nicht erhöht, da diese Funktion wie folgt verwendet wird:
Code kopieren
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



Wo RightFooterButton- es ist nur styled.buttonvon styled-components, was sehr schnell aktualisiert wird. Aber Speicherverbrauch unsere Anwendung erhöhen , da reagiert immer in Erinnerung behalten selectedMetricsStatus, onApplyFilterClickund die Version der Funktion applyFilters, relevant für diese Abhängigkeiten.

Wenn Ihnen diese Argumente nicht ausreichen, lesen Sie den Artikel , in dem dieses Thema ausführlicher behandelt wird.




Ergebnisse


  • Reaktionsformen sind einfach. Probleme mit ihnen entstehen aufgrund der Entwickler selbst und der React-Dokumentation, in der dieses Thema nicht ausreichend detailliert behandelt wird.
  • aka value onChange . useFieldChange, , , v-model Vue .
  • KISS YAGNI. DRY, , .
  • , , React- .
  • , .




P.S.


Zunächst wollte ich über die deklarative Validierung komplexer verschachtelter Formulare schreiben. Aber am Ende habe ich beschlossen, das Publikum darauf vorzubereiten, damit das Bild so vollständig wie möglich ist.
Im folgenden Tutorial geht es darum, das in diesem Tutorial implementierte einfache Formular zu validieren.
Vielen Dank an alle, die bis zum Ende gelesen haben!

Es spielt keine Rolle, wie Sie Code schreiben oder was Sie tun, die Hauptsache ist, ihn zu genießen.

All Articles