Esiste un modo elegante per scrivere codice con percorsi differenti aventi un segmento medio comune?

3

Ho iniziato con la seguente, semplice routine:

void originalCode(){
    boolean status = getStatus();

    function2(status);

    if(status){
         return;
    }

    function3();    
}

Tuttavia, è richiesta una funzionalità aggiuntiva per chiamare function1 sul percorso status == true . Era anche importante chiamare function1 prima di function2 .

I risultati erano una delle due seguenti mostruosità:

void alternative1(){
    boolean status = getStatus();

    if(status){
        function1();
    }

    function2(status);

    if(status){
         return;
    }

    function3();    
}

o

void alternative2(){
    boolean status = getStatus();

    if(status){
        function1();
        function2(status);
        return;
    }

    function2(status);
    function3();    
}

Entrambi hanno la duplicazione del codice e, in generale, non sembrano giusti.

Ora, nel mio caso particolare, function3 non è richiesto per essere chiamato dopo function2 , quindi ho optato per quanto segue:

void niceAlternative(){
    boolean status = getStatus();

    if(status){
        function1();
    }else{
        function3();
    }

    function2(status);
}

Ma se non fosse un'opzione - cioè. se function2 doveva essere chiamato prima di function3 ? E in generale, come si struttura il codice con più percorsi di esecuzione che hanno un aspetto comune nel medio ?

    
posta executor21 27.11.2017 - 01:33
fonte

3 risposte

5

Non tutti gli identificativi ripetuti sono "duplicati di codice". L'indicatore per la duplicazione del codice è che quando vuoi apportare una modifica (i nomi non contano) a una sua occorrenza, di solito devi fare lo stesso identico cambiamento in tutte le occasioni.

Chiamare function2(status); più volte non è la duplicazione del codice (supponendo, naturalmente, è davvero una funzione e non un segnaposto per diverse righe di codice. Se è il caso, dovresti estrarlo in una funzione). Potrebbe essere stata la duplicazione del codice se avessi una lunga lista di argomenti, o se alcuni di questi argomenti fossero composti. Se fosse così, e volessi aggiungere / rimuovere / cambiare argomenti, dovresti farlo in tutti i punti in cui viene chiamata la funzione. Ma con un argomento e anche uno semplice? Le probabilità che vorrai cambiarle sono basse.

Controllare if(status) più volte non è nemmeno la duplicazione del codice. Se invece di status avessi qualche espressione complessa sarebbe stata la duplicazione del codice, ma in questo caso avresti potuto semplicemente memorizzarlo in una variabile.

Quindi, puoi scegliere entrambi gli stili, ma ti suggerisco di selezionare quello che controlla status due volte, perché:

  1. Una chiamata a una funzione, anche una semplice, può ancora cambiare se è necessario aggiungere argomenti. if(status) ha molte meno possibilità di cambiare, perché ogni possibile modifica che potresti inserire nella sua dichiarazione di assegnazione.
  2. Il flusso è più chiaro - function1 , function2 e function3 sono chiamati in ordine, con function1 e function3 chiamati solo se status è true.
risposta data 27.11.2017 - 02:08
fonte
4

Il mio primo pensiero (l'ho chiesto in un commento alla domanda) è, "cosa fa la funzione2 in modo diverso in base al valore del parametro di input?" Ho il sospetto che function2 abbia un codice che è combinato in modo inappropriato e forse dovrebbe essere suddiviso. Anche se questo è non il caso, avrei ancora più probabilmente scritto il codice originale in questo modo:

void originalCode() {
    if (getStatus()) {
        function2(true)
    }
    else {
        function2(false)
        function3()
    }
}

La motivazione alla base di quanto sopra è che esiste un insieme di effetti collaterali per quando lo stato è vero e un diverso insieme di effetti collaterali per quando lo stato è falso. La struttura sopra indica chiaramente al lettore questo a colpo d'occhio. In altre parole, quando il lettore gli chiede se stesso, "Cosa succede se lo stato è vero / falso?" La risposta è in un unico posto ovvio e il lettore non ha bisogno di eseguire la scansione dell'intera funzione per cercare di capirlo.

Anche quando il nuovo requisito - quella funzione1 deve essere chiamata prima di function2 e solo se getStatus è vera - arriva, la modifica è una semplice aggiunta:

void code() {
    if (getStatus()) {
        function1()
        function2(true)
    }
    else {
        function2(false)
        function3()
    }
}

Il codice mi fornisce ancora un elenco sintetico di esattamente cosa dovrebbe accadere se getStatus restituisce true e cosa dovrebbe accadere se restituisce false.

    
risposta data 27.11.2017 - 03:24
fonte
1

La tua descrizione è piuttosto astratta, quindi sarà la mia risposta, ma si spera chiarisca un po 'di luce sulla tua domanda.

L'esempio che hai fornito è scritto in stile procedurale. Nella programmazione procedurale, senza utilizzare alcun modello di rimedio, può essere inevitabile non avere assolutamente alcuna duplicazione del codice. Ad es. C va benissimo (anche se esistono tabelle delle funzioni ) e potrebbe anche essere adatto per i linguaggi orientati agli oggetti in base al presupposto che non ti aspetti che questo pezzo di codice cambierà in futuro. Avevamo uno scenario simile per un robot industriale in cui il comportamento era legato alle capacità meccaniche del robot stesso. A meno che non abbiamo modificato l'hardware, cosa che non abbiamo fatto, non è necessario modificare il codice.

Generalmente se hai una logica condizionale complessa puoi rimediare con i seguenti approcci:

  1. Quando è necessario cambiare funzionalità in base a qualche variabile state , nella programmazione orientata agli oggetti il Modello di stato potrebbe rivelarsi utile. A seconda della lingua che si utilizza crea più o meno lastra, ma a volte ne vale la pena. Il modello ruota intorno alla nozione di stati in contrasto con il prossimo approccio.
  2. Crea un Albero dei comportamenti che può essere visto come un piano di esecuzione. È popolare nello sviluppo di giochi in cui una macchina a stati può causare un incubo di manutenzione, specialmente quando vengono aggiunte funzionalità o informazioni aggiuntive nel tempo. Il concetto dell'albero del comportamento si basa su azioni anziché su uno stato in cui ogni nodo incorpora un'azione da eseguire.
  3. C'è un altro metodo come elenco di puntatori di funzioni (oggetti in OOP) per sostituire lunghe catene if-else o tabelle delle funzioni .

Ogni metodo ha i suoi pro e contro e dipende dal tuo caso d'uso. Potrebbe essere che nessuno di loro è pratico nel tuo.

Per quanto riguarda la necessità di aggiungere funzionalità come mostrato in alternative2 generalmente Decorator o Pattern composito sono buone soluzioni. In combinazione con il pattern State, potrebbero districare il costrutto if-else .

Ma ancora una volta, è più una lista che puoi tornare e valutare se ha senso impiegare una o una combinazione di questi modelli. Alcuni modelli hanno un peso significativo come Modello di stato o Alberi di comportamento . Come regola generale, rimango pragmatico e vado per niceAlternative e uso un pattern se necessario o sai già che verrà migliorato nel tempo.

    
risposta data 27.11.2017 - 17:25
fonte

Leggi altre domande sui tag