Come vengono scritte le interfacce di database astratte per supportare più tipi di database?


12

Come si può iniziare a progettare una classe astratta nella loro più grande applicazione in grado di interfacciarsi con diversi tipi di database, come MySQL, SQLLite, MSSQL, ecc?

Come si chiama questo modello di progettazione e da dove inizia esattamente?

Supponiamo che tu abbia bisogno di scrivere una classe che abbia i seguenti metodi

public class Database {
   public DatabaseType databaseType;
   public Database (DatabaseType databaseType){
      this.databaseType = databaseType;
   }

   public void SaveToDatabase(){
       // Save some data to the db
   }
   public void ReadFromDatabase(){
      // Read some data from db
   }
}

//Application
public class Foo {
    public Database db = new Database (DatabaseType.MySQL);
    public void SaveData(){
        db.SaveToDatabase();
    }
}

L'unica cosa che mi viene in mente è un'istruzione if in ogni singolo Databasemetodo

public void SaveToDatabase(){
   if(databaseType == DatabaseType.MySQL){

   }
   else if(databaseType == DatabaseType.SQLLite){

   }
}

Risposte:


11

Ciò che desideri sono più implementazioni per l' interfaccia utilizzata dall'applicazione.

così:

public interface IDatabase
{
    void SaveToDatabase();
    void ReadFromDatabase();
}

public class MySQLDatabase : IDatabase
{
   public MySQLDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //MySql implementation
   }
   public void ReadFromDatabase(){
      //MySql implementation
   }
}

public class SQLLiteDatabase : IDatabase
{
   public SQLLiteDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //SQLLite implementation
   }
   public void ReadFromDatabase(){
      //SQLLite implementation
   }
}

//Application
public class Foo {
    public IDatabase db = GetDatabase();

    public void SaveData(){
        db.SaveToDatabase();
    }

    private IDatabase GetDatabase()
    {
        if(/*some way to tell if should use MySql*/)
            return new MySQLDatabase();
        else if(/*some way to tell if should use MySql*/)
            return new SQLLiteDatabase();

        throw new Exception("You forgot to configure the database!");
    }
}

Per quanto riguarda un modo migliore di impostare la corretta IDatabaseimplementazione in fase di esecuzione nella tua applicazione, dovresti esaminare cose come " Metodo di fabbrica " e " Iniezione di dipendenza ".


25

La risposta di Caleb, mentre è sulla buona strada, è in realtà sbagliata. La sua Fooclasse funge sia da facciata di database che da fabbrica. Queste sono due responsabilità e non dovrebbero essere inserite in un'unica classe.


Questa domanda, specialmente nel contesto del database, è stata posta troppe volte. Qui proverò a mostrarti a fondo il vantaggio di usare l'astrazione (usando le interfacce) per rendere la tua applicazione meno accoppiata e più versatile.

Prima di leggere oltre, ti consiglio di leggere e ottenere una conoscenza di base dell'iniezione di dipendenza , se non lo conosci ancora. Potresti anche voler controllare il modello di progettazione dell'adattatore , che è fondamentalmente ciò che significa nascondere i dettagli dell'implementazione dietro i metodi pubblici dell'interfaccia.

L'iniezione di dipendenza, unita al modello di progettazione di fabbrica , è la pietra miliare e un modo semplice per codificare il modello di progettazione di strategia , che fa parte del principio IoC .

Non chiamarci, ti chiameremo . (AKA il principio di Hollywood ).


Disaccoppiamento di un'applicazione mediante l'astrazione

1. Realizzare lo strato di astrazione

Si crea un'interfaccia - o una classe astratta, se si sta codificando in un linguaggio come C ++ - e si aggiungono metodi generici a questa interfaccia. Poiché entrambe le interfacce e le classi astratte hanno il comportamento di non poterle utilizzare direttamente, ma è necessario implementarle (in caso di interfaccia) o estenderle (in caso di classe astratta), il codice stesso suggerisce già, si è necessario disporre di implementazioni specifiche per completare il contratto dato dall'interfaccia o dalla classe astratta.

L'interfaccia del tuo database (esempio molto semplice) potrebbe assomigliare a questa (le classi DatabaseResult o DbQuery rispettivamente sarebbero le tue implementazioni che rappresentano le operazioni del database):

public interface Database
{
    DatabaseResult DoQuery(DbQuery query);
    void BeginTransaction();
    void RollbackTransaction();
    void CommitTransaction();
    bool IsInTransaction();
}

Poiché questa è un'interfaccia, essa stessa non fa davvero nulla. Quindi hai bisogno di una classe per implementare questa interfaccia.

public class MyMySQLDatabase : Database
{
    private readonly CSharpMySQLDriver _mySQLDriver;

    public MyMySQLDatabase(CSharpMySQLDriver mySQLDriver)
    {
        _mySQLDriver = mySQLDriver;
    }

    public DatabaseResult DoQuery(DbQuery query)
    {
        // This is a place where you will use _mySQLDriver to handle the DbQuery
    }

    public void BeginTransaction()
    {
        // This is a place where you will use _mySQLDriver to begin transaction
    }

    public void RollbackTransaction()
    {
    // This is a place where you will use _mySQLDriver to rollback transaction
    }

    public void CommitTransaction()
    {
    // This is a place where you will use _mySQLDriver to commit transaction
    }

    public bool IsInTransaction()
    {
    // This is a place where you will use _mySQLDriver to check, whether you are in a transaction
    }
}

Ora hai una classe che implementa il Database, l'interfaccia è diventata utile.

2. Utilizzo dello strato di astrazione

Da qualche parte nella tua applicazione, hai un metodo, chiamiamo il metodo SecretMethod, solo per divertimento, e all'interno di questo metodo devi usare il database, perché vuoi recuperare alcuni dati.

Ora hai un'interfaccia, che non puoi creare direttamente (uh, come la uso allora), ma hai una classe MyMySQLDatabase, che può essere costruita usando la newparola chiave.

GRANDE! Voglio usare un database, quindi userò il MyMySQLDatabase.

Il tuo metodo potrebbe apparire così:

public void SecretMethod()
{
    var database = new MyMySQLDatabase(new CSharpMySQLDriver());

    // you will use the database here, which has the DoQuery,
    // BeginTransaction, RollbackTransaction and CommitTransaction methods
}

Questo non è un bene. Stai creando direttamente una classe all'interno di questo metodo e, se lo stai facendo all'interno di SecretMethod, è sicuro supporre che faresti lo stesso in altri 30 metodi. Se si desidera modificare il tipo MyMySQLDatabasein una classe diversa, ad esempio MyPostgreSQLDatabase, è necessario modificarlo in tutti i 30 metodi.

Un altro problema è che, se la creazione MyMySQLDatabasenon è riuscita, il metodo non finirà mai e quindi non sarebbe valido.

Iniziamo con il refactoring della creazione del MyMySQLDatabasepassandolo come parametro al metodo (questo si chiama iniezione di dipendenza).

public void SecretMethod(MyMySQLDatabase database)
{
    // use the database here
}

Questo ti risolve il problema, che l' MyMySQLDatabaseoggetto non potrebbe mai essere creato. Poiché si SecretMethodaspetta un MyMySQLDatabaseoggetto valido , se accadesse qualcosa e l'oggetto non gli sarebbe mai passato, il metodo non verrebbe mai eseguito. E va benissimo.


In alcune applicazioni questo potrebbe essere sufficiente. Potresti essere soddisfatto, ma rifattiamolo per essere ancora migliore.

Lo scopo di un altro refactoring

Puoi vedere, in questo momento SecretMethodusa un MyMySQLDatabaseoggetto. Supponiamo che tu sia passato da MySQL a MSSQL. Non hai davvero voglia di cambiare tutta la logica dentro di te SecretMethod, un metodo che chiama a BeginTransactione CommitTransactionmetodi sulla databasevariabile passati come parametro, quindi crei una nuova classe MyMSSQLDatabase, che avrà anche i metodi BeginTransactione CommitTransaction.

Quindi vai avanti e modifica la dichiarazione SecretMethodcome segue.

public void SecretMethod(MyMSSQLDatabase database)
{
    // use the database here
}

E perché le classi MyMSSQLDatabasee MyMySQLDatabasehanno gli stessi metodi, non è necessario cambiare niente altro e si continuerà a funzionare.

Oh aspetta!

Hai Databaseun'interfaccia, che MyMySQLDatabaseimplementa, hai anche la MyMSSQLDatabaseclasse, che ha esattamente gli stessi metodi di MyMySQLDatabase, forse il driver MSSQL potrebbe anche implementare l' Databaseinterfaccia, quindi la aggiungi alla definizione.

public class MyMSSQLDatabase : Database { }

E se, in futuro, non volessi più utilizzare MyMSSQLDatabase, perché sono passato a PostgreSQL? Dovrei, ancora una volta, sostituire la definizione di SecretMethod?

Sì, lo faresti. E questo non suona bene. In questo momento sappiamo, che MyMSSQLDatabasee MyMySQLDatabasehanno gli stessi metodi ed entrambi implementare l' Databaseinterfaccia. Quindi rifattori SecretMethodper assomigliare a questo.

public void SecretMethod(Database database)
{
    // use the database here
}

Si noti come SecretMethodnon si sa più se si utilizza MySQL, MSSQL o PotgreSQL. Sa che utilizza un database, ma non si preoccupa dell'implementazione specifica.

Ora, se si desidera creare il nuovo driver del database, ad esempio per PostgreSQL, non è necessario modificarlo SecretMethodaffatto. Farai un MyPostgreSQLDatabase, lo farai implementare l' Databaseinterfaccia e una volta che avrai finito di codificare il driver PostgreSQL e funzionerà, creerai la sua istanza e la inietterai nel SecretMethod.

3. Ottenere l'implementazione desiderata di Database

Devi ancora decidere, prima di chiamare il SecretMethod, quale implementazione Databasedell'interfaccia desiderata (se si tratta di MySQL, MSSQL o PostgreSQL). Per questo, è possibile utilizzare il modello di progettazione di fabbrica.

public class DatabaseFactory
{
    private Config _config;

    public DatabaseFactory(Config config)
    {
        _config = config;
    }

    public Database getDatabase()
    {
        var databaseType = _config.GetDatabaseType();

        Database database = null;

        switch (databaseType)
        {
        case DatabaseEnum.MySQL:
            database = new MyMySQLDatabase(new CSharpMySQLDriver());
            break;
        case DatabaseEnum.MSSQL:
            database = new MyMSSQLDatabase(new CSharpMSSQLDriver());
            break;
        case DatabaseEnum.PostgreSQL:
            database = new MyPostgreSQLDatabase(new CSharpPostgreSQLDriver());
            break;
        default:
            throw new DatabaseDriverNotImplementedException();
            break;
        }

        return database;
    }
}

La fabbrica, come puoi vedere, sa quale tipo di database usare da un file di configurazione (di nuovo, la Configclasse potrebbe essere la tua implementazione).

Idealmente, avrai l' DatabaseFactoryinterno del contenitore per l'iniezione delle dipendenze. Il tuo processo potrebbe quindi apparire così.

public class ProcessWhichCallsTheSecretMethod
{
    private DIContainer _di;
    private ClassWithSecretMethod _secret;

    public ProcessWhichCallsTheSecretMethod(DIContainer di, ClassWithSecretMethod secret)
    {
        _di = di;
        _secret = secret;
    }

    public void TheProcessMethod()
    {
        Database database = _di.Factories.DatabaseFactory.getDatabase();
        _secret.SecretMethod(database);
    }
}

Guarda come da nessuna parte nel processo stai creando un tipo di database specifico. Non solo, non stai creando nulla. Stai chiamando un GetDatabasemetodo DatabaseFactorysull'oggetto memorizzato nel tuo contenitore di iniezione di dipendenza (la _divariabile), un metodo, che ti restituirà l'istanza corretta Databasedell'interfaccia, in base alla tua configurazione.

Se, dopo 3 settimane di utilizzo di PostgreSQL, vuoi tornare a MySQL, apri un singolo file di configurazione e cambi il valore del DatabaseDrivercampo da DatabaseEnum.PostgreSQLa DatabaseEnum.MySQL. E il gioco è fatto. All'improvviso il resto dell'applicazione utilizza correttamente MySQL, cambiando una sola riga.


Se non sei ancora sorpreso, ti consiglio di immergerti un po 'di più nell'IoC. Come è possibile prendere determinate decisioni non da una configurazione, ma da un input dell'utente. Questo approccio è chiamato modello di strategia e sebbene possa essere ed è utilizzato nelle applicazioni aziendali, è molto più frequentemente utilizzato durante lo sviluppo di giochi per computer.


Adoro la tua risposta, David. Ma come tutte queste risposte, non riesce a descrivere come si possa metterlo in pratica. Il vero problema non è sottrarre la possibilità di chiamare in diversi motori di database, il problema è la sintassi SQL effettiva. Prendi il tuo DbQueryoggetto, per esempio. Supponendo che l'oggetto contenesse un membro per l'esecuzione di una stringa di query SQL, come si può renderlo generico?
DonBoitnott,

1
@DonBoitnott Non credo che avresti mai bisogno di tutto per essere generico. Di solito vuoi introdurre l'astrazione tra i livelli dell'applicazione (dominio, servizi, persistenza), potresti anche voler introdurre l'astrazione per i moduli, potresti voler introdurre l'astrazione in una piccola ma riutilizzabile e altamente personalizzabile libreria che stai sviluppando per un progetto più ampio, ecc. Potresti semplicemente astrarre tutto sulle interfacce, ma raramente è necessario. È davvero difficile dare una risposta unica, perché, purtroppo, non ce n'è davvero una e viene dai requisiti.
Andy,

2
Inteso. Ma lo intendevo davvero letteralmente. Una volta che hai la tua classe sottratta e arrivi al punto in cui vuoi chiamare _secret.SecretMethod(database);come si riconcilia tutto ciò che funziona con il fatto che ora il mio SecretMethoddeve ancora sapere con quale DB sto lavorando per usare il dialetto SQL corretto ? Hai lavorato molto duramente per mantenere la maggior parte del codice ignorante di quel fatto, ma poi all'undicesima ora, devi di nuovo sapere. Sono in questa situazione ora e sto cercando di capire come altri hanno risolto questo problema.
DonBoitnott,

@DonBoitnott Non sapevo cosa volevi dire, lo vedo ora. È possibile utilizzare un'interfaccia anziché un'implementazione concreta della DbQueryclasse, fornire implementazioni di detta interfaccia e utilizzare quella invece, avendo una factory per costruire l' IDbQueryistanza. Non credo che avresti bisogno di un tipo generico per la DatabaseResultclasse, puoi sempre aspettarti che i risultati di un database siano formattati in modo simile. La cosa qui è, quando si ha a che fare con database e SQL raw, si è già a un livello così basso nella propria applicazione (dietro DAL e Repository), che non è necessario per ...
Andy,

... più approccio generico.
Andy,
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.