it-swarm.com.de

Bin ich zu "schlau", um von Jr. Entwicklern gelesen zu werden? Zu viel funktionale Programmierung in meinem JS?

Ich bin ein Sr. Front-End-Entwickler, der in Babel ES6 codiert. Ein Teil unserer App führt einen API-Aufruf durch. Basierend auf dem Datenmodell, das wir vom API-Aufruf erhalten, müssen bestimmte Formulare ausgefüllt werden.

Diese Formulare werden in einer doppelt verknüpften Liste gespeichert (wenn das Back-End angibt, dass einige der Daten ungültig sind, können wir den Benutzer schnell zu der einen Seite zurückbringen, die er durcheinander gebracht hat, und sie dann wieder auf das Ziel bringen, indem wir einfach die ändern Liste.)

Wie auch immer, es gibt eine Reihe von Funktionen zum Hinzufügen von Seiten, und ich frage mich, ob ich zu schlau bin. Dies ist nur eine grundlegende Übersicht - der eigentliche Algorithmus ist viel komplexer, mit Tonnen verschiedener Seiten und Seitentypen, aber dies gibt Ihnen ein Beispiel.

Ich denke, so würde ein unerfahrener Programmierer damit umgehen.

export const addPages = (apiData) => {
   let pagesList = new PagesList(); 

   if(apiData.pages.foo){
     pagesList.add('foo', apiData.pages.foo){
   }

   if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

   if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

   return pagesList;
}

Um testbarer zu sein, habe ich alle if-Anweisungen genommen und getrennt, eigenständige Funktionen erstellt und sie dann zugeordnet.

Jetzt ist testbar eine Sache, aber auch lesbar und ich frage mich, ob ich die Dinge hier weniger lesbar mache.

// file: '../util/functor.js'

export const Identity = (x) => ({
  value: x,
  map: (f) => Identity(f(x)),
})

// file 'addPages.js' 

import { Identity } from '../util/functor'; 

export const parseFoo = (data) => (list) => {
   list.add('foo', data); 
}

export const parseBar = (data) => (list) => {
   data.forEach((bar) => {
     list.add(bar.name, bar.data)
   }); 
   return list; 
} 

export const parseBaz = (data) => (list) => {
   data.forEach((baz) => {
      list.add(customBazParser(baz)); 
   })
   return list;
}


export const addPages = (apiData) => {
   let pagesList = new PagesList(); 
   let { foo, arrayOfBars: bars, customBazes: bazes } = apiData.pages; 

   let pages = Identity(pagesList); 

   return pages.map(foo ? parseFoo(foo) : x => x)
               .map(bars ? parseBar(bars) : x => x)
               .map(bazes ? parseBaz(bazes) : x => x)
               .value

}

Hier ist meine Sorge. Um ich ist der Boden besser organisiert. Der Code selbst ist in kleinere Teile unterteilt, die isoliert getestet werden können. ABER ich denke: Wenn ich das als Junior-Entwickler lesen müsste, ohne an Konzepte wie die Verwendung von Identitätsfunktoren, Currying oder ternären Anweisungen gewöhnt zu sein, könnte ich dann überhaupt verstehen, was die letztere Lösung tut? Ist es manchmal besser, Dinge "falsch, einfacher" zu machen?

134
Brian Boyko

In Ihrem Code haben Sie mehrere Änderungen vorgenommen:

  • die Destrukturierung der Zuordnung zu Zugriffsfeldern in pages ist eine gute Änderung.
  • das Extrahieren der parseFoo() -Funktionen usw. ist eine möglicherweise gute Änderung.
  • die Einführung eines Funktors ist… sehr verwirrend.

Einer der verwirrendsten Teile hier ist, wie Sie funktionale und zwingende Programmierung mischen. Mit Ihrem Funktor transformieren Sie Daten nicht wirklich, sondern verwenden sie, um eine veränderbare Liste durch verschiedene Funktionen zu übergeben. Das scheint keine sehr nützliche Abstraktion zu sein, dafür haben wir bereits Variablen. Die Sache, die möglicherweise abstrahiert werden sollte - nur das Parsen dieses Elements, wenn es existiert - ist noch explizit in Ihrem Code enthalten, aber jetzt müssen wir um die Ecke denken. Zum Beispiel ist es nicht offensichtlich, dass parseFoo(foo) eine Funktion zurückgibt. JavaScript verfügt nicht über ein statisches Typsystem, das Sie darüber informiert, ob dies zulässig ist. Daher ist ein solcher Code ohne einen besseren Namen (makeFooParser(foo)?) Wirklich fehleranfällig. Ich sehe keinen Nutzen in dieser Verschleierung.

Was ich stattdessen erwarten würde:

if (foo) parseFoo(pages, foo);
if (bars) parseBar(pages, bars);
if (bazes) parseBaz(pages, bazes);
return pages;

Dies ist jedoch auch nicht ideal, da auf der Anrufseite nicht klar ist, dass die Elemente zur Seitenliste hinzugefügt werden. Wenn stattdessen die Analysefunktionen rein sind und eine (möglicherweise leere) Liste zurückgeben, die wir explizit zu den Seiten hinzufügen können, ist dies möglicherweise besser:

pages.addAll(parseFoo(foo));
pages.addAll(parseBar(bars));
pages.addAll(parseBaz(bazes));
return pages;

Zusätzlicher Vorteil: Die Logik, was zu tun ist, wenn das Element leer ist, wurde jetzt in die einzelnen Analysefunktionen verschoben. Wenn dies nicht angemessen ist, können Sie dennoch Bedingungen einführen. Die Veränderbarkeit der pages-Liste wird jetzt zu einer einzigen Funktion zusammengefasst, anstatt sie auf mehrere Aufrufe zu verteilen. Das Vermeiden nicht lokaler Mutationen ist ein weitaus größerer Teil der funktionalen Programmierung als Abstraktionen mit lustigen Namen wie Monad.

Also ja, dein Code war zu schlau. Bitte wenden Sie Ihre Klugheit an, um keinen klugen Code zu schreiben, sondern um kluge Wege zu finden, um die Notwendigkeit einer offensichtlichen Klugheit zu vermeiden. Die besten Designs sind nicht schauen schick, aber für jeden, der sie sieht, offensichtlich. Und gute Abstraktionen dienen dazu, die Programmierung zu vereinfachen und keine zusätzlichen Ebenen hinzuzufügen, die ich zuerst in meinem Kopf entwirren muss (hier, um herauszufinden, dass der Funktor einer Variablen entspricht und effektiv entfernt werden kann).

Bitte: Halten Sie im Zweifelsfall Ihren Code einfach und dumm (KISS-Prinzip).

320
amon

Wenn Sie Zweifel haben, ist es wahrscheinlich zu klug! Das zweite Beispiel führt zufällige Komplexität mit Ausdrücken wie foo ? parseFoo(foo) : x => x ein, und insgesamt ist der Code komplexer, was bedeutet, dass es schwieriger ist, ihm zu folgen.

Der angebliche Vorteil, dass Sie die Chunks einzeln testen können, könnte auf einfachere Weise erreicht werden, indem Sie einfach in einzelne Funktionen einbrechen. Und im zweiten Beispiel koppeln Sie die ansonsten getrennten Iterationen, sodass Sie tatsächlich weniger Isolation erhalten.

Was auch immer Sie über den funktionalen Stil im Allgemeinen denken, dies ist eindeutig ein Beispiel, bei dem der Code dadurch komplexer wird.

Ich finde ein kleines Warnsignal darin, dass Sie "unerfahrenen Entwicklern" einfachen und unkomplizierten Code zuordnen. Das ist eine gefährliche Mentalität. Nach meiner Erfahrung ist es das Gegenteil: Anfänger neigen zu übermäßig komplexem und cleverem Code, da mehr Erfahrung erforderlich ist, um die einfachste und klarste Lösung zu finden.

Bei den Ratschlägen gegen "cleveren Code" geht es nicht wirklich darum, ob der Code fortgeschrittene Konzepte verwendet oder nicht, die ein Anfänger möglicherweise nicht versteht. Es geht vielmehr darum, Code zu schreiben, der komplexer oder komplizierter als nötig ist. Dies macht es schwieriger, dem Code für alle, Anfänger und Experten gleichermaßen zu folgen, und wahrscheinlich auch für Sie selbst einige Monate später.

224
JacquesB

Meine Antwort kommt etwas spät, aber ich möchte mich trotzdem einmischen. Nur weil Sie die neuesten ES6-Techniken oder das beliebteste Programmierparadigma verwenden, bedeutet dies nicht unbedingt, dass Ihr Code korrekter ist oder der Code des Junior ist falsch. Funktionale Programmierung (oder eine andere Technik) sollte verwendet werden, wenn sie tatsächlich benötigt wird. Wenn Sie versuchen, die kleinste Chance zu finden, die neuesten Programmiertechniken in jedes Problem zu integrieren, erhalten Sie immer eine überentwickelte Lösung.

Machen Sie einen Schritt zurück und versuchen Sie, das Problem, das Sie lösen möchten, für eine Sekunde zu verbalisieren. Im Wesentlichen möchten Sie nur, dass eine Funktion addPages verschiedene Teile von apiData in eine Reihe von Schlüssel-Wert-Paaren umwandelt und dann alle zu PagesList hinzufügt.

Und wenn das alles ist, warum sollte man sich dann die Mühe machen, identity function Mit ternary operator Oder functor für das Parsen von Eingaben zu verwenden? Außerdem, warum denkst du, ist es überhaupt ein richtiger Ansatz, functional programming Anzuwenden, der Nebenwirkungen (durch Mutation der Liste) verursacht? Warum all diese Dinge, wenn Sie nur Folgendes brauchen:

const processFooPages = (foo) => foo ? [['foo', foo]] : [];
const processBarPages = (bar) => bar ? bar.map(page => [page.name, page.data]) : [];
const processBazPages = (baz) => baz ? baz.map(page => [page.id, page.content]) : [];

const addPages = (apiData) => {
  const list = new PagesList();
  const pages = [].concat(
    processFooPages(apiData.pages.foo),
    processBarPages(apiData.pages.arrayOfBars),
    processBazPages(apiData.pages.customBazes)
  );
  pages.forEach(([pageName, pageContent]) => list.addPage(pageName, pageContent));

  return list;
}

(eine lauffähige jsfiddle hier )

Wie Sie sehen können, verwendet dieser Ansatz immer noch functional programming, Jedoch in Maßen. Beachten Sie auch, dass alle 3 Transformationsfunktionen, da sie keinerlei Nebenwirkungen verursachen, kinderleicht zu testen sind. Der Code in addPages ist ebenfalls trivial und bescheiden, was Anfänger oder Experten auf einen Blick verstehen können.

Vergleichen Sie diesen Code mit dem, was Sie oben gefunden haben. Sehen Sie den Unterschied? Zweifellos sind die Syntaxen functional programming Und ES6 leistungsstark, aber wenn Sie das Problem mit diesen Techniken falsch aufteilen, erhalten Sie sogar noch chaotischeren Code.

Wenn Sie sich nicht auf das Problem einlassen und die richtigen Techniken an den richtigen Stellen anwenden, können Sie den Code haben, der von Natur aus funktional ist und dennoch von allen Teammitgliedern sehr gut organisiert und gewartet werden kann. Diese Eigenschaften schließen sich nicht gegenseitig aus.

21
b0nyb0y

Das zweite Snippet ist nicht testbarer als das erste. Es wäre ziemlich einfach, alle erforderlichen Tests für eines der beiden Snippets einzurichten.

Der wirkliche Unterschied zwischen den beiden Schnipsel ist die Verständlichkeit. Ich kann den ersten Ausschnitt ziemlich schnell lesen und verstehen, was los ist. Der zweite Ausschnitt, nicht so sehr. Es ist weit weniger intuitiv und wesentlich länger.

Dies erleichtert die Wartung des ersten Snippets, was eine wertvolle Codequalität darstellt. Ich finde sehr wenig Wert im zweiten Ausschnitt.

TD; DR

  1. Können Sie dem Junior Developer Ihren Code innerhalb von 10 Minuten oder weniger erklären?
  2. Können Sie in zwei Monaten Ihren Code verstehen?

Detaillierte Analyse

Klarheit und Lesbarkeit

Der ursprüngliche Code ist beeindruckend klar und für jeden Programmierer leicht verständlich. Es ist in einem Stil , der jedem vertraut ist .

Die Lesbarkeit basiert größtenteils auf Vertrautheit, nicht auf einer mathematischen Zählung von Token . IMO, zu diesem Zeitpunkt haben Sie zu viel ES6 in Ihrem Umschreiben. Vielleicht werde ich in ein paar Jahren diesen Teil meiner Antwort ändern. :-) Übrigens, ich mag auch die Antwort von @ b0nyb0y als vernünftigen und klaren Kompromiss.

Testbarkeit

if(apiData.pages.foo){
   pagesList.add('foo', apiData.pages.foo){
}

Unter der Annahme, dass PagesList.add () Tests hat, die es sollte, ist dies ein völlig einfacher Code, und es gibt keinen offensichtlichen Grund für diesen Abschnitt, spezielle separate Tests zu benötigen.

if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

Auch hier sehe ich keine unmittelbare Notwendigkeit für spezielle separate Tests dieses Abschnitts. Es sei denn, PagesList.add () hat ungewöhnliche Probleme mit Nullen oder Duplikaten oder anderen Eingaben.

if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

Dieser Code ist auch sehr einfach. Angenommen, customBazParser wird getestet und gibt nicht zu viele "spezielle" Ergebnisse zurück. Also noch einmal, es sei denn, es gibt schwierige Situationen mit `PagesList.add () (die es geben könnte, da ich mit Ihrer Domain nicht vertraut bin). Ich verstehe nicht, warum dieser Abschnitt spezielle Tests erfordert.

Im Allgemeinen sollte das Testen der gesamten Funktion einwandfrei funktionieren.

Haftungsausschluss: Wenn es spezielle Gründe gibt, alle 8 Möglichkeiten der drei if() - Anweisungen zu testen, dann teilen Sie die Tests auf. Oder, wenn PagesList.add() empfindlich ist, ja, teilen Sie die Tests auf.

Struktur: Lohnt es sich, in drei Teile zu zerlegen (wie Gallien)

Hier haben Sie das beste Argument. Persönlich denke ich nicht, dass der ursprüngliche Code "zu lang" ist (ich bin kein SRP-Fanatiker). Aber wenn es noch ein paar if (apiData.pages.blah) Abschnitte gäbe, dann ist SRP der hässliche Kopf und es wäre eine Aufteilung wert. Insbesondere wenn DRY gilt und die Funktionen an anderen Stellen des Codes verwendet werden könnten.

Mein einziger Vorschlag

YMMV. Um eine Codezeile und eine Logik zu speichern, könnte ich if und let in einer Zeile kombinieren: zB

let bars = apiData.pages.arrayOfBars || [];
bars.forEach((bar) => {
   pagesList.add(bar.name, bar.data);
})

Dies schlägt fehl, wenn apiData.pages.arrayOfBars eine Zahl oder ein String ist, aber auch der ursprüngliche Code. Und für mich ist es klarer (und eine überstrapazierte Sprache).

3
user949300