Come evitare iterazioni infinite di progetti ugualmente non ottimali?


10

Quindi, probabilmente come molti, mi trovo spesso ad avere 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 avvertimento che rende difficile implementare il modello / approccio senza un qualche tipo di lavoro attorno al quale poi nega il beneficio del modello / approccio. Posso facilmente finire per iterare attraverso molti schemi / approcci perché prevedibilmente quasi tutti hanno alcuni avvertimenti molto significativi in ​​situazioni del mondo reale in cui semplicemente non c'è una soluzione facile.


Esempio:

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

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

Quindi dopo questo potrei semplicemente scegliere di usare osservatori o delegati completamente per evitare gli svantaggi che derivano dal mescolare pesantemente gli approcci. Tuttavia, questo ha i suoi problemi. Ad esempio, potrei scoprire che alla fine ho bisogno di osservatori o delegati per una quantità crescente di comportamenti fino a quando non ho 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. Quindi il prossimo, quello dopo ecc.


Questo processo iterativo diventa molto difficile quando ogni approccio sembra avere tanti problemi quanti ne sono gli altri e porta a una sorta di paralisi delle decisioni di progettazione . È anche difficile accettare che il codice finisca allo stesso modo problematico indipendentemente da quale modello di progettazione o approccio viene utilizzato. Se finisco in questa situazione, significa che il problema stesso deve essere ripensato? Cosa fanno gli altri quando incontrano questa situazione?

Modifica: sembra che ci siano diverse interpretazioni della domanda che voglio chiarire:

  • Ho completamente eliminato OOP dalla domanda perché si scopre che in realtà non è specifico di OOP, inoltre è troppo facile interpretare erroneamente alcuni dei commenti che ho fatto passando per OOP.
  • Alcuni hanno affermato che dovrei adottare un approccio iterativo e provare schemi diversi o che dovrei scartare uno schema quando smette 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.

3
Non "iscriverti alla filosofia OO". È uno strumento, non una grande decisione di vita. Usalo quando aiuta e non usarlo quando non lo fa.
user253751

Se sei bloccato a pensare in termini di determinati schemi, forse prova a scrivere qualche progetto moderatamente complesso in C, sarà interessante sperimentare quanto puoi fare senza quegli schemi.
user253751

2
Oh C ha dei modelli. Sono solo schemi molto diversi. :)
candied_orange,

Ho chiarito la maggior parte di questi fraintendimenti nella modifica
Jonathan,

Avere questi problemi compaiono una volta ogni tanto è normale. Farli accadere spesso non dovrebbe essere il caso. Molto probabilmente, stai usando il paradigma di programmazione sbagliato per il problema o i tuoi progetti sono una miscela di paradigmi nei posti sbagliati.
Dunk

Risposte:


8

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

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

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

  3. Ancora più importante:
    quali sono esattamente i miei requisiti? Non sono i requisiti sulla carta, quelli reali fondamentali?
    Posso in qualche modo riformulare il problema / modificare i suoi requisiti, in modo che consenta una soluzione semplice e diretta?
    Posso rappresentare i miei dati in modo diverso, in modo da consentire una soluzione così semplice e diretta?

    Queste sono le domande sui fondamenti del problema percepito. Si può scoprire che stavi effettivamente cercando di risolvere il problema sbagliato. Il 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 prima di continuare. Fai una passeggiata, fai un passo nel tuo ufficio, fai tutto il necessario per rimuginare sulle risposte a queste domande. Tuttavia, rispondere a queste domande non dovrebbe richiedere quantità di tempo improprie. I tempi corretti possono variare da 15 minuti a qualcosa come una settimana, dipendono dalla cattiveria delle soluzioni che hai già trovato e dal suo impatto nel complesso.

Il valore di questo approccio è che a volte troverai soluzioni sorprendentemente buone. Soluzioni eleganti. Le soluzioni meritano il tempo che hai investito nel rispondere a queste tre domande. E non troverai quella soluzione se inserisci semplicemente la prossima iterazione immediatamente.

Certo, a volte non sembra esistere una buona soluzione. In tal caso, sei bloccato con le tue risposte alla prima domanda e il bene è semplicemente il meno cattivo. In questo caso, il valore di dedicare del tempo a rispondere a queste domande è che è possibile evitare iterazioni che sono destinate a fallire. Assicurati di tornare alla codifica entro un periodo di tempo adeguato.


Potrei anche contrassegnare questa come risposta, questo ha più senso per me e sembra esserci un consenso sul semplice rivalutazione del problema stesso.
Jonathan,

Mi piace anche il modo in cui lo hai suddiviso in passaggi, quindi esiste una sorta di procedura libera per valutare la situazione.
Jonathan,

OP, ti vedo bloccato nella meccanica della soluzione del codice, quindi sì "potresti anche contrassegnare questa risposta". Il miglior codice che abbiamo scritto è stato dopo un'attenta analisi dei requisiti, dei requisiti derivati, dei diagrammi di classe, delle interazioni di classe, ecc. Per fortuna abbiamo resistito a tutto il clamore dei posti economici (leggi gestione e collaboratori): "Tu stai spendendo troppo tempo nel design "," sbrigati e inizia a scrivere codice! "," sono troppe lezioni! ", ecc. Una volta arrivati, programmare è stato una gioia, più che divertente. Obiettivamente quello era il miglior codice nell'intero progetto.
radarbob

13

Per prima cosa, i modelli sono astrazioni utili, non tutto alla fine del design, per non parlare del design OO.

In secondo luogo, la moderna OO è abbastanza intelligente da sapere che non tutto è un oggetto. A volte l'uso di semplici vecchie funzioni o persino di alcuni script di stile imperativo produrrà una soluzione migliore per determinati problemi.

Ora alla carne delle cose:

Questo diventa molto difficile quando ogni approccio sembra avere tanti problemi quanti ne sono gli altri.

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 è fisso. Prova qualcosa, vedi se è buono. Iterate .

È anche difficile accettare che il codice finirà per essere molto problematico indipendentemente da quale modello di progettazione o approccio viene utilizzato.

Noci Dure. Problemi difficili: i veri problemi matematicamente difficili sono solo difficili. Provvidentemente difficile. Non c'è letteralmente una buona soluzione per loro. E si scopre che i problemi facili non sono davvero preziosi.

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

Ciò si traduce in una perdita di motivazione perché il codice "pulito" cessa di essere un'opzione a volte.

Perfetto è il nemico del bene. Fai funzionare qualcosa, quindi riformattalo.

Ci sono approcci alla progettazione che possono aiutare a minimizzare questo problema? Esiste una pratica accettata per ciò che si dovrebbe fare in una situazione come questa?

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


Ho pensato di dire che ho eliminato alcune interpretazioni errate nella modifica. In genere faccio l'approccio "get it working" e refactor. Tuttavia, spesso ciò porta a molti debiti tecnici nella mia esperienza. Ad esempio, se riesco solo a far funzionare qualcosa, ma poi io o altri ci costruiamo sopra, alcune di queste cose potrebbero avere inevitabili dipendenze che quindi necessitano anche di un sacco di refactoring. Ovviamente non puoi sempre saperlo in anticipo. Ma se sai già che l'approccio è imperfetto, dovresti farlo funzionare e quindi rifrangere iterativamente prima di costruire sul codice?
Jonathan,

@Jonathan - tutti i design sono una serie di compromessi. Sì, dovresti iterare immediatamente se il design è difettoso e hai un modo chiaro per migliorarlo. Se non ci sono chiari miglioramenti, smetti di fottere.
Telastyn,

"hanno tagliato le loro responsabilità in un modo innaturale per il problema in questione. Nessuna buona opzione può essere un odore che c'è qualcosa di fondamentalmente sbagliato nel tuo approccio." Scommetto su questo. Alcuni sviluppatori non incontrano mai problemi descritti nel PO e altri sembrano farlo abbastanza frequentemente. Spesso la soluzione non è facile da vedere quando viene proposta dallo sviluppatore originale perché ha già distorto il design nel modo in cui inquadra il problema. A volte l'unico modo per trovare la soluzione abbastanza ovvia è ricominciare dai requisiti per la progettazione.
Dunk

8

La situazione che stai descrivendo sembra un approccio dal basso verso l'alto. Prendi una foglia, provi a ripararla e scopri che è collegata a un ramo che è anch'esso collegato a un altro ramo ecc.

È come provare a costruire un'auto a partire da una gomma.

Ciò di cui hai bisogno è fare un passo indietro e guardare il quadro generale. Come si colloca quella foglia nel disegno complessivo? È ancora attuale e corretto?

Come sarebbe il modulo se lo progettaste e lo implementaste da zero? Quanto è lontana da questo "ideale" la tua attuale implementazione.

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


4

Il tuo esempio descrive una situazione che si presenta in genere con pezzi di codice legacy più grandi e quando stai cercando di effettuare 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!

Certo, è più facile da scrivere che da fare, quindi come realizzarlo nella 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 a ogni caso. Ma lasciami usare il tuo esempio ipotetico. "composizione-sopra-ereditarietà" non è un obiettivo di progettazione tutto o niente, è un candidato perfetto da raggiungere in pochi piccoli passi.

Supponiamo che tu abbia notato che "composizione per ereditarietà" è lo strumento giusto per il caso. Devono esserci delle indicazioni perché questo sia un obiettivo ragionevole, altrimenti non lo avresti scelto. Supponiamo quindi che ci siano molte funzionalità nella superclasse che viene semplicemente "chiamata" dalle sottoclassi, quindi questa funzionalità è un candidato per non rimanere in quella superclasse.

Se noti che non puoi rimuovere immediatamente la superclasse dalle sottoclassi, puoi iniziare prima con il refactoring della superclasse in componenti più piccoli che incapsulano la funzionalità sopra menzionata. Inizia con i frutti più bassi, estrai prima alcuni componenti più semplici, il che renderà già la tua superclasse meno complessa. Più piccola diventa la superclasse, più facili saranno i refactoring aggiuntivi. Utilizzare questi componenti dalle sottoclassi e dalla superclasse.

Se sei fortunato, il codice rimanente nella superclasse diventerà così semplice durante questo processo che puoi 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 desideri riutilizzare senza ereditarietà.

Se non sei sicuro da dove iniziare, perché non sai se un refactoring si rivelerà semplice, a volte l'approccio migliore è quello di eseguire un refactoring scratch .

Certo, la tua situazione reale potrebbe essere più complicata. Quindi impara, raccogli esperienza ed abbi pazienza, per ottenere ciò giusto ci vogliono anni. Ci sono due libri che posso consigliare qui, forse li trovi utili:

  • Refactoring di Fowler: descrive un catalogo completo di refactoring molto piccoli .

  • Lavorare efficacemente con il codice legacy di Feathers: fornisce ottimi consigli su come gestire grossi blocchi di codice mal progettato e renderlo più testabile in passaggi più piccoli


1
Anche questo è molto utile. Un piccolo refactoring graduale o graffiante è una buona idea perché diventa qualcosa che può essere progressivamente completato insieme ad altri lavori senza ostacolarlo troppo. Vorrei poter approvare più risposte!
Jonathan,

1

A volte i migliori due principi di progettazione sono KISS * e YAGNI **. Non sentire la necessità di stipare ogni modello di design noto 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 rispecchiando in una certa misura ciò che dice Pieter B):

A volte prendi presto una decisione architettonica che ti porta a un design particolare che porta a tutti i tipi di bruttezza quando provi a implementarlo. Sfortunatamente, a quel punto, la soluzione "corretta" è 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 fino a quando non lo fai.

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


Ho chiarito un po 'di questo nella modifica, ma in generale mi riferisco esattamente a quei problemi in cui non è disponibile una soluzione semplice.
Jonathan,

Ah, sì, sembra che stia emergendo un consenso sul fatto di fare un passo indietro e rivalutare il problema. Questa è una buona idea.
Jonathan,

1

Quando sono in questa situazione, la prima cosa che faccio è fermarmi. Passo a un altro problema e ci lavoro 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, ci torno 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 a un collega che ha più esperienza in questo settore. Spesso il metodo che ritengo sia l'approccio giusto risulta completamente sbagliato per quello che sto tentando di fare. Ho mal interpretato alcuni aspetti del problema e in realtà non si adatta allo schema, penso che lo faccia. Quando ciò accade, avere qualcun altro che dice "Sai, sembra più simile a ..." può essere di grande aiuto.

Relativo a quanto sopra è la progettazione o il debug per confessione. Vai da un collega e dici: "Ti dirò il problema che sto avendo, e poi ti spiegherò le soluzioni che ho. Indichi i problemi in ogni approccio e suggerisci altri approcci ". Spesso prima ancora che l'altra persona parli, mentre sto spiegando, inizio a rendermi conto che un percorso che sembrava uguale agli altri è in realtà molto migliore o peggiore di quanto pensassi inizialmente. La conversazione che ne risulta può rafforzare questa percezione o evidenziare cose nuove a cui non ho pensato.

Quindi TL; DR: fai una pausa, non forzarlo, chiedi aiuto.

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.