web-dev-qa-db-fra.com

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

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 méthode Database

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

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

   }
}
12
tones31

Ce que vous voulez, c'est implémentations multiples pour 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 un meilleur moyen de configurer la bonne implémentation de IDatabase au moment de l'exécution dans votre application, vous devriez examiner des choses comme "méthode d'usine" et "dépendance Injection ".

11
Caleb

La réponse de Caleb, alors qu'il est sur la bonne voie, est en fait fausse. Sa classe Foo 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 continuer à lire, je vous recommande de lire et d'acquérir une compréhension de base de injection de dépendance , si vous ne le connaissez pas encore. Vous pouvez également vouloir vérifier le modèle de conception de l'adaptateur , qui est essentiellement ce que signifie cacher les détails de l'implémentation derrière les méthodes publiques de l'interface.

L'injection de dépendance, couplée à modèle de conception d'usine , est la pierre angulaire et un moyen facile de coder le modèle de conception de stratégie , qui fait partie de 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éaliser 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 méthode 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 en utilisant le mot clé new.

GRAND! Je veux utiliser une base de données, donc j'utiliserai 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 voulez changer le MyMySQLDatabase en une classe différente, comme MyPostgreSQLDatabase, vous devrez 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 MyMySQLDatabase en 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'objet MyMySQLDatabase n'a jamais pu être créé. Étant donné que SecretMethod attend un objet MyMySQLDatabase valide, si quelque chose se 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, le SecretMethod utilise un objet MyMySQLDatabase. Supposons que vous êtes passé de MySQL à MSSQL. Vous n'avez pas vraiment envie de changer toute la logique à l'intérieur de votre SecretMethod, une méthode qui appelle les méthodes BeginTransaction et CommitTransaction sur la variable database passée en tant que , vous créez donc une nouvelle classe MyMSSQLDatabase, qui aura également les méthodes BeginTransaction et CommitTransaction.

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

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

Et parce que les classes MyMSSQLDatabase et MyMySQLDatabase ont 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 interface Database, que le MyMySQLDatabase implémente, vous avez également la classe MyMSSQLDatabase, qui a exactement les mêmes méthodes que MyMySQLDatabase, peut-être le MSSQL le pilote peut également implémenter l'interface Database, vous l'ajoutez donc à la définition.

public class MyMSSQLDatabase : Database { }

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

Oui, vous le feriez. Et cela ne sonne pas bien. À l'heure actuelle, nous savons que MyMSSQLDatabase et MyMySQLDatabase ont les mêmes méthodes et implémentent tous les deux l'interface Database. Vous refactorisez donc le SecretMethod pour qu'il ressemble à ceci.

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

Remarquez comment le SecretMethod ne sait plus, que vous utilisiez 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 du tout besoin de changer le SecretMethod. Vous allez créer un MyPostgreSQLDatabase, lui faire implémenter l'interface Database et une fois que vous aurez fini de coder le pilote PostgreSQL et que cela fonctionnera, vous créerez son instance et l'injecterez dans le SecretMethod.

3. Obtention de l'implémentation souhaitée de Database

Vous devez encore décider, avant d'appeler SecretMethod, quelle implémentation de l'interface Database vous voulez (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 classe Config peut être votre propre implémentation).

Idéalement, vous aurez le DatabaseFactory à l'intérieur de votre conteneur d'injection de dépendances. 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 méthode GetDatabase sur l'objet DatabaseFactory stocké dans votre conteneur d'injection de dépendance (le _di variable), une méthode qui vous renverra l'instance correcte de l'interface Database, 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 champ DatabaseDriver 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.

25
Andy