Applicazione dei principi SOLIDI


13

Sono abbastanza nuovo ai principi di progettazione SOLID . Capisco la loro causa e i loro benefici, ma non riesco ad applicarli a un progetto più piccolo che voglio refactificare come esercizio pratico per usare i principi SOLIDI. So che non è necessario modificare un'applicazione che funzioni perfettamente, ma voglio comunque riformattarla per acquisire esperienza di progettazione per progetti futuri.

L'applicazione ha il seguente compito (in realtà molto più di questo ma manteniamolo semplice): Deve leggere un file XML che contiene le definizioni Tabella database / Colonna / Visualizza ecc. E creare un file SQL che può essere utilizzato per creare uno schema di database ORACLE.

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

Per iniziare, ho scelto di guardare solo le tabelle e i vincoli. Se si ignorano le colonne, è possibile dichiararlo nel modo seguente:

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

Innanzitutto, spiegherò come appare l'applicazione in questo 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 ciascun vincolo. Detto metodo utilizzerà le connessioni alla tabella del proprietario e alla tabella di riferimento per recuperare i loro nomi.

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

Quindi ci sono un paio di domande:

  1. Devo risolvere le dipendenze circolari utilizzando Iniezione dipendenze? In tal caso, suppongo che il Vincolo debba ricevere la tabella del proprietario (e facoltativamente la referenziata) nel suo costruttore. Ma come potrei quindi scorrere l'elenco dei vincoli per una singola tabella?
  2. Se la classe Table memorizza sia lo stato di se stesso (ad esempio il nome della tabella, il commento della tabella ecc.) Sia i collegamenti ai Vincoli, queste o una o due "responsabilità" stanno pensando al principio di responsabilità singola?
  3. Nel caso 2. è giusto, devo semplicemente creare una nuova classe nel livello aziendale logico che gestisce i collegamenti? In tal caso, 1. ovviamente non sarebbe più pertinente.
  4. I metodi "createStatement" dovrebbero far parte delle classi Table / Constraint o dovrei spostarli anch'essi? In tal caso, dove? Una classe Manager per ogni classe di archiviazione dei dati (ad es. Tabella, vincolo, ...)? O piuttosto creare una classe manager per collegamento (simile a 3.)?

Ogni volta che provo a rispondere a una di queste domande, mi ritrovo a correre in cerchio da qualche parte.

Il problema ovviamente diventa molto più complesso se includi colonne, indici e così via, ma se mi aiutate con la semplice cosa Tabella / Vincolo, potrei forse risolvere il resto da solo.


3
Che lingua stai usando? Potresti pubblicare almeno un po 'di codice scheletro? È molto difficile discutere della qualità del codice e dei possibili refactoring senza vedere il codice effettivo.
Péter Török,

Sto usando il C ++ ma stavo cercando di tenerlo fuori dalla discussione perché potresti avere questo problema in qualsiasi lingua
Tim Meyer,

Sì, ma l'applicazione di schemi e refactoring dipende dalla lingua. Ad esempio, @ back2dos ha suggerito AOP nella sua risposta di seguito, che ovviamente non si applica al C ++.
Péter Török,

Per ulteriori informazioni sui principi SOLID, consultare programmers.stackexchange.com/questions/155852/…
LCJ

Risposte:


8

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

Quindi quando hai intenzione di leggere il tuo file XML, creare un modello di dati da esso e scrivere SQL, ciò che non dovresti fare è implementare qualcosa nella tua Tableclasse che sia specifico di XML o SQL. Desideri che il tuo flusso di dati sia simile al seguente:

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

Così l'unico luogo in cui deve essere posizionato il codice specifico XML è una classe di nome, per esempio, Read_XML. L'unico posto per il codice specifico di SQL dovrebbe essere una classe simile Write_SQL. Naturalmente, forse hai intenzione di dividere queste 2 attività in più attività secondarie (e dividere le tue classi in più classi manageriali), ma il tuo "modello di dati" non dovrebbe assumersi alcuna responsabilità da quel livello. Quindi non aggiungere createStatementa nessuna delle classi del tuo modello di dati, poiché ciò assegna al tuo modello di dati la responsabilità per l'SQL.

Non vedo alcun problema quando descrivi che una tabella è responsabile del mantenimento di tutte le sue parti (nome, colonne, commenti, vincoli ...), questa è l'idea alla base di un modello di dati. Ma hai descritto "Table" è anche responsabile della gestione della memoria di alcune sue parti. Questo è un problema specifico di C ++, che non affronteresti così facilmente in linguaggi come Java o C #. Il modo C ++ per sbarazzarsi di tali responsabilità consiste nell'utilizzare i puntatori intelligenti, delegando la proprietà a un livello diverso (ad esempio, la libreria boost o al proprio livello di puntatore "intelligente"). Ma attenzione, le dipendenze cicliche possono "irritare" alcune implementazioni di puntatori intelligenti.

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

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

spiegando SOLID con un piccolo esempio. Proviamo ad applicarlo al tuo caso:

  • avrai bisogno non solo di classi Read_XMLe Write_SQL, ma anche di una terza classe che gestisce l'interazione di quelle 2 classi. Chiamiamolo a ConversionManager.

  • L'applicazione del principio DI potrebbe significare qui: ConversionManager non dovrebbe creare istanze di Read_XMLe Write_SQLda solo. 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_XMLeredita, e IDataModelWriterlo stesso per Write_SQL. Questo ConversionManagerapre le estensioni (fornisci molto facilmente diversi lettori o scrittori) senza doverlo cambiare - quindi abbiamo un esempio per il principio Open / Closed. Pensa a cosa dovresti cambiare quando vuoi supportare un altro fornitore di database - in realtà, non devi cambiare nulla nel tuo modello di dati, basta fornire invece un altro SQL-Writer.


Mentre questo è un esercizio molto ragionevole di SOLID, (votato) nota che viola la "vecchia scuola Kay / Holub OOP" richiedendo getter e setter per un modello di dati piuttosto anemico. Mi ricorda anche il famigerato Steve Yegge rant .
user949300

2

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

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

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

Per scomporlo 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 getContraintsReferencingmetodo potrebbe davvero eseguire la ricerca Databaseper indicizzazione del tutto per le Tableistanze e per eseguire la ricerca per indicizzazione Constraintdei risultati per ottenere il risultato. A seconda di quanto sia costoso 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 dai rispettivi Tablee Constraintistanze, quando cambiano. Una soluzione leggermente più semplice sarebbe quella di Indexcreare un "indice di snapshot" del tutto Databasesu cui lavorare, che poi scarterebbe. Questo è ovviamente possibile solo se l'applicazione fa una grande distinzione tra "tempo di modellazione" e "tempo di interrogazione". Se è piuttosto probabile che facciano quei due allo stesso tempo, allora questo non è praticabile.
  • Un'altra opzione sarebbe quella di utilizzare AOP per intercettare le chiamate dell'intera creazione e mantenere di conseguenza l'indice.

Risposta molto dettagliata, mi piace la tua soluzione finora! Cosa penseresti se eseguissi DI per la classe Table, dandogli un elenco di vincoli durante la costruzione? Ho comunque una classe TableParser, che potrebbe fungere da fabbrica o collaborare con una fabbrica per quel caso.
Tim Meyer,

@Tim Meyer: DI non è necessariamente l'iniezione del costruttore. DI può anche essere eseguito dalle funzioni membro. Se la tabella deve ottenere tutte le sue parti attraverso il costruttore dipende dal fatto che si desideri aggiungere quelle parti solo in fase di costruzione e non modificarle mai in seguito, o se si desidera creare una tabella passo-passo. Questa dovrebbe essere la base della tua decisione di progettazione.
Doc Brown,

1

La cura per le dipendenze circolari è di giurare che non le creerai mai e poi mai. Trovo che la codifica test-first sia un forte deterrente.

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

Ma, come sospetti, la migliore soluzione a questo problema potrebbe non richiedere alcun tracciamento delle relazioni dell'oggetto. Se si desidera solo tradurre XML in SQL, non è necessaria una rappresentazione in memoria del grafico del vincolo. Il grafico dei vincoli sarebbe utile se si volessero eseguire algoritmi grafici, ma non lo hai menzionato, quindi suppongo che non sia un requisito. Hai solo bisogno di un elenco di tabelle e di un elenco di vincoli e di 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 fossero cambiati, non avrei avuto alcun problema con l'accoppiamento del generatore SQL al DOM XML. Risparmia domani per domani.


È qui che entra in gioco "(in realtà molto più di questo, ma teniamolo semplice)". Ad esempio, ci sono casi in cui è necessario eliminare una tabella, quindi è necessario verificare se eventuali vincoli fanno riferimento a questa tabella.
Tim Meyer,
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.