Aggiungendo complessità generalizzando: quanto lontano dovresti andare? [duplicare]

18

Domanda di riferimento: link

La domanda di cui sopra ha chiesto di risolvere un problema per una matrice NxN. Mentre c'era una soluzione facile, ho dato una soluzione più generale per risolvere il problema più generale per una matrice NxM. Una manciata di persone ha commentato che questa generalizzazione è stata negativa perché ha reso la soluzione più complessa. Uno di questi commenti è votato +8.

Mettendo da parte gli effetti di voto difficili da spiegare su SO, ci sono due tipi di complessità da considerare qui:

  1. Complessità di runtime, ovvero quanto velocemente il codice viene eseguito
  2. Complessità del codice, cioè quanto è difficile leggere e capire il codice

La questione della complessità del tempo di esecuzione è qualcosa che richiede una migliore comprensione dei dati di input oggi e di come potrebbe apparire in futuro, tenendo conto dei vari fattori di crescita laddove necessario.

La questione della complessità del codice è quella a cui sono interessato qui. Generalizzando la soluzione, evitiamo di doverla riscrivere nel caso in cui i vincoli cambino. Tuttavia, allo stesso tempo può spesso complicare il codice. Nella domanda di riferimento, il codice per NxN è facilmente comprensibile per qualsiasi programmatore competente, ma il caso NxM (a meno che non sia documentato bene) potrebbe facilmente confondere qualcuno che si imbatte nel codice per la prima volta.

Quindi, la mia domanda è questa: dove dovresti tracciare la linea di demarcazione tra generalizzare e mantenere il codice di facile comprensione?

    
posta marcog 07.01.2011 - 15:03
fonte

15 risposte

18

La tua domanda è pericolosa. Supponendo che la generalità sia l'opposto della semplicità.

Poiché questa domanda, in parte, riguarda la semantica, definiamo i termini.

Generale - applicabile a una varietà di situazioni, robusto, utile Semplice: facile da capire, ovvio, con poche parti

Questi sono in realtà due assi. L'opposto del generale è specifico . L'opposto di semplice è complicato (non complesso).

Le migliori soluzioni di programmazione sono sia semplici che ampli; Generale.

Usare un linguaggio bene è l'arte stessa di rendere semplice la soluzione generale.

Dato che stai chiedendo qualcosa di soggettivo, ecco la mia opinione. La tua soluzione al problema è generale, ma non è semplice. Anche altre soluzioni, come quelle che richiedono intuizione su 0 o 1 nella matrice, non sono semplici. In effetti, qualsiasi soluzione descritta in termini di algoritmo per trovare la soluzione non è semplice.

Questo è un errore che molti programmatori (la maggior parte?) fanno. Decompongono il problema nelle loro teste, ma non condividono tale decomposizione con il codice. In questa domanda, la prima cosa che si dovrebbe fare per rendere evidente a un nuovo lettore del codice è "Cosa stiamo facendo"? Stiamo trasformando le righe in numeri? Stiamo navigando in una griglia di bit? Stiamo ragionando sulle proprietà della matrice? Rendilo esplicito. Ciò impedisce a un nuovo lettore di dover decodificare le idee ogni volta che esamina il codice.

La tua soluzione è confrontare le righe .

È possibile aggiungere al codice la nozione secondo cui si confrontano le righe prima che sia necessario preoccuparsi se si tratta di NxN o NxM. Questi dettagli vengono gestiti nel modo in cui si confrontano le righe, ma a quel punto il problema è cambiato da una matrice a due righe (un problema più semplice). Successivamente, anche il confronto di due righe semplifica il confronto di due bit in quelle righe.

Interfacce fluenti, codice dichiarativo, ecc., tutti cercano di affrontare questi problemi. I compilatori stanno migliorando sempre di più nell'integrare l'efficienza (anche i processori). I giorni di dover scrivere un algoritmo di loop di basso livello intelligente per l'efficienza stanno andando via.

Considera un codice come questo:

Row smallest = matrix.FirstRow
for each Row r in matrix.rows
 if r < smallest
   smallest = r

L'orientamento degli oggetti consente di memorizzare tutti i tipi di stato in un oggetto Row. ciò consente di incorporare gli stratagemmi nella soluzione nell'oggetto Row (incluso il trucco 'which row am I on'; l'operatore < può essere sovraccaricato per fare cose come la colonna 0 o 1 confronta su quella riga). L'efficienza può essere fatta allo stesso modo, pur rendendo semplice (ovvio) cosa sta succedendo.

Ancora prima di parlare di implementare la 'ricerca' per la riga migliore, ci sono un sacco di informazioni nel setup della matrice stessa (righe ordinate, ecc.) In effetti, se fosse C #, scriverei semplicemente la ricerca come matrix.Rows.Min () e crea una rappresentazione di Matrix e Rows che prende queste assunzioni nella sua definizione. Ciò rende questi invarianti configurati dalla domanda implicita nel codice invece che solo "conosciuti" dalle supposizioni difficili che l'algoritmo di loop può fare.

Una volta che lo fai, vedrai accadere ogni genere di cose belle. I trucchi dicevano di cercare solo il 0000, o 0001 sarà ovvio perché sarete in grado di ottimizzare la struttura della matrice stessa (pensate alla matrice di NumberOf0 [RowNumber]). L'ultimo bit è "diventare complicato" ma almeno è la complessità in che rappresenta i dati di input e non ancora cercare la soluzione .

La decomposizione fornisce sia la generalità che la potenza, mantenendo le cose semplici.

Link:

link

link

    
risposta data 29.01.2011 - 07:17
fonte
19

Semplice è il re. Ogni bit di complessità che viene aggiunto deve giustificare la sua esistenza. Come migliora il codice? Questo vantaggio vale la complessità aggiunta? Ci avviciniamo sempre aggiungendo più complessità come un atto malvagio al codice. Solo dopo aver determinato che il codice non può davvero vivere senza di esso, è opportuno aggiungerlo.

Se non puoi quantificare e giustificare la complessità, allora lascia perdere.

    
risposta data 07.01.2011 - 15:30
fonte
13

La regola generale che seguo è generalizzare il codice solo dopo averlo riutilizzato alcune volte. Questo di solito è maggiore di 3 volte. La maggiore complessità implicata nella generalizzazione dei primi non vale la pena.

    
risposta data 07.01.2011 - 15:18
fonte
6

Prima di tutto, metterei in discussione l'assunto che ci sia necessariamente una linea tra generalizzazione e comprensibilità. In questo caso particolare, sembra che tu stia discutendo di un algoritmo ben noto, quindi non vedo davvero nulla di sbagliato nella generalizzazione.

Come per la maggior parte delle cose nella programmazione, ci sono dei compromessi da prendere in considerazione. La cosa più importante è che stai pensando al quadro generale. È molto raro che tu abbia un pezzo di codice come questo che fluttua nel vuoto. In questo caso particolare, la complessità che stai aggiungendo è molto localizzata. Non è ancora l'ideale, ma la complessità è molto più facile da gestire se riesci a chiuderla nella sua piccola area. Quando la complessità inizia a influenzare altri pezzi di codice, ha la tendenza a moltiplicarsi e sfuggire al controllo molto rapidamente.

Quindi, dal momento che sei:

  • Implementazione di un algoritmo ben noto
  • Localizzazione della complessità

... dico di andare per questo. Se riesci a risolvere un potenziale problema in anticipo senza troppo sforzo e una complessità minima, non c'è alcun motivo per non farlo.

    
risposta data 07.01.2011 - 16:10
fonte
5

In questo caso la tua soluzione generale è molto più semplice. Né utilizza l'assunzione di unicità né l'assunzione di ortogonalità che significa concettualmente è molto più facile da capire. La soluzione generale dipende semplicemente dalla struttura dei vettori di bit di una certa forma e chiunque può verificare che il tuo codice funzioni semplicemente osservando 2 casi di matrici 2xM perché l'algoritmo di attraversamento del percorso che hai descritto deve solo guardare la riga successiva e quindi si dirama in base a ciò che vede. Mentre la soluzione più specifica per il caso NxN richiede un quadro più globale perché l'unicità è una proprietà più globale e non vedo come la comprensione della soluzione nel caso più semplice possibile si generalizzi al caso NxN. Probabilmente ti saresti allontanato se non avessi detto che la tua soluzione era più generale. Alcune persone raggiungono istintivamente il capo della monaca quando vedono le parole "più generali".

    
risposta data 07.01.2011 - 19:51
fonte
3

Direi che la linea viene tracciata nel punto in cui vi è una probabile possibilità che debba essere generalizzata. La motivazione entra in gioco qui: stai generalizzando perché vuoi vedere un pezzo di codice chiarificatore, generale, man-am-I-cool, o perché puoi effettivamente vedere un buon caso d'uso del mondo reale per la versione generalizzata?

Penso che una frase chiave qui sia "a meno che non sia documentata bene". Una soluzione più generalizzata, se ben commentata, può ancora essere gestibile e comprensibile. Non credo nelle cose stupide per i programmatori incompetenti che potrebbero seguire. Se commentate bene la soluzione, i programmatori più solidi dovrebbero essere in grado di seguirvi e capire cosa state facendo.

Allo stesso tempo, se una generalizzazione non ha senso, allora stai utilizzando risorse senza motivo. Generalizzare rimuovendo il contraint NxN può essere una buona idea, ma la generalizzazione di più dimensioni (NxMxKx ...) probabilmente non lo sarebbe, poiché si tratta di una modifica molto meno probabile dello spazio di lavoro. (So che più dimensioni non hanno senso nel contesto della domanda originale, ma volevo solo fornire qualche esempio di out-there).

Anche in attesa di refactoring, nella mia mente, non è una buona soluzione, se si tratta di codice in cui vi è una possibilità decente ne avrete bisogno. Questo aggiunge solo del lavoro extra in quanto dovrai scrivere l'intera cosa due volte, aggiornare la documentazione, inviare nuove versioni, ecc.

In breve, un po 'di preveggenza fa molto.

    
risposta data 25.01.2011 - 17:48
fonte
2

Mentre sono d'accordo con la risposta di MattiasK (+1 da me), ho osservato molte volte che la codifica tenendo presente il caso generale aiuta a scrivere codice più pulito, perché il caso generale può richiedere che i concetti siano definiti con più precisione, il che aiuta a scrivere codici più specifici in modo corretto.

    
risposta data 07.01.2011 - 15:16
fonte
2

Sembra un po 'come una generalizzazione prematura, di solito è meglio mantenere le cose semplici se non sei abbastanza sicuro di aver bisogno di qualcosa

Se è applicabile e sei davvero preoccupato delle estensioni future, forse puoi racchiuderlo in un modello di strategia o qualcosa di simile

Il codice dovrebbe essere reso il più semplice possibile, ma non più semplice:)

Ho un'eccezione alla regola di generalizzazione e questo è quando qualcosa è molto generale (il che significa che può potenzialmente essere usato in diversi progetti) e hai un mucchio di codice correlato, quindi tendo a costruire una libreria di utilità che aggiungo a la mia "cassetta degli attrezzi". Se qualche progetto ha bisogno di qualche aggiunta che rientri in una di queste librerie di strumenti, posso invece aggiungerle alla lib, tende a velocizzare molto lo sviluppo dopo un paio d'anni:)

    
risposta data 07.01.2011 - 15:11
fonte
2

Hai mai sentito parlare di YAGNI ?

Potresti anche generalizzare la soluzione per lavorare con le matrici dei float. O potresti rimuovere il vincolo dell'unicità. Qual è il punto? A meno che non ci sia un motivo reale per credere che questo requisito verrà comunque, è solo una perdita di tempo.

Quando si traccia la linea tra generalizzare e mantenere le cose semplici, il posto giusto non compromette il mantenimento delle cose semplici.

Scrivere un buon codice, vale a dire mantenibile, flessibile, estensibile e semplice, non si ottiene generalizzando i singoli componenti per soddisfare una moltitudine di problemi che non è necessario risolvere, ma strutturando adeguatamente i programmi e garantendo un accoppiamento molto basso da parte un uso sano dell'astrazione e dell'incapsulamento.
Per illustrare ciò: non importa, se il tuo algoritmo fa l'ipotesi, che la matrice sia NxN o no, perché per essere onesti, non è molto da riscrivere il codice. È molto più importante, che il resto del codice non si rompa, perché quell'assunzione non è più valida ed è necessario riscrivere quel bit.

Se dovessi andare a un combattimento ravvicinato armato, preferirei avere un club, piuttosto che un coltellino svizzero.

    
risposta data 07.01.2011 - 17:31
fonte
1

Non mi preoccupo di generalizzare il codice almeno fino al secondo caso d'uso. Può essere difficile prevedere come verrà utilizzato il codice in futuro, il che rende più difficile determinare in anticipo ciò che dovrà essere generalizzato. Se hai un esempio a portata di mano, questo rende il processo molto più semplice e accurato.

    
risposta data 07.01.2011 - 15:27
fonte
1

Gran parte delle risposte finora non riflettono completamente la mia opinione.

La cosa più importante della scrittura del codice è accettare che non si scriva codice in isolamento. Potresti avere altri sviluppatori nella tua stessa squadra. Altri sviluppatori potrebbero aver bisogno di usare il tuo codice come libreria e, una volta passati, altri sviluppatori manterranno ed estenderanno il tuo codice.

Quando inizi a pensare ai tuoi compagni di squadra, concetti come soluzioni coerenti per problemi simili, l'uso di tecniche che vengono capite, piuttosto che la sintassi C oscura ti aiuteranno a decidere come gestire la complessità.

Il mio pensiero finale su questo però è pensare ai diversi utenti / spettatori del tuo codice. Le persone che usano le tue lezioni apprezzeranno la semplicità d'uso che crei facendo internamente tutte le cose complesse. Il programmatore di manutenzione potrebbe tuttavia scoprire che avere il codice distribuito su diverse classi per disorientare quando cerca di rintracciare un bug.

    
risposta data 07.01.2011 - 15:57
fonte
0

È difficile rispondere senza una comprensione completa dello scenario. Potrebbe essere che quella funzione verrà utilizzata in una (diciamo) 3d vg dove sappiamo quasi sicuramente ne avremo bisogno di una generica nel prossimo futuro. Oppure potrebbe essere utilizzato per alcuni calcoli che richiedono solo matrici quadrate.

In caso di dubbio cerco di usare il principio Yagni sopra menzionato, ma nel caso più generale per me il principio OpenClosed è molto utile:

"le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte per l'estensione, ma chiuse per la modifica"

link

    
risposta data 27.01.2011 - 12:32
fonte
0

La risposta è semplice nella programmazione orientata agli oggetti: se la tua classe fa più di una cosa, continua a generalizzare. link

Seguendo l'SRP migliorerà anche espressività / leggibilità. Il tuo codice sarà dichiarativo, piuttosto che imperativo. Il risultato è un codice che è più facile da capire e mantenere nonostante la supposta complessità. Ogni componente è semplice e diretto, anche se l'interazione del tutto diventa qualcosa di più grande.

Detto questo, non ingegnare le soluzioni per risolvere problemi che ancora non hai.

    
risposta data 28.01.2011 - 06:04
fonte
0

La domanda dell'intervista è stata probabilmente progettata per testare che l'oggetto avrebbe notato che c'erano solo n + 1 possibili righe diverse; se una riga finisce con uno zero, sarebbe la più piccola. Altrimenti la riga più piccola sarebbe 000 ... 001. Sebbene in generale sia preferibile progettare metodi che funzionino con matrici NxM di dimensioni arbitrarie piuttosto che NxN, tale ipotesi si basa sul fatto che la maggior parte dei metodi ha senso solo con le matrici quadrate oppure non si preoccupa molto della forma della matrice . Una soluzione efficiente per il problema generale sarà molto più complessa del codice che verifica semplicemente se l'ultima colonna contiene zero.

Per usare un'analogia, supponiamo che un programma riceva un elenco di 999.999 numeri distinti da 1 a 1.000.000 e ha il compito di identificare il numero "mancante". L'algoritmo più semplice sarebbe quello di "xor" insieme tutti i numeri e confrontare tale risultato con lo xor dei numeri da 1 a 1.000.000. Se la lista avesse solo 999.998 numeri distinti, trovare anche solo uno dei numeri mancanti sarebbe molto più difficile, ma non è necessario un algoritmo in grado di gestire quel caso.

    
risposta data 03.09.2014 - 04:54
fonte
-1

Non so chi sia stato il fatto che detta complessità del codice si riduca con il quadrato del numero di funzioni, ma ciò sembra rilevante qui. Il tempo di sviluppo e la complessità sono entrambe spese, le seconde da pagare in futuro, e qualsiasi investimento dovrebbe essere giustificato almeno da una spiegazione plausibile del perché questa funzionalità sarà necessaria in seguito.

Una via d'oro centrale sarebbe quella di sviluppare con le future estensioni in mente, usando solo i nomi e le strutture più chiari. Questo sarebbe un doppio vantaggio: la semplicità del codice sarebbe vantaggiosa sia per la manutenzione che per l'estensione.

    
risposta data 27.01.2011 - 09:32
fonte

Leggi altre domande sui tag