2010-08-27 5 views
5

Torna storia:Refactoring .NET, ASCIUTTO. doppia eredità, l'accesso ai dati e la separazione degli interessi

così mi è stato bloccato su un problema di architettura da un paio di notti su un refactoring che ho accarezzato. Niente di importante, ma mi ha infastidito. In realtà è un esercizio in DRY e un tentativo di portarlo a un livello così estremo come l'architettura DAL è completamente SECCO. È un esercizio completamente filosofico/teorico.

Il codice è basato in parte su uno dei refactoring di @JohnMacIntyre che recentemente l'ho convinto a scrivere su http://whileicompile.wordpress.com/2010/08/24/my-clean-code-experience-no-1/. Ho modificato leggermente il codice, come tendo, al fine di portare il codice un ulteriore livello - di solito, solo per vedere quale chilometraggio extra posso ottenere da un concetto ... comunque, le mie ragioni sono in gran parte irrilevanti.

Parte del mio strato di accesso ai dati si basa sulla seguente architettura:

abstract public class AppCommandBase : IDisposable { } 

Questo contiene cose di base, come la creazione di un oggetto di comando e di pulitura dopo l'AppCommand viene smaltito. Tutti i miei oggetti di comando derivano da questo.

abstract public class ReadCommandBase<T, ResultT> : AppCommandBase 

Questo contiene cose di base che colpisce tutti i read-comandi - in particolare in questo caso, la lettura dei dati da tabelle e viste. Nessuna modifica, nessun aggiornamento, nessun salvataggio.

abstract public class ReadItemCommandBase<T, FilterT> : ReadCommandBase<T, T> { } 

Questo contiene alcune cose generiche più di base - come la definizione di metodi che saranno necessarie per leggere un singolo elemento da una tabella del database, in cui il nome della tabella, il nome campo chiave ed elenco campo nomi sono definiti come richieste proprietà astratte (da definire dalla classe derivata.

public class MyTableReadItemCommand : ReadItemCommandBase<MyTableClass, Int?> { } 

Questo contiene proprietà specifiche che definiscono il mio nome della tabella, l'elenco dei campi della tabella o vista, il nome del campo chiave, un metodo per analizzare i dati fuori dalla riga IDataReader nel mio oggetto business e un metodo che avvia l'intero processo

Ora, ho anche questa struttura per il mio ReadList ...

abstract public ReadListCommandBase<T> : ReadCommandBase<T, IEnumerable<T>> { } 
public class MyTableReadListCommand : ReadListCommandBase<MyTableClass> { } 

La differenza è che le classi elenco contengono le proprietà che appartengono alla lista generazione (vale a dire Page Start, PageSize, Sort e restituisce un oggetto IEnumerable) rispetto al ritorno di un singolo DataObject (che richiede solo un filtro che identifica un record univoco).

Problema:

sto odiando che ho un sacco di immobili a mia classe MyTableReadListCommand che sono identici nella mia classe MyTableReadItemCommand. Ho pensato di spostarli in una classe helper, ma mentre ciò potrebbe centralizzare i contenuti dei membri in un posto, avrò comunque membri identici in ciascuna classe, che invece puntano alla classe helper, che ancora non mi piace.

Il mio primo pensiero era la duplice ereditarietà che avrebbe risolto questo problema, anche se sono d'accordo sul fatto che la doppia ereditarietà sia di solito un odore di codice - ma risolverebbe questo problema in modo molto elegante. Quindi, dato che .NET non supporta la doppia ereditarietà, da dove vado?

Forse un altro refactor sarebbe più adatto ...ma sto avendo problemi a sistemare la mia mente su come superare questo problema.

Se qualcuno ha bisogno di una base di codice completa per vedere su cosa sto insistendo, ho una soluzione prototipo sul mio DropBox allo http://dl.dropbox.com/u/3029830/Prototypes/Prototype%20-%20DAL%20Refactor.zip. Il codice in questione si trova nel progetto DataAccessLayer.

P.S. Questo non fa parte di un progetto attivo in corso, è più un puzzle refactoring per il mio stesso divertimento.

Grazie in anticipo gente, lo apprezzo.

+0

Per riferimento, la soluzione non vi piace è conosciuto come il modello di delega: http://en.wikipedia.org/wiki/Delegation_pattern. –

+0

anche: http://stackoverflow.com/questions/1224830/difference-between-strategy-pattern-and-delegation-pattern.Mi piace l'approccio di mson sul modello: "la delega implica che invece di avere un singolo oggetto responsabile di tutto, delega le responsabilità ad altri oggetti, il motivo per cui questa è una tecnica comune è che impone due principi ancora più fondamentali dello sviluppo del software riducendo l'accoppiamento e aumentando la coesione. " –

+0

Non sono sicuro se sia meglio abbandonare questo progetto e inventarne uno completamente nuovo, o rifattorizzarlo per inventare qualcosa che abbia senso. L'uso crescente di generici come parametri di tipo all'interno di una definizione generica è un segno sicuro che stai seguendo una linea molto sottile tra chiarezza e follia. Sono tentato di dire "usa NHibernate ..." oh aspetta lì l'ho detto ... –

risposta

4

Separare l'elaborazione dei risultati dal recupero dei dati. La tua gerarchia di ereditarietà è già abbastanza profonda in ReadCommandBase.

Definire un'interfaccia IDatabaseResultParser. Implementa ItemDatabaseResultParser e ListDatabaseResultParser, entrambi con un parametro costruttore del tipo ReadCommandBase (e magari convertirlo anche in un'interfaccia).

Quando si chiama IDatabaseResultParser.Value() esegue il comando, analizza i risultati e restituisce un risultato di tipo T.

I suoi comandi concentrarsi sul recupero dei dati dal database e restituirli come tuple di qualche descrizione (Tuple effettive o e array di matrici, ecc. ecc.), il parser si concentra sulla conversione delle tuple in oggetti di qualunque tipo sia necessario. Vedi NHibernates IResultTransformer per un'idea di come questo può funzionare (ed è probabilmente un nome migliore di IDatabaseResultParser).

Composizione di favore sull'eredità.

Dopo aver guardato il campione andrò ancora di più ...

  1. Buttare via AppCommandBase - non aggiunge valore alla vostra gerarchia di ereditarietà, come tutto ciò che fa è controllare che la connessione non è nullo e aperto e crea un comando.
  2. Creazione separata di query dall'esecuzione della query e analisi dei risultati - ora è possibile semplificare notevolmente l'implementazione dell'esecuzione della query poiché è un'operazione di lettura che restituisce un'enumerazione di tuple o un'operazione di scrittura che restituisce il numero di righe interessate.
  3. Il generatore di query potrebbe essere racchiuso in una classe per includere il paging/l'ordinamento/il filtro, tuttavia potrebbe essere più semplice creare una sorta di struttura limitata attorno a questi in modo da poter separare il paging e l'ordinamento e il filtro. Se lo facessi, non mi preoccuperei di costruire le query, semplicemente scriverei sql all'interno di un oggetto che mi permettesse di passare alcuni parametri (stored procedure in modo efficace in C#).

Così ora avete IDatabaseQuery/IDatabaseCommand/IResultTransformer e quasi nessuna eredità =)

+3

"Favorisci la composizione sull'ereditarietà". <- Una lezione sempre insegnata, facilmente dimenticata, fino a quando non ci dimentichiamo che ci morde dietro. Bella risposta. –

+0

Concettualmente, non sono d'accordo. Parlando da un punto di vista ipotetico, tuttavia, credo che il punto dell'esercizio qui sia vedere fino a che punto può essere adottato il principio dell'umido. Ad esempio, può essere considerato così estremo che l'architettura è * completamente * SECCA? O c'è qualche fattore limitante di OOP (almeno in .NET) che impedisce a DRY di essere portato a quell'estremo? – BenAlabaster

+0

Troppa acqua ti ucciderà, proprio come troppo poca volontà. =) – Neal

0

Come menzionato da Kirk, questo è lo schema di delega. Quando faccio questo, di solito costruisco un'interfaccia che è implementata dal delegante e dalla classe delegata. Questo riduce l'odore del codice percepito, almeno per me.

+0

Ho già controllato quegli schemi, e devo essere sincero, solo perché sono "schemi", non soddisfa me. Che si tratti di un odore percepito dal codice o meno, non riesco proprio a farmi piacere. Quindi sono a caccia di una soluzione migliore. Grazie però, apprezzo le informazioni. – BenAlabaster

0

Penso che la risposta è semplice ... Dato .NET non supporta ereditarietà multipla, c'è sempre sarà alcune ripetizioni durante la creazione di oggetti di tipo simile. .NET semplicemente non ti dà gli strumenti per riutilizzare alcune classi in un modo che faciliterebbe il perfetto ESSERE.

La risposta non-così-semplice è che è possibile utilizzare strumenti di generazione del codice, strumentazione, codice dom e altre tecniche per iniettare gli oggetti desiderati nelle classi desiderate.Crea ancora duplicazioni in memoria, ma semplificherebbe il codice sorgente (a costo di maggiore complessità nel tuo framework di iniezione del codice).

Questo può sembrare insoddisfacente come le altre soluzioni, tuttavia se ci pensate, è proprio quello che le lingue che supportano l'MI stanno facendo dietro le quinte, collegando i sistemi di delega che non potete vedere nel codice sorgente.

La questione si riduce a, quanti sforzi siete disposti a mettere in rendere il vostro codice sorgente semplice. Pensaci, è piuttosto profondo.

+0

L'ereditarietà dovrebbe essere l'ultima risorsa. L'ereditarietà multipla dovrebbe essere l'ultima risorsa, se davvero sai davvero cosa stai facendo (e anche allora probabilmente dovrebbe essere evitato). MI dice efficacemente che c è un a e un b che probabilmente significa che c ha troppa responsabilità. – Neal

+0

@Neal - Dato che MI non è nemmeno un'opzione in C#, non è un punto controverso? – BenAlabaster

+0

@Neal - Non sono sicuro di quale sia il tuo punto. Questo è un esercizio teorico, non pratico. Concordo sul fatto che, in pratica, l'MI è problematico. Ma è uno strumento utile per ottenere ASCIUTTO. –

1

Penso che la risposta breve sia che, in un sistema in cui l'ereditarietà multipla è stata dichiarata "per la protezione", la strategia/delegazione è il sostituto diretto diretto. Sì, hai ancora una struttura parallela, come la proprietà dell'oggetto delegato. Ma è minimizzato il più possibile entro i confini della lingua.

Ma lascia passo indietro dalla risposta semplice e prendere un ampio panorama ....

Un altro grande alternativa è quella di refactoring la struttura di progettazione più ampia in modo tale che si intrinsecamente evitare questa situazione in cui una determinata classe consiste in composito di comportamenti di più classi "fratello" o "cugino" sopra di esso nell'albero ereditario. Per dirla in modo più conciso, refactoring ad una catena di ereditarietà piuttosto che un'eredità albero. Questo è più facile a dirsi che a farsi. Di solito richiede l'astrazione di funzionalità molto diverse.

La sfida che si avrà nel prendere questo aspetto che sto raccomandando è che hai già fatto una concessione nel tuo progetto: stai ottimizzando per SQL diversi nei casi "oggetto" e "elenco". Mantenerlo come è, non importa cosa, perché hai dato loro una fatturazione uguale, quindi devono necessariamente essere fratelli. Quindi direi che il tuo primo passo nel tentativo di uscire da questo "massimo locale" dell'eleganza del design sarebbe quello di ripristinare quell'ottimizzazione e trattare il singolo oggetto come ciò che è veramente: un caso speciale di un elenco, con un solo caso elemento. Puoi sempre provare a reintrodurre un'ottimizzazione per singoli articoli più tardi. Ma aspetta di aver affrontato il problema dell'eleganza che ti sta tormentando in questo momento.

Ma devi riconoscere che qualsiasi ottimizzazione per qualcosa di diverso dall'eleganza del tuo codice C# sta per mettere un blocco stradale in termini di eleganza del design per il codice C#. Questo trade-off, proprio come il coniugato "spazio-memoria" del design dell'algoritmo, è fondamentale per la natura stessa della programmazione.

+0

Grazie Chris, è molto perspicace. Avevo già pensato a questo approccio, e hai ragione, richiede una virata più "fissa". Non avevo ancora intrapreso questo approccio, ma penso che potrebbe esplorare questo approccio per vedere fino a che punto posso prenderlo. – BenAlabaster

0

Non ho guardato a fondo lo scenario, ma ho alcuni problemi sul problema della doppia gerarchia in C#. Per condividere il codice in un dual-gerarchia, abbiamo bisogno di un costrutto diverso nella lingua: o un mixin, un trait (pdf) (C# research -pdf) o di un ruolo (come in perl 6). C# rende molto semplice la condivisione del codice con l'ereditarietà (che non è l'operatore giusto per il riutilizzo del codice) e molto laborioso per condividere il codice tramite la composizione (lo sai, devi scrivere tutto quel codice di delega a mano).

ci sono modi per ottenere un kind of mixin in C#, ma non è l'ideale.

Il linguaggio Oxygene (download) (un Object Pascal per .NET) ha anche una caratteristica interessante per interface delegation che può essere utilizzato per creare tutto il codice di delega per te.