Sono troppo 'intelligente' per essere leggibile dagli sviluppatori Jr.? Troppa programmazione funzionale nel mio JS? [chiuso]


133

Sono uno sviluppatore di front-end Sr., codifica in Babel ES6. Parte della nostra app effettua una chiamata API e in base al modello di dati che riceviamo dalla chiamata API, alcuni moduli devono essere compilati.

Tali moduli sono memorizzati in un elenco doppiamente collegato (se il back-end dice che alcuni dei dati non sono validi, possiamo rapidamente riportare l'utente sull'unica pagina in cui hanno incasinato e poi riportarli sulla destinazione, semplicemente modificando il elenco.)

Ad ogni modo, ci sono un sacco di funzioni usate per aggiungere pagine e mi chiedo se sono troppo intelligente. Questa è solo una panoramica di base: l'algoritmo attuale è molto più complesso, con tonnellate di pagine e tipi di pagina diversi, ma questo ti darà un esempio.

È così che, a mio avviso, un programmatore principiante lo gestirà.

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

Ora, per essere più verificabile, ho preso tutte quelle istruzioni if ​​e le ho separate, le funzioni autonome e poi le mappa su di esse.

Ora, testabile è una cosa, ma così è leggibile e mi chiedo se sto rendendo le cose meno leggibili qui.

// 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

}

Ecco la mia preoccupazione. Per me il fondo è più organizzato. Il codice stesso è suddiviso in blocchi più piccoli che possono essere testati in isolamento. MA sto pensando: se dovessi leggerlo come sviluppatore junior, inutilizzato a concetti come l'utilizzo di funzioni Identity, curry o dichiarazioni ternarie, sarei anche in grado di capire cosa sta facendo quest'ultima soluzione? È meglio fare le cose nel modo "sbagliato, più facile" a volte?


13
come qualcuno che ha solo 10 anni di auto-insegnamento in JS, mi considero un Jr. e mi hai perso aBabel ES6
RozzA

26
OMG - opera nel settore dal 1999 e codifica dal 1983 e sei il tipo di sviluppatore più dannoso che esista. Quello che pensi sia "intelligente" è in realtà chiamato "costoso" e "difficile da mantenere" e "fonte di bug" e non ha spazio in un ambiente aziendale. Il primo esempio è semplice, facile da capire e funziona mentre il secondo esempio è complesso, difficile da capire e non dimostrabilmente corretto. Per favore, smetti di fare questo genere di cose. NON è meglio, tranne forse in un certo senso accademico che non si applica al mondo reale.
user1068

15
Voglio solo citare Brian Kerninghan qui: "Tutti sanno che il debug è due volte più difficile della scrittura di un programma in primo luogo. Quindi se sei intelligente quanto puoi quando lo scrivi, come lo debug mai? " - en.wikiquote.org/wiki/Brian_Kernighan / "Gli elementi dello stile di programmazione", 2a edizione, capitolo 2.
MarkusSchaber,

7
@Logister Coolness non è più un obiettivo primario della semplicità. L'obiezione qui è alla complessità gratuita , che è nemica della correttezza (sicuramente una preoccupazione primaria) perché rende il codice più difficile da ragionare e più probabilmente contiene casi d'angolo imprevisti. Dato il mio scetticismo precedentemente affermato di affermazioni che in realtà è più facile da testare, non ho visto alcun argomento convincente per questo stile. In analogia con la regola del minimo privilegio sulla sicurezza, forse potrebbe esserci una regola empirica che dice che si dovrebbe essere cauti nell'utilizzare potenti funzionalità linguistiche per fare cose semplici.
sdenham,

6
Il tuo codice sembra un codice junior. Mi aspetto che un anziano scriva il primo esempio.
sed

Risposte:


322

Nel tuo codice hai apportato più modifiche:

  • l'assegnazione di destrutturazione ai campi di accesso in pagesè un buon cambiamento.
  • l'estrazione delle parseFoo()funzioni ecc. è forse un buon cambiamento.
  • introdurre un funzione è ... molto confuso.

Una delle parti più confuse qui è come stai mescolando la programmazione funzionale e imperativa. Con il tuo funzione non stai davvero trasformando i dati, li stai usando per passare un elenco mutabile attraverso varie funzioni. Non sembra un'astrazione molto utile, abbiamo già delle variabili per questo. La cosa che avrebbe potuto essere astratta - analizzando quell'elemento solo se esiste - è ancora lì esplicitamente nel tuo codice, ma ora dobbiamo pensare dietro l'angolo. Ad esempio, è in qualche modo non ovvio che parseFoo(foo)restituirà una funzione. JavaScript non ha un sistema di tipo statico per avvisarti se questo è legale, quindi tale codice è realmente soggetto a errori senza un nome migliore ( makeFooParser(foo)?). Non vedo alcun beneficio in questo offuscamento.

Quello che mi aspetterei di vedere invece:

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

Ma non è nemmeno l'ideale, perché dal sito della chiamata non è chiaro che gli elementi verranno aggiunti all'elenco delle pagine. Se invece le funzioni di analisi sono pure e restituiscono un elenco (possibilmente vuoto) che possiamo aggiungere esplicitamente alle pagine, potrebbe essere meglio:

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

Vantaggio aggiunto: la logica su cosa fare quando l'elemento è vuoto è stata ora spostata nelle singole funzioni di analisi. Se ciò non è appropriato, puoi comunque introdurre i condizionali. La mutabilità pagesdell'elenco è ora riunita in un'unica funzione, anziché diffonderla su più chiamate. Evitare le mutazioni non locali è una parte molto più grande della programmazione funzionale rispetto alle astrazioni con nomi divertenti come Monad.

Quindi sì, il tuo codice era troppo intelligente. Si prega di applicare la propria intelligenza non per scrivere codice intelligente, ma per trovare modi intelligenti per evitare la necessità di palese intelligenza. I migliori design non sembrano fantasiosi, ma sembrano ovvi a chiunque li veda. E buone astrazioni sono lì per semplificare la programmazione, non per aggiungere ulteriori livelli che devo districare nella mia mente prima (qui, capire che il funzione è equivalente a una variabile e può essere efficacemente eluso).

Per favore: in caso di dubbi, mantieni il tuo codice semplice e stupido (principio KISS).


2
Da un punto di vista di simmetria, da cosa let pages = Identity(pagesList)differisce parseFoo(foo)? Dato che, mi avrebbe probabilmente ... {Identity(pagesList), parseFoo(foo), parseBar(bar)}.flatMap(x -> x).
Art

8
Grazie per aver spiegato che avere tre espressioni lambda nidificate per la raccolta di un elenco mappato (per il mio occhio non allenato) potrebbe essere un po ' troppo intelligente.
Thorbjørn Ravn Andersen,

2
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
yannis,

Forse uno stile fluente funzionerebbe bene nel tuo secondo esempio?
user1068

225

Se hai dei dubbi, probabilmente è troppo intelligente! Il secondo esempio introduce complessità accidentale con espressioni simili foo ? parseFoo(foo) : x => x, e nel complesso il codice è più complesso, il che significa che è più difficile da seguire.

Il presunto vantaggio, che puoi testare singolarmente i blocchi, potrebbe essere ottenuto in un modo più semplice semplicemente entrando nelle singole funzioni. E nel secondo esempio si accoppiano le iterazioni altrimenti separate, in modo da ottenere effettivamente meno isolamento.

Qualunque siano i tuoi sentimenti sullo stile funzionale in generale, questo è chiaramente un esempio in cui rende il codice più complesso.

Trovo un po 'un segnale di avvertimento nel fatto che associ un codice semplice e diretto a "sviluppatori alle prime armi". Questa è una mentalità pericolosa. Nella mia esperienza è l'opposto: gli sviluppatori alle prime armi sono inclini a un codice troppo complesso e intelligente, perché richiede più esperienza per poter vedere la soluzione più semplice e chiara.

Il consiglio contro il "codice intelligente" non riguarda in realtà se il codice usi o meno concetti avanzati che un principiante potrebbe non capire. Piuttosto si tratta di scrivere codice che è più complesso o contorto del necessario . Questo rende il codice più difficile da seguire per tutti , principianti ed esperti, e probabilmente anche per voi stessi alcuni mesi dopo.


156
"Gli sviluppatori alle prime armi sono inclini a un codice eccessivamente complesso e intelligente, perché richiede una maggiore esperienza per poter vedere la soluzione più semplice e chiara" non può essere più d'accordo con te. Risposta eccellente!
Bonifacio,

23
Il codice troppo complesso è anche abbastanza passivo-aggressivo. Stai deliberatamente producendo codice che pochi altri possono leggere o eseguire il debug facilmente ... il che significa sicurezza del lavoro per te, ed inferno assoluto per tutti gli altri in tua assenza. Puoi anche scrivere la tua documentazione tecnica interamente in latino.
Ivan

14
Non credo che il codice intelligente sia sempre una cosa da mostrare. A volte sembra naturale e sembra ridicolo solo per una seconda ispezione.

5
Ho rimosso la frase sul "mettersi in mostra" poiché sembrava più giudicante del previsto.
JacquesB,

11
@BaileyS - Penso che enfatizzi l'importanza della revisione del codice; ciò che sembra naturale e diretto per il programmatore, specialmente se gradualmente sviluppato in quel modo, può facilmente sembrare contorto per un revisore. Il codice quindi non passa la revisione fino a quando non viene refactored / riscritto per rimuovere la convoluzione.
Myles,

21

Questa mia risposta arriva un po 'in ritardo, ma voglio ancora intervenire. Solo perché stai usando le ultime tecniche ES6 o stai usando il paradigma di programmazione più popolare non significa necessariamente che il tuo codice sia più corretto, o quello di quel junior è sbagliato. La programmazione funzionale (o qualsiasi altra tecnica) dovrebbe essere utilizzata quando è effettivamente necessaria. Se cerchi di trovare la minima possibilità di inserire le ultime tecniche di programmazione in ogni problema, finirai sempre con una soluzione troppo ingegnerizzata.

Fai un passo indietro e prova a verbalizzare il problema che stai cercando di risolvere per un secondo. In sostanza, vuoi solo una funzione addPagesper trasformare diverse parti apiDatain un insieme di coppie chiave-valore, quindi aggiungerle tutte in PagesList.

E se questo è tutto quello che c'è da fare, perché preoccuparsi utilizzando identity functioncon ternary operator, o utilizzare functorper l'analisi di ingresso? Inoltre, perché pensi che sia un approccio corretto da applicare functional programmingche causa effetti collaterali (mutando la lista)? Perché tutte queste cose, quando tutto ciò che serve è proprio questo:

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

(un jsfiddle eseguibile qui )

Come puoi vedere, questo approccio utilizza ancora functional programming, ma con moderazione. Inoltre, poiché tutte e 3 le funzioni di trasformazione non causano alcun effetto collaterale, sono facilmente testabili. Il codice addPagesè anche banale e senza pretese che i principianti o gli esperti possono capire con un semplice sguardo.

Ora, confronta questo codice con quello che hai inventato sopra, vedi la differenza? Indubbiamente, functional programminge le sintassi ES6 sono potenti, ma se affetti il ​​problema nel modo sbagliato con queste tecniche, finirai con un codice ancora più disordinato.

Se non ti precipiti nel problema e applichi le tecniche giuste nei posti giusti, puoi avere il codice che è di natura funzionale, mentre è ancora molto organizzato e gestibile da tutti i membri del team. Queste caratteristiche non si escludono a vicenda.


2
+1 per sottolineare questo atteggiamento diffuso (non si applica necessariamente all'OP): "Solo perché stai utilizzando le ultime tecniche ES6 o stai utilizzando il paradigma di programmazione più popolare non significa necessariamente che il tuo codice sia più corretto, o il codice di quel junior è sbagliato ".
Giorgio,

+1. Solo una piccola osservazione pedante, puoi usare l'operatore spread (...) invece di _.concat per rimuovere quella dipendenza.
YoTengoUnLCD,

1
@YoTengoUnLCD Ah, buona cattura. Ora sai che io e il mio team siamo ancora in viaggio per disimparare un po 'del nostro lodashuso. Quel codice può usare spread operator, o anche [].concat()se si vuole mantenere intatta la forma del codice.
b0nyb0y

Siamo spiacenti, ma questo elenco di codici è ancora molto meno ovvio per me rispetto al "codice junior" originale nel post di OP. Fondamentalmente: non usare mai l'operatore ternario se è possibile evitarlo. È troppo teso. In un linguaggio funzionale "reale", le dichiarazioni if ​​sarebbero espressioni e non dichiarazioni, e quindi più leggibili.
Olle Härstedt,

@ OlleHärstedt Umm, questa è un'affermazione interessante che hai fatto. Il fatto è che il paradigma di programmazione funzionale o qualsiasi altro paradigma là fuori non è mai legato a un particolare linguaggio funzionale "reale", tanto meno alla sua sintassi. Quindi, dettare quali costrutti condizionali dovrebbero essere o non dovrebbero "mai" essere usati non ha alcun senso. A ternary operatorè valido come una normale ifdichiarazione, che ti piaccia o no. Il dibattito sulla leggibilità tra if-elsee il ?:campo è infinito, quindi non ci entrerò. Tutto ciò che dirò è che, con occhi allenati, linee come queste non sono quasi "troppo tese".
b0nyb0y,

5

Il secondo frammento non è più testabile del primo. Sarebbe ragionevolmente semplice impostare tutti i test necessari per uno dei due frammenti.

La vera differenza tra i due frammenti è la comprensibilità. Riesco a leggere il primo frammento abbastanza rapidamente e capire cosa sta succedendo. Il secondo frammento, non tanto. È molto meno intuitivo e sostanzialmente più lungo.

Ciò semplifica la manutenzione del primo frammento, che rappresenta una preziosa qualità del codice. Trovo molto poco valore nel secondo frammento.


3

TD; DR

  1. Puoi spiegare il tuo codice allo sviluppatore Junior in 10 minuti o meno?
  2. Tra due mesi, puoi capire il tuo codice?

Analisi dettagliata

Chiarezza e leggibilità

Il codice originale è straordinariamente chiaro e facile da capire per qualsiasi livello di programmatore. È in uno stile familiare a tutti .

La leggibilità si basa in gran parte sulla familiarità, non su alcuni conteggi matematici dei token . IMO, a questo punto nel tempo, hai troppa ES6 nella tua riscrittura. Forse tra un paio d'anni cambierò questa parte della mia risposta. :-) A proposito, mi piace anche la risposta @ b0nyb0y come un compromesso ragionevole e chiaro.

testabilità

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

Supponendo che PagesList.add () abbia dei test, che dovrebbe, questo è un codice completamente semplice e non vi è alcuna ragione ovvia per questa sezione che necessiti di speciali test separati.

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

Ancora una volta, non vedo la necessità immediata di alcun test separato speciale di questa sezione. A meno che PagesList.add () abbia problemi insoliti con valori null o duplicati o altri input.

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

Questo codice è anche molto semplice. Supponendo che customBazParsersia testato e non restituisca troppi risultati "speciali". Quindi di nuovo, a meno che non ci siano situazioni difficili con `PagesList.add (), (che potrebbe esserci perché non ho familiarità con il tuo dominio) non vedo perché questa sezione abbia bisogno di test speciali.

In generale, testare l'intera funzione dovrebbe funzionare correttamente.

Dichiarazione di non responsabilità : se ci sono ragioni speciali per testare tutte e 8 le possibilità delle tre if()affermazioni, allora sì, suddividere i test. Oppure, se PagesList.add()è sensibile, sì, suddividere i test.

Struttura: vale la pena dividersi in tre parti (come la Gallia)

Qui hai l'argomento migliore. Personalmente, non credo che il codice originale sia "troppo lungo" (non sono un fanatico di SRP). Ma, se ci fossero alcune if (apiData.pages.blah)sezioni in più , allora SRP impara che è brutta testa e varrebbe la pena dividerla. Soprattutto se si applica DRY e le funzioni potrebbero essere utilizzate in altri punti del codice.

Il mio unico suggerimento

YMMV. Per salvare una riga di codice e un po 'di logica, potrei combinare if e let in una riga: ad es

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

Questo fallirà se apiData.pages.arrayOfBars è un numero o una stringa, ma lo sarà anche il codice originale. E per me è più chiaro (e un linguaggio abusato).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.