Wie werden abstrakte Datenbankschnittstellen geschrieben, um mehrere Datenbanktypen zu unterstützen?

12

Wie fängt man an, eine abstrakte Klasse in ihrer größeren Anwendung zu entwerfen, die mit verschiedenen Arten von Datenbanken wie MySQL, SQLLite, MSSQL usw. kommunizieren kann?

Wie heißt dieses Entwurfsmuster und wo genau beginnt es?

Angenommen, Sie müssen eine Klasse mit den folgenden Methoden schreiben

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

Das einzige, was mir einfällt, ist eine if-Anweisung in jeder einzelnen DatabaseMethode

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

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

   }
}
tones31
quelle

Antworten:

11

Was Sie wollen, sind mehrere Implementierungen für die Schnittstelle , die Ihre Anwendung verwendet.

wie so:

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

Um die korrekte IDatabaseImplementierung zur Laufzeit in Ihrer Anwendung besser einrichten zu können, sollten Sie sich mit Themen wie " Factory-Methode " und " Abhängigkeitsinjektion " befassen .

Caleb
quelle
25

Calebs Antwort ist falsch, obwohl er auf dem richtigen Weg ist. Seine FooKlasse fungiert sowohl als Datenbankfassade als auch als Fabrik. Dies sind zwei Verantwortlichkeiten und sollten nicht in eine einzelne Klasse eingeordnet werden.


Diese Frage, insbesondere im Datenbankkontext, wurde zu oft gestellt. Hier werde ich versuchen, Ihnen den Nutzen der Verwendung von Abstraktion (unter Verwendung von Schnittstellen) zu zeigen, um Ihre Anwendung weniger gekoppelt und vielseitiger zu machen.

Bevor Sie weiterlesen, empfehle ich Ihnen, sich mit der Abhängigkeitsinjektion vertraut zu machen , falls Sie diese noch nicht kennen. Möglicherweise möchten Sie auch das Adapter-Entwurfsmuster überprüfen. Dies ist im Grunde das Verbergen von Implementierungsdetails hinter den öffentlichen Methoden der Schnittstelle.

Die Abhängigkeitsinjektion in Verbindung mit dem Factory-Entwurfsmuster ist der Grundstein und eine einfache Möglichkeit, das Strategieentwurfsmuster zu codieren , das Teil des IoC-Prinzips ist .

Rufen Sie uns nicht an, wir rufen Sie an . (AKA das Hollywood-Prinzip ).


Entkopplung einer Anwendung durch Abstraktion

1. Erstellen Sie die Abstraktionsschicht

Sie erstellen eine Schnittstelle - oder abstrakte Klasse, wenn Sie in einer Sprache wie C ++ codieren - und fügen dieser Schnittstelle generische Methoden hinzu. Da sowohl Interfaces als auch abstrakte Klassen das Verhalten haben, dass Sie sie nicht direkt verwenden können, sondern sie entweder implementieren (bei Interfaces) oder erweitern (bei abstrakten Klassen) müssen, schlägt der Code dies bereits vor Es sind spezielle Implementierungen erforderlich, um den von der Schnittstelle oder der abstrakten Klasse angegebenen Vertrag zu erfüllen.

Ihre (sehr einfache Beispiel-) Datenbankschnittstelle könnte folgendermaßen aussehen (die DatabaseResult- bzw. DbQuery-Klassen wären Ihre eigenen Implementierungen, die Datenbankoperationen darstellen):

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

Da dies eine Schnittstelle ist, tut sie selbst nichts. Sie benötigen also eine Klasse, um diese Schnittstelle zu implementieren.

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

Jetzt haben Sie eine Klasse, die das implementiert Database, die Schnittstelle wurde gerade nützlich.

2. Verwenden der Abstraktionsschicht

Irgendwo in Ihrer Anwendung haben Sie eine Methode, die wir SecretMethodnur zum Spaß aufrufen , und innerhalb dieser Methode müssen Sie die Datenbank verwenden, weil Sie einige Daten abrufen möchten.

Jetzt haben Sie eine Schnittstelle, die Sie nicht direkt erstellen können (wie verwende ich sie dann?), Aber Sie haben eine Klasse MyMySQLDatabase, die mit dem newSchlüsselwort erstellt werden kann.

GROSSARTIG! Ich möchte eine Datenbank verwenden, also verwende ich die MyMySQLDatabase.

Ihre Methode könnte folgendermaßen aussehen:

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

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

Das ist nicht gut. Sie erstellen direkt eine Klasse innerhalb dieser Methode. Wenn Sie dies innerhalb der Methode tun SecretMethod, können Sie davon ausgehen, dass Sie dies auch in 30 anderen Methoden tun würden. Wenn Sie beispielsweise MyMySQLDatabasein eine andere Klasse MyPostgreSQLDatabasewechseln möchten, müssen Sie dies in all Ihren 30 Methoden ändern.

Ein anderes Problem ist, wenn die Erstellung MyMySQLDatabasefehlschlägt, wird die Methode niemals beendet und ist daher ungültig.

Wir beginnen mit der Umgestaltung der Erstellung von, MyMySQLDatabaseindem wir sie als Parameter an die Methode übergeben (dies wird als Abhängigkeitsinjektion bezeichnet).

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

Dies löst das Problem, dass das MyMySQLDatabaseObjekt niemals erstellt werden konnte. Da das Objekt SecretMethodein gültiges MyMySQLDatabaseObjekt erwartet , würde die Methode niemals ausgeführt, wenn etwas passiert und das Objekt niemals an das Objekt übergeben würde. Und das ist völlig in Ordnung.


In einigen Anwendungen kann dies ausreichen. Sie mögen zufrieden sein, aber lassen Sie es uns umgestalten, um es noch besser zu machen.

Der Zweck eines anderen Refactorings

Sie können sehen, dass gerade SecretMethodein MyMySQLDatabaseObjekt verwendet wird. Nehmen wir an, Sie sind von MySQL zu MSSQL gewechselt. Sie haben nicht wirklich Lust, die gesamte Logik in Ihrer SecretMethodMethode zu ändern , die eine BeginTransactionund CommitTransaction-Methode für die databaseals Parameter übergebene Variable aufruft, und erstellen eine neue Klasse MyMSSQLDatabase, die auch die BeginTransactionund CommitTransaction-Methoden enthält.

Dann gehen Sie vor und ändern Sie die Deklaration SecretMethodwie folgt.

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

Und weil die Klassen MyMSSQLDatabaseund MyMySQLDatabasedie gleichen Methoden haben, müssen Sie nichts anderes ändern und es wird immer noch funktionieren.

Oh, Moment mal!

Sie haben eine DatabaseSchnittstelle, die MyMySQLDatabaseimplementiert wird, Sie haben auch die MyMSSQLDatabaseKlasse, die genau die gleichen Methoden hat wie MyMySQLDatabase, vielleicht könnte der MSSQL-Treiber die DatabaseSchnittstelle auch implementieren , also fügen Sie sie der Definition hinzu.

public class MyMSSQLDatabase : Database { }

Aber was ist, wenn ich das in Zukunft nicht mehr verwenden möchte MyMSSQLDatabase, weil ich auf PostgreSQL umgestiegen bin? Ich müsste noch einmal die Definition des SecretMethod?

Ja du würdest. Und das klingt nicht richtig. Im Moment wissen wir, dass MyMSSQLDatabaseund MyMySQLDatabasehaben die gleichen Methoden und beide implementieren die DatabaseSchnittstelle. Also überarbeiten Sie das SecretMethod, um so auszusehen.

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

Beachten Sie, woher der SecretMethodnicht mehr weiß, ob Sie MySQL, MSSQL oder PotgreSQL verwenden. Es weiß, dass es eine Datenbank verwendet, kümmert sich aber nicht um die spezifische Implementierung.

Wenn Sie nun Ihren neuen Datenbanktreiber erstellen möchten, zum Beispiel für PostgreSQL, müssen Sie den überhaupt nicht ändern SecretMethod. Sie erstellen ein MyPostgreSQLDatabase, lassen es die DatabaseSchnittstelle implementieren , und sobald Sie den PostgreSQL-Treiber codiert haben und er funktioniert, erstellen Sie seine Instanz und fügen ihn in das ein SecretMethod.

3. Erhalten der gewünschten Implementierung von Database

Sie müssen sich noch vor dem Aufruf der entscheiden SecretMethod, welche Implementierung der DatabaseSchnittstelle Sie wollen (ob es sich um MySQL, MSSQL oder PostgreSQL handelt). Hierfür können Sie das Factory Design Pattern verwenden.

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

Wie Sie sehen, weiß die Factory anhand einer Konfigurationsdatei, welcher Datenbanktyp zu verwenden ist (auch hier kann die ConfigKlasse Ihre eigene Implementierung sein).

Im Idealfall haben Sie das DatabaseFactoryInnere Ihres Abhängigkeitsinjektionsbehälters. Ihr Prozess könnte dann so aussehen.

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

Schauen Sie, wie nirgendwo im Prozess Sie einen bestimmten Datenbanktyp erstellen. Nicht nur das, du erschaffst überhaupt nichts. Sie rufen eine GetDatabaseMethode für das DatabaseFactoryObjekt auf, das in Ihrem Abhängigkeitsinjektionscontainer (der _diVariablen) gespeichert ist. Diese Methode Databasegibt basierend auf Ihrer Konfiguration die richtige Instanz der Schnittstelle zurück.

Wenn Sie nach 3 Wochen mit PostgreSQL zu MySQL zurückkehren möchten, öffnen Sie eine einzelne Konfigurationsdatei und ändern den Feldwert DatabaseDrivervon DatabaseEnum.PostgreSQLin DatabaseEnum.MySQL. Und du bist fertig. Plötzlich verwendet der Rest Ihrer Anwendung MySQL wieder korrekt, indem eine einzelne Zeile geändert wird.


Wenn Sie immer noch nicht begeistert sind, empfehle ich Ihnen, sich ein bisschen mehr mit IoC zu beschäftigen. Wie Sie bestimmte Entscheidungen nicht aus einer Konfiguration, sondern aus einer Benutzereingabe treffen können. Dieser Ansatz wird als Strategiemuster bezeichnet, und obwohl er in Unternehmensanwendungen verwendet werden kann und wird, wird er bei der Entwicklung von Computerspielen viel häufiger verwendet.

Andy
quelle
Liebe deine Antwort, David. Aber wie alle diese Antworten reicht es nicht aus, zu beschreiben, wie man es in die Praxis umsetzen könnte. Das eigentliche Problem besteht nicht darin, die Fähigkeit zum Aufrufen verschiedener Datenbankmodule zu verbergen, sondern in der tatsächlichen SQL-Syntax. Nehmen Sie DbQueryzum Beispiel Ihr Objekt. Angenommen, dieses Objekt enthält einen Member für eine auszuführende SQL-Abfragezeichenfolge. Wie kann man diesen generischen Wert festlegen?
DonBoitnott
1
@DonBoitnott Ich glaube nicht, dass Sie jemals alles brauchen würden, um generisch zu sein. Normalerweise möchten Sie Abstraktionen zwischen Anwendungsebenen (Domäne, Dienste, Persistenz) einführen. Möglicherweise möchten Sie auch Abstraktionen für Module einführen. Sie möchten Abstraktionen für eine kleine, aber wiederverwendbare und stark anpassbare Bibliothek einführen, die Sie für ein größeres Projekt entwickeln Man könnte einfach alles zu Interfaces abstrahieren, aber das ist selten nötig. Es ist wirklich schwierig, eine Antwort für alles zu geben, da es leider wirklich keine gibt und sie von den Anforderungen herrührt.
Andy
2
Verstanden. Aber das habe ich wirklich wörtlich gemeint. Sobald Sie Ihre abstrahierte Klasse haben und an dem Punkt angelangt sind, an dem Sie aufrufen möchten, _secret.SecretMethod(database);wie kann man all das in Einklang bringen, mit der Tatsache, dass meine Klasse jetzt SecretMethodnoch wissen muss, mit welcher Datenbank ich arbeite, um den richtigen SQL-Dialekt zu verwenden ? Sie haben sehr hart gearbeitet, um den größten Teil des Codes von dieser Tatsache unberücksichtigt zu lassen, aber um die 11. Stunde müssen Sie es erneut wissen. Ich bin jetzt in dieser Situation und versuche herauszufinden, wie andere dieses Problem gelöst haben.
DonBoitnott
@DonBoitnott Ich wusste nicht, was du meinst, ich sehe es jetzt. Sie können eine Schnittstelle anstelle einer konkreten Implementierung der DbQueryKlasse verwenden, Implementierungen dieser Schnittstelle bereitstellen und stattdessen diese verwenden, indem Sie eine Factory zum Erstellen der IDbQueryInstanz verwenden. Ich glaube nicht, dass Sie einen generischen Typ für die DatabaseResultKlasse benötigen , Sie können immer erwarten, dass die Ergebnisse einer Datenbank auf ähnliche Weise formatiert werden. Die Sache hier ist, wenn Sie mit Datenbanken und unformatiertem SQL zu tun haben, befinden Sie sich in Ihrer Anwendung bereits auf einem so niedrigen Niveau (hinter DAL und Repositories), dass es nicht erforderlich ist, ...
Andy
... generischer Ansatz mehr.
Andy