Calebs Antwort ist falsch, obwohl er auf dem richtigen Weg ist. Seine Foo
Klasse 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 SecretMethod
nur 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 new
Schlü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 MyMySQLDatabase
in eine andere Klasse MyPostgreSQLDatabase
wechseln möchten, müssen Sie dies in all Ihren 30 Methoden ändern.
Ein anderes Problem ist, wenn die Erstellung MyMySQLDatabase
fehlschlägt, wird die Methode niemals beendet und ist daher ungültig.
Wir beginnen mit der Umgestaltung der Erstellung von, MyMySQLDatabase
indem 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 MyMySQLDatabase
Objekt niemals erstellt werden konnte. Da das Objekt SecretMethod
ein gültiges MyMySQLDatabase
Objekt 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 SecretMethod
ein MyMySQLDatabase
Objekt verwendet wird. Nehmen wir an, Sie sind von MySQL zu MSSQL gewechselt. Sie haben nicht wirklich Lust, die gesamte Logik in Ihrer SecretMethod
Methode zu ändern , die eine BeginTransaction
und CommitTransaction
-Methode für die database
als Parameter übergebene Variable aufruft, und erstellen eine neue Klasse MyMSSQLDatabase
, die auch die BeginTransaction
und CommitTransaction
-Methoden enthält.
Dann gehen Sie vor und ändern Sie die Deklaration SecretMethod
wie folgt.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
Und weil die Klassen MyMSSQLDatabase
und MyMySQLDatabase
die gleichen Methoden haben, müssen Sie nichts anderes ändern und es wird immer noch funktionieren.
Oh, Moment mal!
Sie haben eine Database
Schnittstelle, die MyMySQLDatabase
implementiert wird, Sie haben auch die MyMSSQLDatabase
Klasse, die genau die gleichen Methoden hat wie MyMySQLDatabase
, vielleicht könnte der MSSQL-Treiber die Database
Schnittstelle 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 MyMSSQLDatabase
und MyMySQLDatabase
haben die gleichen Methoden und beide implementieren die Database
Schnittstelle. Also überarbeiten Sie das SecretMethod
, um so auszusehen.
public void SecretMethod(Database database)
{
// use the database here
}
Beachten Sie, woher der SecretMethod
nicht 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 Database
Schnittstelle 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 Database
Schnittstelle 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 Config
Klasse Ihre eigene Implementierung sein).
Im Idealfall haben Sie das DatabaseFactory
Innere 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 GetDatabase
Methode für das DatabaseFactory
Objekt auf, das in Ihrem Abhängigkeitsinjektionscontainer (der _di
Variablen) gespeichert ist. Diese Methode Database
gibt 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 DatabaseDriver
von DatabaseEnum.PostgreSQL
in 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.
DbQuery
zum Beispiel Ihr Objekt. Angenommen, dieses Objekt enthält einen Member für eine auszuführende SQL-Abfragezeichenfolge. Wie kann man diesen generischen Wert festlegen?_secret.SecretMethod(database);
wie kann man all das in Einklang bringen, mit der Tatsache, dass meine Klasse jetztSecretMethod
noch 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.DbQuery
Klasse verwenden, Implementierungen dieser Schnittstelle bereitstellen und stattdessen diese verwenden, indem Sie eine Factory zum Erstellen derIDbQuery
Instanz verwenden. Ich glaube nicht, dass Sie einen generischen Typ für dieDatabaseResult
Klasse 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, ...