Come si evita l'iterazione all'infinito attraverso design ugualmente sub-ottimali?

10

Quindi probabilmente, come molti, mi trovo spesso a dover affrontare mal di testa con problemi di progettazione in cui, ad esempio, esiste un modello / approccio di progettazione che sembra adattarsi intuitivamente al problema e ha i benefici desiderati. Molto spesso c'è qualche avvertenza che rende difficile implementare il modello / approccio senza un qualche tipo di lavoro attorno al quale poi si annulla il beneficio del modello / approccio. Posso facilmente finire iterando attraverso molti pattern / approcci, perché quasi tutti hanno delle avvertenze molto significative in situazioni reali in cui semplicemente non c'è una soluzione facile.

Esempio:

Ti darò un esempio ipotetico basato in modo approssimativo su uno reale che ho incontrato di recente. Diciamo che voglio usare la composizione sull'ereditarietà perché le gerarchie di ereditarietà hanno ostacolato la scalabilità del codice nel passato. Potrei rifattorizzare il codice ma poi scoprire che ci sono alcuni contesti in cui la superclasse / la classe di base semplicemente ha bisogno di chiamare funzionalità nella sottoclasse, nonostante i tentativi di evitarlo.

Il prossimo approccio migliore sembra implementare un modello di mezzo delegato / osservatore e un modello a metà composizione in modo che la superclasse possa delegare un comportamento o che la sottoclasse possa osservare eventi di superclasse. Quindi la classe è meno scalabile e mantenibile perché non è chiaro come dovrebbe essere estesa, inoltre è difficile estendere gli ascoltatori / delegati esistenti. Anche le informazioni non sono nascoste bene perché si inizia a dover conoscere l'implementazione per vedere come estendere la superclasse (a meno che non si utilizzino i commenti molto ampiamente).

Quindi, dopo questo, potrei scegliere semplicemente di usare semplicemente osservatori o delegati completamente per uscire dagli inconvenienti che derivano dal mescolare pesantemente gli approcci. Tuttavia questo ha i suoi problemi. Per esempio, potrei scoprire che alla fine ho bisogno di osservatori o delegati per una quantità crescente di comportamenti finché non avrò bisogno di osservatori / delegati per praticamente ogni comportamento. Un'opzione potrebbe essere quella di avere solo un grande ascoltatore / delegato per tutto il comportamento, ma poi la classe di implementazione finisce con molti metodi vuoti ecc.

Quindi potrei provare un altro approccio, ma ci sono altrettanti problemi con questo. Poi il prossimo, e quello dopo ecc.

Questo processo iterativo diventa molto difficile quando ogni approccio sembra avere tanti problemi quanti altri e porta a una sorta di paralisi della decisione progettuale . È anche difficile accettare che il codice finisca per essere ugualmente problematico, indipendentemente dal modello o dall'approccio di progettazione. Se finisco in questa situazione vuol dire che il problema stesso deve essere ripensato? Cosa fanno gli altri quando incontrano questa situazione?

Modifica Sembra esserci una serie di interpretazioni della domanda che voglio chiarire:

  • Ho tolto completamente l'OOP perché risulta che in realtà non è specifico per OOP, inoltre è troppo facile interpretare erroneamente alcuni dei commenti che ho fatto durante il passaggio a OOP.
  • Alcuni hanno affermato che dovrei adottare un approccio iterativo e provare diversi schemi, o che dovrei scartare un modello quando cessa di funzionare. Questo è il processo a cui intendevo fare riferimento in primo luogo. Ho pensato che questo fosse chiaro dall'esempio, ma avrei potuto renderlo più chiaro, quindi ho modificato la domanda per farlo.
posta Jonathan 28.02.2018 - 04:39
fonte

6 risposte

8

Quando arrivo a una decisione così dura, di solito mi pongo tre domande:

  1. Quali sono i pro e i contro di tutti le soluzioni disponibili?

  2. C'è una soluzione, non ho ancora preso in considerazione?

  3. Soprattutto:
    Quali sono esattamente le mie esigenze? Non i requisiti su carta, i veri fondamentali?
    Posso in qualche modo riformulare il problema / modificare i suoi requisiti, in modo da consentire una soluzione semplice e immediata?
    Posso rappresentare i miei dati in un modo diverso, in modo da consentire una soluzione così semplice e immediata?

    Questa è la domanda sui fondamenti del problema percepito. Potrebbe risultare che stavi effettivamente cercando di risolvere il problema sbagliato. Il tuo problema potrebbe avere requisiti specifici che consentono una soluzione molto più semplice rispetto al caso generale. Metti in discussione la tua formulazione del tuo problema!

Trovo molto importante pensare a tutte e tre le domande molto prima di continuare. Fai una passeggiata, percorri il tuo ufficio, fai tutto il possibile per riflettere davvero sulle risposte a queste domande. Tuttavia, rispondere a queste domande non dovrebbe richiedere tempi impropri. I tempi corretti possono variare da 15 minuti a qualcosa come una settimana, dipendono dalla cattiveria delle soluzioni che hai già trovato e dal loro impatto sull'intero.

Il valore di questo approccio è che a volte trovi soluzioni sorprendentemente buone. Soluzioni eleganti Le soluzioni valevano il tempo investito nel rispondere a queste tre domande. E non troverai quelle soluzioni, se inserisci la successiva iterazione immediatamente.

Naturalmente, a volte sembra non esistere alcuna soluzione valida. In tal caso, sei bloccato con le tue risposte alla domanda 1, e il bene è semplicemente il meno male. In questo caso, il valore di prendere il tempo per rispondere a queste domande è che è possibile evitare le iterazioni che sono destinate a fallire. Assicurati di tornare alla codifica entro un lasso di tempo adeguato.

    
risposta data 28.02.2018 - 17:02
fonte
13

Per prima cosa - i pattern sono astrazioni utili, non alla fine tutto si basa sul design, per non parlare del design OO.

In secondo luogo - l'OO moderno è abbastanza intelligente da sapere che non tutto è un oggetto. A volte usando semplici vecchie funzioni, o anche alcuni imperativi script di stile produrranno una soluzione migliore per determinati problemi.

Ora per la carne delle cose:

This gets very difficult when each approach seems to have as many problems as any of the others.

Perché? Quando hai un sacco di opzioni simili, la tua decisione dovrebbe essere più facile! Non perderai molto scegliendo l'opzione "sbagliata". E davvero, il codice non è stato risolto. Prova qualcosa, vedi se è buono. Iterate .

It is also difficult to accept that the code will end up very problematic regardless of which design pattern or approach is used.

Dadi difficili. Problemi difficili: i problemi matematici attuali sono solo difficili. Provabilmente difficile. Non c'è letteralmente nessuna buona soluzione per loro. E si scopre che i problemi facili non sono davvero preziosi.

Ma fai attenzione. Troppo spesso ho visto persone frustrate da nessuna buona opzione perché sono bloccate a guardare il problema in un certo modo, o che hanno tagliato le loro responsabilità in un modo che è innaturale al problema in questione. "Nessuna buona opzione" può essere l'odore del fatto che c'è qualcosa di fondamentalmente sbagliato nel tuo approccio.

This results in a loss of motivation because "clean" code ceases to be an option sometimes.

Perfetto è il nemico del bene. Ottieni qualcosa funzionante, quindi refactoring.

Are there any approaches to design that can help to minimise this problem? Is there an accepted practice for what one should do in a situation like this?

Come ho già detto, lo sviluppo iterativo generalmente minimizza questo problema. Una volta che hai capito qualcosa, hai più familiarità con il problema, mettendoti in una posizione migliore per risolverlo. Hai codice reale da guardare e valutare, non un disegno astratto che non sembra giusto.

    
risposta data 28.02.2018 - 05:10
fonte
8

La situazione che stai descrivendo sembra un approccio dal basso verso l'alto. Prendi una foglia, prova a risolverla e scopri che è connessa a un ramo che è anch'esso connesso a un altro ramo, ecc.

È come provare a costruire una macchina iniziando con una gomma.

Ciò di cui hai bisogno è fare un passo indietro e guardare l'immagine più grande. Come si colloca questa foglia nel design generale? È ancora attuale e corretto?

Come apparirebbe il modulo se dovessi progettarlo e implementarlo da zero? Quanto lontano da questo "ideale" è la tua attuale implementazione.

In questo modo hai una visione più ampia su cosa lavorare. (O se decidi che è troppo lavoro, quali sono i problemi).

    
risposta data 28.02.2018 - 11:42
fonte
4

Il tuo esempio descrive una situazione che si presenta in genere con pezzi più ampi di codice legacy e quando stai cercando di eseguire un refactoring "troppo grande".

Il miglior consiglio che posso darti per questa situazione è:

  • non cercare di raggiungere il tuo obiettivo principale in un "big bang" ,

  • scopri come migliorare il tuo codice in piccoli passaggi!

Ovviamente, è più facile scrivere che fare, quindi come realizzarlo in realtà? Bene, hai bisogno di pratica ed esperienza, dipende molto dal caso, e non esiste una regola rigida che dice "fai questo o quello" che si adatta ad ogni caso. Ma fammi usare il tuo esempio ipotetico. "composition-over-inheritance" non è un obiettivo di design tutto-o-niente, è un candidato perfetto da raggiungere in diversi piccoli passi.

Diciamo che hai notato che "la composizione sull'ereditarietà" è lo strumento giusto per il caso. Ci devono essere delle indicazioni perché questo sia un obiettivo ragionevole, altrimenti non avresti scelto questo. Quindi, supponiamo che ci sia un sacco di funzionalità nella superclasse che è appena "chiamata" dalle sottoclassi, quindi questa funzionalità è un candidato per non rimanere in quella superclasse.

Se si nota che non è possibile rimuovere immediatamente la superclasse dalle sottoclassi, è possibile iniziare prima con il refactoring della superclasse in componenti più piccoli che incapsulano le funzionalità sopra menzionate. Inizia con i frutti a più bassa pendenza, estrai prima alcuni componenti più semplici, che renderanno già la tua superclasse meno complessa. Più piccola è la superclasse, più facili saranno i refactoring aggiuntivi. Utilizza questi componenti dalle sottoclassi e dalla superclasse.

Se sei fortunato, il codice rimanente nella superclasse diventerà così semplice durante questo processo che potrai rimuovere la superclasse dalle sottoclassi senza ulteriori problemi. Oppure noterai che mantenere la superclasse non è più un problema, dal momento che hai già estratto abbastanza del codice in componenti che vuoi riutilizzare senza ereditarietà.

Se non sei sicuro di dove cominciare, perché non sai se un refactoring risulterà essere semplice, a volte l'approccio migliore è fare un scratch refactoring .

Ovviamente, la tua situazione reale potrebbe essere più complicata. Quindi impara, raccogli esperienze e sii paziente, ottenere questo giusto richiede anni. Ci sono due libri che posso raccomandare qui, forse li trovi utili:

risposta data 28.02.2018 - 13:52
fonte
1

A volte i due migliori principi di design sono KISS * e YAGNI **. Non sentire il bisogno di stipare tutti i pattern di design conosciuti in un programma che deve solo stampare "Hello world!".

 * Keep It Simple, Stupid
 ** You Ain't Gonna Need It

Modifica dopo l'aggiornamento della domanda (e il mirroring di ciò che Pieter B dice in una certa misura):

A volte prendi presto una decisione architettonica che ti porta a un particolare design che porta a tutti i tipi di bruttezza quando cerchi di implementarlo. Sfortunatamente, a quel punto, la soluzione "corretta" è di fare un passo indietro e capire come sei arrivato a quella posizione. Se non riesci ancora a vedere la risposta, continua a fare un passo indietro finché non lo fai.

Ma se il lavoro da fare sarebbe sproporzionato, ci vuole una decisione pragmatica per trovare la soluzione meno brutta al problema.

    
risposta data 28.02.2018 - 09:53
fonte
1

Quando sono in questa situazione, la prima cosa che faccio è smettere. Passo a un altro problema e ci lavoro su per un po '. Forse un'ora, forse un giorno, forse di più. Non è sempre un'opzione, ma il mio subconscio lavorerà sulle cose mentre il mio cervello cosciente fa qualcosa di più produttivo. Alla fine, torno ad esso con occhi nuovi e riprovo.

Un'altra cosa che faccio è chiedere a qualcuno più intelligente di me. Questo può assumere la forma di chiedere su Stack Exchange, leggere un articolo sul web sull'argomento o chiedere ad un collega che abbia più esperienza in questo settore. Spesso il metodo che ritengo sia l'approccio giusto si rivela completamente sbagliato per quello che sto tentando di fare. Ho interpretato in modo errato alcuni aspetti del problema e in realtà non si adatta al modello che penso che faccia. Quando ciò accade, avere qualcun altro dice "Sai, questo sembra più ..." può essere di grande aiuto.

Relativo a quanto sopra è il design o il debugging per confessione. Vai da un collega e dì: "Sto per dirti il problema che sto avendo, e poi ti spiegherò le soluzioni che ho. Segnala problemi in ogni approccio e suggerisci altri approcci ". Spesso prima che l'altra persona parli, come sto spiegando, comincio a rendermi conto che un percorso che sembrava uguale agli altri è in realtà molto meglio o peggio di quanto pensassi in origine. La conversazione risultante potrebbe rinforzare quella percezione o indicare nuove cose a cui non avevo pensato.

Quindi TL; DR: prenditi una pausa, non forzarla, chiedi aiuto.

    
risposta data 01.03.2018 - 04:32
fonte