Comment les interfaces de base de données abstraites sont-elles écrites pour prendre en charge plusieurs types de base de données?

12

Comment peut-on commencer à concevoir une classe abstraite dans leur plus grande application qui peut s'interfacer avec plusieurs types de bases de données, telles que MySQL, SQLLite, MSSQL, etc.?

Comment s'appelle ce modèle de conception et où commence-t-il exactement?

Disons que vous devez écrire une classe qui a les méthodes suivantes

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();
    }
}

La seule chose à laquelle je peux penser est une instruction if dans chaque Databaseméthode

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

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

   }
}
tons31
la source

Réponses:

11

Ce que vous voulez, c'est plusieurs implémentations pour l' interface que votre application utilise.

ainsi:

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!");
    }
}

En ce qui concerne une meilleure façon de configurer l' IDatabaseimplémentation correcte au moment de l'exécution dans votre application, vous devriez examiner des choses comme « Méthode d'usine » et « Injection de dépendances ».

Caleb
la source
25

La réponse de Caleb, alors qu'il est sur la bonne voie, est en fait fausse. Sa Fooclasse agit à la fois comme façade de base de données et comme usine. Ce sont deux responsabilités et ne devraient pas être regroupées dans une seule classe.


Cette question, en particulier dans le contexte de la base de données, a été posée trop souvent. Ici, je vais essayer de vous montrer en détail l'avantage d'utiliser l'abstraction (en utilisant des interfaces) pour rendre votre application moins couplée et plus polyvalente.

Avant de poursuivre la lecture, je vous recommande de lire et d'acquérir une compréhension de base de l' injection de dépendance , si vous ne la connaissez pas encore. Vous pouvez également vouloir vérifier le modèle de conception de l' adaptateur , qui est essentiellement ce que signifie masquer les détails d'implémentation derrière les méthodes publiques de l'interface.

L'injection de dépendances, couplée au modèle de conception Factory , est la pierre angulaire et un moyen facile de coder le modèle de conception Strategy , qui fait partie du principe IoC .

Ne nous appelez pas, nous vous appellerons . (AKA le principe d'Hollywood ).


Découplage d'une application à l'aide de l'abstraction

1. Réalisation de la couche d'abstraction

Vous créez une interface - ou une classe abstraite, si vous codez dans un langage comme C ++ - et ajoutez des méthodes génériques à cette interface. Parce que les interfaces et les classes abstraites ont le comportement de ne pas pouvoir les utiliser directement, mais vous devez les implémenter (en cas d'interface) ou les étendre (en cas de classe abstraite), le code lui-même le suggère déjà, vous le ferez besoin d'avoir des implémentations spécifiques pour remplir le contrat donné par l'interface ou la classe abstraite.

Votre (exemple très simple) interface de base de données pourrait ressembler à ceci (les classes DatabaseResult ou DbQuery respectivement seraient vos propres implémentations représentant les opérations de base de données):

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

Parce que c'est une interface, elle ne fait rien en elle-même. Vous avez donc besoin d'une classe pour implémenter cette interface.

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
    }
}

Vous avez maintenant une classe qui implémente le Database, l'interface est juste devenue utile.

2. Utilisation de la couche d'abstraction

Quelque part dans votre application, vous avez une méthode, appelons-la SecretMethod, juste pour le plaisir, et à l'intérieur de cette méthode, vous devez utiliser la base de données, car vous voulez récupérer des données.

Vous avez maintenant une interface, que vous ne pouvez pas créer directement (euh, comment puis-je l'utiliser alors), mais vous avez une classe MyMySQLDatabase, qui peut être construite à l'aide du newmot - clé.

GÉNIAL! Je souhaite utiliser une base de données, je vais donc utiliser le MyMySQLDatabase.

Votre méthode pourrait ressembler à ceci:

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

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

Ce n'est pas bien. Vous créez directement une classe à l'intérieur de cette méthode, et si vous le faites à l'intérieur de SecretMethod, il est sûr de supposer que vous feriez de même dans 30 autres méthodes. Si vous vouliez changer le MyMySQLDatabasepour une classe différente, par exemple MyPostgreSQLDatabase, vous devriez le changer dans toutes vos 30 méthodes.

Un autre problème est que si la création de MyMySQLDatabaseéchouait, la méthode ne se terminerait jamais et ne serait donc pas valide.

Nous commençons par refactoriser la création de la MyMySQLDatabaseen la passant comme paramètre à la méthode (c'est ce qu'on appelle l'injection de dépendance).

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

Cela vous résout le problème, que l' MyMySQLDatabaseobjet n'a jamais pu être créé. Étant donné que le SecretMethodattend un MyMySQLDatabaseobjet valide , si quelque chose s'est produit et que l'objet ne lui est jamais transmis, la méthode ne s'exécute jamais. Et c'est tout à fait bien.


Dans certaines applications, cela peut suffire. Vous pouvez être satisfait, mais refactorisons-le pour qu'il soit encore meilleur.

Le but d'un autre refactoring

Vous pouvez voir, en ce moment, SecretMethodutilise un MyMySQLDatabaseobjet. Supposons que vous êtes passé de MySQL à MSSQL. Vous ne vous sentez pas vraiment envie de changer toute la logique dans votre SecretMethod, une méthode qui appelle BeginTransactionet CommitTransactionméthodes sur la databasevariable passée en paramètre, vous créez une nouvelle classe MyMSSQLDatabase, qui aura également la BeginTransactionet CommitTransactionméthodes.

Ensuite, vous allez de l'avant et modifiez la déclaration de SecretMethodce qui suit.

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

Et parce que les classes MyMSSQLDatabaseet MyMySQLDatabaseont les mêmes méthodes, vous n'avez pas besoin de changer quoi que ce soit d'autre et cela fonctionnera toujours.

Oh, attendez!

Vous avez une Databaseinterface, qui MyMySQLDatabaseimplémente, vous avez également la MyMSSQLDatabaseclasse, qui a exactement les mêmes méthodes que MyMySQLDatabase, peut-être que le pilote MSSQL pourrait également implémenter l' Databaseinterface, alors vous l'ajoutez à la définition.

public class MyMSSQLDatabase : Database { }

Mais que se passe-t-il si, à l'avenir, je ne veux MyMSSQLDatabaseplus l'utiliser , parce que je suis passé à PostgreSQL? Je devrais, encore une fois, remplacer la définition du SecretMethod?

Oui, vous le feriez. Et cela ne sonne pas bien. En ce moment , nous savons que MyMSSQLDatabaseet MyMySQLDatabaseont les mêmes méthodes et à la fois mettre en œuvre l' Databaseinterface. Donc, vous refactorisez le SecretMethodpour ressembler à ceci.

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

Remarquez, comment le SecretMethodne sait plus, si vous utilisez MySQL, MSSQL ou PotgreSQL. Il sait qu'il utilise une base de données, mais ne se soucie pas de l'implémentation spécifique.

Maintenant, si vous vouliez créer votre nouveau pilote de base de données, pour PostgreSQL par exemple, vous n'aurez pas besoin de changer du SecretMethodtout. Vous ferez un MyPostgreSQLDatabase, lui ferez implémenter l' Databaseinterface et une fois que vous aurez fini de coder le pilote PostgreSQL et qu'il fonctionnera, vous créerez son instance et l'injecterez dans le SecretMethod.

3. Obtenir la mise en œuvre souhaitée de Database

Vous devez encore décider, avant d'appeler le SecretMethod, quelle implémentation de l' Databaseinterface vous souhaitez (que ce soit MySQL, MSSQL ou PostgreSQL). Pour cela, vous pouvez utiliser le modèle de conception d'usine.

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 fabrique, comme vous pouvez le voir, sait quel type de base de données utiliser à partir d'un fichier de configuration (encore une fois, la Configclasse peut être votre propre implémentation).

Idéalement, vous aurez l' DatabaseFactoryintérieur de votre conteneur d'injection de dépendance. Votre processus peut alors ressembler à ceci.

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);
    }
}

Regardez, comment nulle part dans le processus, vous créez un type de base de données spécifique. Non seulement cela, vous ne créez rien du tout. Vous appelez une GetDatabaseméthode sur l' DatabaseFactoryobjet stocké à l'intérieur de votre conteneur d'injection de dépendance (la _divariable), une méthode, qui vous renverra l'instance d' Databaseinterface correcte , en fonction de votre configuration.

Si, après 3 semaines d'utilisation de PostgreSQL, vous souhaitez revenir à MySQL, vous ouvrez un seul fichier de configuration et modifiez la valeur du DatabaseDriverchamp de DatabaseEnum.PostgreSQLà DatabaseEnum.MySQL. Et vous avez terminé. Du coup, le reste de votre application utilise à nouveau correctement MySQL, en changeant une seule ligne.


Si vous n'êtes toujours pas étonné, je vous recommande de plonger un peu plus dans l'IoC. Comment vous pouvez prendre certaines décisions non pas à partir d'une configuration, mais à partir d'une entrée utilisateur. Cette approche est appelée le modèle de stratégie et bien qu'elle puisse être et est utilisée dans les applications d'entreprise, elle est beaucoup plus fréquemment utilisée lors du développement de jeux informatiques.

Andy
la source
Aimez votre réponse, David. Mais comme toutes ces réponses, elle ne décrit pas comment on pourrait la mettre en pratique. Le vrai problème n'est pas d'abstraire la possibilité d'appeler dans différents moteurs de base de données, le problème est la syntaxe SQL réelle. Prenez votre DbQueryobjet, par exemple. En supposant que cet objet contenait un membre pour une chaîne de requête SQL à exécuter, comment pourrait-on rendre cela générique?
DonBoitnott
1
@DonBoitnott Je ne pense pas que vous auriez besoin de tout pour être générique. Vous souhaitez généralement introduire l'abstraction entre les couches d'application (domaine, services, persistance), vous pouvez également introduire l'abstraction pour les modules, vous pouvez vouloir introduire l'abstraction dans une petite bibliothèque réutilisable et hautement personnalisable que vous développez pour un projet plus important, etc. Vous pouvez simplement tout résumer aux interfaces, mais cela est rarement nécessaire. Il est vraiment difficile de donner une réponse un pour tout, car, malheureusement, il n'y en a vraiment pas et cela vient des exigences.
Andy
2
Compris. Mais je pensais vraiment cela littéralement. Une fois que vous avez votre classe abstraite, et vous arrivez au point où vous voulez appeler _secret.SecretMethod(database);comment réconcilier tout cela avec le fait que maintenant mon SecretMethoddoit encore savoir avec quelle base de données je travaille afin d'utiliser le dialecte SQL approprié ? Vous avez travaillé très dur pour que la majorité du code ignore ce fait, mais à la 11e heure, vous devez à nouveau le savoir. Je suis dans cette situation maintenant et j'essaie de comprendre comment d'autres ont résolu ce problème.
DonBoitnott
@DonBoitnott Je ne savais pas ce que tu voulais dire, je le vois maintenant. Vous pouvez utiliser une interface au lieu d'une implémentation concrète de la DbQueryclasse, fournir des implémentations de ladite interface et utiliser celle-ci à la place, en ayant une fabrique pour construire l' IDbQueryinstance. Je ne pense pas que vous auriez besoin d'un type générique pour la DatabaseResultclasse, vous pouvez toujours vous attendre à ce que les résultats d'une base de données soient formatés de manière similaire. Le problème ici est que, lorsque vous traitez avec des bases de données et du SQL brut, vous êtes déjà à un niveau si bas sur votre application (derrière DAL et les référentiels), qu'il n'y a pas besoin de ...
Andy
... approche générique plus.
Andy