Chiamando il metodo successivo da uno precedente ... Perché questo cattivo design?

7

Se createWorld() è veramente lungo e ho bisogno di dividerlo, posso dividerlo in createLight() , createEarth() , createPlants() e createAnimals() .

Quindi, naturalmente lo faccio:

function createLight(){
    //work 1
}
function createEarth(){
    //work 2
}
function createPlants(){
    //work 3
}
function createAnimals(){
    //work 3
}

function createWorld(){
    createLight()
    createEarth()
    createPlants()
    createAnimals()
}

Ma vedo che molti nuovi sviluppatori fanno ciò che sto provvisoriamente definendo " l'antipattern di Stairstep ". Va qualcosa come:

function createWorld(){
    //create light
    //work1
    createEarth()
}
function createEarth(){
    //work 2
    createPlants()
}
function createPlants(){
    //work 3
    createAnimals()
}
function createAnimals(){
    //work 4
}

Sono sicuro che sia peggio, ma non credo che la mia opinione da sola possa influenzare i miei colleghi. Penso che se un passo è createButterfly() allora quella funzione non ha business chiamando createGrasshopper() alla fine solo perché questo è il "prossimo passo".

E anche se lo chiami createButterflyThenCallCreateGrasshopper() , è ancora una cattiva progettazione perché non puoi testare bene il codice createButterfly e quando hai diversi passaggi diventa difficile vedere dove vengono chiamate le funzioni.

Lo chiamo antipattern Stairstep perché lo penso in questo modo:

|
|createWorld(){
              |
              |createEarth(){
                            |
                            |createPlants(){
                                           |createAnimals()

Ho ragione nel ritenere che questo sia un cattivo design? Perché questo è un cattivo design?

    
posta AwokeKnowing 05.12.2016 - 21:53
fonte

6 risposte

18

Oltre al fatto ovvio che nel caso # 2 createEarth e createPlants non fanno quello che il loro nome finge di fare, penso che il difetto principale qui sia violazione del principio singolo livello di astrazione :

createEarth esegue alcuni lavori direttamente, senza alcuna astrazione, quindi chiama createPlants , che è un'astrazione separata che svolge un lavoro aggiuntivo. Ma quest'ultimo dovrebbe essere allo stesso livello di astrazione del lavoro svolto prima. Così diversi livelli di astrazione sono mescolati.

Cercando di risolvere questo problema, in genere uno in genere rifattorizza createPlants dalla # 2 in questo modo:

function createPlants(){
    doWork3()  //work 3
    createAnimals()
}

Ora è tutto sullo stesso livello di astrazione, quindi possiamo risolvere il problema di denominazione: doWork3 dovrebbe essere rinominato in createPlants e createPlants in createLife , poiché crea animali e piante:

  function createLife(){
      createPlants()  
      createAnimals()
  }

Penso che ora sia ovvio che dopo alcuni passaggi di refactoring, uno automaticamente finisce nel caso # 1, che segue il principio SLA: all'interno di createWorld , ogni chiamata è sullo stesso livello di astrazione. All'interno di ciascuno degli altri metodi, (si spera) il lavoro svolto è anche su un livello di astrazione.

Si noti che, tecnicamente, anche il caso n. 2 funzionerà. Il problema inizia quando si deve cambiare qualcosa, quando il codice deve essere mantenuto o evoluto. Il caso n. 1 crea per lo più blocchi predefiniti, che possono essere più facilmente compresi, testati, riutilizzati, estesi o riordinati da soli. Nel caso n. 2 ogni blocco predefinito dipende da un altro, il quale getta tutti i precedenti vantaggi a bordo.

    
risposta data 06.12.2016 - 08:06
fonte
3

Questo è un cattivo design perché è più strettamente accoppiato di quanto non debba essere.

Quando le tue funzioni sono composte in un aggregatore, ognuna di esse può essere utilizzata (e testata) separatamente. Quando CreateEarth chiama CreatePlants nella catena, rimuove tale flessibilità dal codice. Le forze delle piante da creare, che è il processo solito , ma cosa succede quando l'azienda arriva e vuole un mondo oceanico o un mondo di vulcano che non ha bisogno di piante ?

Separando le funzioni, puoi adattarti meglio a questa modifica inevitabile .

    
risposta data 06.12.2016 - 17:53
fonte
2

Lo chiamerei qualcosa come "nidificazione eccessiva delle chiamate". L'intero punto di avere un metodo separato è quello di scomporre un singolo pezzo logico dell'algoritmo, idealmente un pezzo riutilizzabile. Se non è funzionalmente distinto o riutilizzabile, considerate di non scomporlo, a meno che faciliterà significativamente la leggibilità e spezzerà una parte molto ampia di codice che copre più pagine in componenti più gestibili. Di solito questo può essere fatto in modo tale che ogni pezzo abbia un ragionevole grado di indipendenza funzionale, anche se non è destinato ad essere chiamato da più di un luogo.

Vedi anche Quando è opportuno creare una funzione separata quando ci sarà sempre una sola chiamata a tale funzione?

    
risposta data 05.12.2016 - 23:20
fonte
1

Uno dei maggiori problemi che vedo è il rigoroso vincolo di ordinamento che # 2 pone sulla costruzione di un mondo. Cosa succede quando voglio createAnimals() prima di I createPlants() ? Cosa succede se decido che qualcosa deve essere fatto in mezzo? Nel primo esempio è semplice come spostare una singola riga di codice. Nel secondo esempio ho bisogno di riscrivere almeno 2 metodi.

    
risposta data 06.12.2016 - 18:06
fonte
-1

Motivi per andare via # 1:

  1. Ottimizzazione dello stack delle chiamate.

  2. Modularità per testabilità.

  3. Modularità per la manutenzione.

Da Wikipedia su stack di chiamate di funzioni (sembra non essere un grosso problema con i computer di oggi, ma lo è ancora con microcontrollori e altri sistemi embedded):

"In informatica, uno stack di chiamate è una struttura di dati stack che memorizza informazioni sulle subroutine attive di un programma per computer".

    
risposta data 07.12.2016 - 20:34
fonte
-3

Si chiama "cattiva denominazione". Un metodo chiamato createPlants che effettivamente crea entrambi gli animali e delle piante ha un nome fuorviante. Possibile indica un fraintendimento dell'astrazione delle funzioni, poiché lo scrittore non si rende conto che createAnimals è parte di ciò che createPlants fa.

Quando si decompone una funzione di grandi dimensioni in più funzioni più piccole, può essere eseguita in modo sequenziale o gerarchico. Entrambi sono completamente validi, a seconda della semantica delle funzioni. Ma ogni funzione dovrebbe avere uno scopo chiaro e ben definito che dovrebbe riflettersi nel suo nome.

Se è difficile trovare un nome naturale per una funzione, indica che lo scopo della funzione non è ben definito. Se hai bisogno di nominare una funzione createAllAnimalsExceptGrashopper() per descrivere il suo scopo, le funzioni non vengono scomposte in modo naturale.

Ma non c'è nulla di sbagliato di per sé nel chiamare il "prossimo passo" alla fine di una funzione. Ad esempio, se createEarth è stato chiamato prima che createPlants possa essere chiamato, direi che è meglio progettare avere createEarth call createPlants piuttosto che averli chiamati sequenzialmente al livello più alto. (Probabilmente la terra potrebbe essere un parametro a createPlants per rendere esplicita la dipendenza.)

Per quanto riguarda il nome di questo "antipattern": Penso che sia errato pensare che qualsiasi particolare istanza di cattivo design debba avere un nome specifico. Do la colpa al sistema educativo per attribuire molta importanza alla conoscenza e al ricordo di una terminologia precisa, piuttosto che a comprendere i problemi sottostanti. Cercando di insegnare alle persone che non dovrebbero scrivere "stepstep metods" senza che loro capiscano il problema sottostante causeranno solo la programmazione settoriale del carico.

    
risposta data 05.12.2016 - 23:28
fonte

Leggi altre domande sui tag