Applicazione dei principi SOLID

13

Sono abbastanza nuovo nei principi di design S.O.L.I.D. . Capisco la loro causa e i loro benefici, ma non riesco ad applicarli a un progetto più piccolo che voglio refactoring come un esercizio pratico per utilizzare i principi SOLID. So che non c'è bisogno di cambiare un'applicazione che funzioni perfettamente, ma voglio rifattorizzarla comunque, così acquisisco esperienza di progettazione per progetti futuri.

L'applicazione ha il seguente compito (in realtà molto più di quello, ma facciamo in modo che sia semplice): Deve leggere un file XML che contiene le definizioni Database / Column / View etc del database e creare un file SQL che può essere utilizzato per creare uno schema di database ORACLE.

(Nota: si prega di astenersi dal discutere perché ho bisogno o perché non uso XSLT e così via, ci sono dei motivi, ma sono fuori tema.)

Come inizio, ho scelto di guardare solo Tabelle e Vincoli. Se ignori le colonne, puoi dichiararlo nel seguente modo:

Un vincolo è parte di una tabella (o più precisamente, parte di un'istruzione CREATE TABLE), e un vincolo può fare riferimento anche a un'altra tabella.

In primo luogo, spiegherò l'aspetto dell'applicazione al momento (non applicando SOLID):

Al momento, l'applicazione ha una classe "Tabella" che contiene un elenco di puntatori a Vincoli di proprietà della tabella e un elenco di puntatori a Vincoli che fanno riferimento a questa tabella. Ogni volta che viene stabilita una connessione, verrà stabilita anche la connessione all'indietro. La tabella ha un metodo createStatement () che a sua volta chiama la funzione createStatement () di ogni vincolo. Il metodo detto utilizzerà le connessioni alla tabella del proprietario e alla tabella di riferimento per recuperarne i nomi.

Ovviamente, questo non si applica a SOLID. Ad esempio, ci sono delle dipendenze circolari che hanno gonfiato il codice in termini di metodi "aggiungi" / "rimuovi" richiesti e alcuni grandi distruttori di oggetti.

Quindi ci sono un paio di domande:

  1. Devo risolvere le dipendenze circolari usando Iniezione delle dipendenze? In tal caso, suppongo che il vincolo debba ricevere la tabella del proprietario (e facoltativamente del riferimento) nel suo costruttore. Ma come posso eseguire l'elenco dei vincoli per una singola tabella, quindi?
  2. Se la classe Tabella memorizza entrambi lo stato di se stesso (ad es. nome tabella, commento tabella ecc.) e i collegamenti a Vincoli, sono queste una o due "responsabilità", pensando al Principio di Responsabilità Unica?
  3. Nel caso in cui 2. abbia ragione, dovrei semplicemente creare una nuova classe nel livello aziendale logico che gestisce i collegamenti? Se è così, 1. ovviamente non sarebbe più rilevante.
  4. I metodi "createStatement" faranno parte delle classi Table / Constraint o dovrei spostarli anch'essi? Se sì, dove? Una classe Manager per ogni classe di archiviazione dati (ad es. Tabella, Vincolo, ...)? O meglio crea una classe manager per link (simile a 3.)?

Ogni volta che cerco di rispondere a una di queste domande mi trovo a correre in cerchio da qualche parte.

Il problema diventa ovviamente molto più complesso se includi colonne, indici e così via, ma se voi ragazzi mi aiutate con la semplice cosa Table / Constraint, potrò forse risolvere il resto da solo.

    
posta Tim Meyer 02.09.2011 - 11:21
fonte

3 risposte

8

Puoi iniziare da un diverso punto di vista per applicare "Principio di singola responsabilità" qui. Quello che ci hai mostrato è (più o meno) solo il modello di dati della tua applicazione. SRP significa qui: assicurati che il tuo modello di dati sia responsabile solo della conservazione dei dati - non di meno, non di più.

Quindi, quando leggerai il tuo file XML, creerai un modello di dati da esso e scrivi SQL, ciò che dovresti non fare è implementare qualsiasi cosa nella tua classe Table che è XML o SQL specifico. Vuoi che il tuo flusso di dati assomigli a questo:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Quindi l'unica posizione in cui il codice specifico XML dovrebbe essere inserito è una classe chiamata, ad esempio, Read_XML . L'unico posto per codice SQL specifico dovrebbe essere una classe come Write_SQL . Naturalmente, forse dividerete queste due attività in più attività secondarie (e dividerete le vostre classi in più classi di manager), ma il vostro "modello di dati" non dovrebbe assumersi alcuna responsabilità da quel livello. Quindi non aggiungere un createStatement a nessuna delle tue classi di modelli di dati, poiché ciò fornisce al tuo modello di dati la responsabilità per l'SQL.

Non vedo alcun problema quando stai descrivendo che una tabella è responsabile di contenere tutte le sue parti, (nome, colonne, commenti, vincoli ...), questa è l'idea alla base di un modello di dati. Ma hai descritto che "Table" è anche responsabile della gestione della memoria di alcune delle sue parti. Questo è un problema specifico del C ++, che non dovresti affrontare così facilmente in linguaggi come Java o C #. Il modo C ++ di liberarsi di tali responsabilità è l'utilizzo di puntatori intelligenti, che delegano la proprietà a un livello diverso (ad esempio, la libreria boost o il proprio livello puntatore "intelligente"). Ma attenzione, le vostre dipendenze cicliche potrebbero "irritare" alcune implementazioni di puntatori intelligenti.

Qualcosa in più su SOLID: ecco un bell'articolo

link

spiegare SOLID con un piccolo esempio. Proviamo ad applicarlo nel tuo caso:

  • avrai bisogno non solo delle classi Read_XML e Write_SQL , ma anche di una terza classe che gestisce l'interazione di queste 2 classi. Consente di chiamarlo ConversionManager .

  • L'applicazione del principio di DI potrebbe significare qui: ConversionManager non dovrebbe creare istanze di Read_XML e Write_SQL di per sé. Invece, quegli oggetti possono essere iniettati attraverso il costruttore. E il costruttore dovrebbe avere una firma come questa

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

dove IDataModelReader è un'interfaccia da cui Read_XML eredita e IDataModelWriter lo stesso per Write_SQL . Ciò rende un ConversionManager aperto per le estensioni (puoi facilmente fornire diversi lettori o scrittori) senza doverlo modificare - quindi abbiamo un esempio per il principio Aperto / Chiuso. Pensa a ciò che dovrai cambiare quando vuoi supportare un altro fornitore di database, in modo ideale, non devi modificare nulla nella tua datamodel, basta invece fornire un altro SQL-writer.

    
risposta data 02.09.2011 - 15:29
fonte
2

Bene, dovresti applicare la S di SOLID in questo caso.

Una tabella contiene tutti i vincoli definiti su di essa. Un vincolo contiene tutte le tabelle a cui fa riferimento. Modello semplice e semplice

Ciò su cui ti attieni, è la capacità di eseguire ricerche inverse, cioè capire da quali vincoli viene fatta riferimento a una tabella.
Quindi quello che vuoi veramente è un servizio di indicizzazione. Questo è un compito completamente diverso e dovrebbe quindi essere eseguito da un oggetto diverso.

Per suddividerlo in una versione molto semplificata:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Per quanto riguarda l'implementazione dell'indice, ci sono 3 modi per andare:

  • il metodo getContraintsReferencing potrebbe davvero solo eseguire la scansione dell'intero Database per Table istanze e eseguire la scansione del Constraint s per ottenere il risultato. A seconda di quanto sia costosa e di quanto spesso ne hai bisogno, potrebbe essere un'opzione.
  • potrebbe anche usare una cache. Se il modello del database può cambiare una volta definito, è possibile mantenere la cache attivando i segnali dalle rispettive istanze Table e Constraint , quando cambiano. Una soluzione un po 'più semplice sarebbe avere il Index per creare un "indice istantaneo" dell'intero Database con cui lavorare, che poi scartare. Questo è ovviamente possibile solo se la tua applicazione fa una grande distinzione tra "tempo di modellazione" e "tempo di interrogazione". Se è piuttosto probabile che eseguano questi due allo stesso tempo, allora questo non è fattibile.
  • Un'altra opzione potrebbe essere quella di utilizzare AOP per intercettare tutte le chiamate di creazione e mantenere l'indice di conseguenza.
risposta data 02.09.2011 - 11:51
fonte
1

La cura per le dipendenze circolari è di giurare che non le creerai mai e poi mai. Trovo che il test di codifica sia un strong deterrente.

In ogni caso, le dipendenze circolari possono sempre essere infrante introducendo una classe base astratta. Questo è tipico per le rappresentazioni grafiche. Qui le tabelle sono nodi e i vincoli di chiave esterna sono bordi. Crea quindi una classe Table astratta e una classe Constraint astratta e magari una classe Column astratta. Quindi tutte le implementazioni possono dipendere dalle classi astratte. Questa potrebbe non essere la migliore rappresentazione possibile, ma è un miglioramento rispetto alle classi mutuamente accoppiate.

Tuttavia, come sospetti, la migliore soluzione a questo problema potrebbe non richiedere alcun monitoraggio delle relazioni tra oggetti. Se vuoi solo tradurre XML in SQL, non hai bisogno di una rappresentazione in memoria del grafico dei vincoli. Il grafico dei vincoli sarebbe bello se volessi eseguire algoritmi di grafi, ma non ne hai parlato, quindi presumo che non sia un requisito. Hai solo bisogno di un elenco di tabelle e un elenco di vincoli e un visitatore per ogni dialetto SQL che desideri supportare. Generare le tabelle, quindi generare i vincoli esterni alle tabelle. Fino a quando i requisiti non saranno cambiati, non avrei alcun problema con l'accoppiamento del generatore SQL al DOM XML. Salva domani per domani.

    
risposta data 02.09.2011 - 22:27
fonte

Leggi altre domande sui tag