W jaki sposób pisane są abstrakcyjne interfejsy baz danych do obsługi wielu typów baz danych?

12

Jak zacząć projektować klasę abstrakcyjną w ich większej aplikacji, która może współpracować z kilkoma typami baz danych, takimi jak MySQL, SQLLite, MSSQL itp.?

Jak nazywa się ten wzorzec projektowy i gdzie dokładnie się zaczyna?

Powiedzmy, że musisz napisać klasę, która ma następujące metody

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

Jedyne, co mogę wymyślić, to instrukcja if w każdej Databasemetodzie

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

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

   }
}
dźwięki 31
źródło

Odpowiedzi:

11

Co chcesz jest wielokrotne implementacje dla interfejsu że aplikacja używa.

tak:

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

Jeśli chodzi o lepszy sposób ustawienia poprawnej IDatabaseimplementacji w czasie wykonywania aplikacji, powinieneś przyjrzeć się takim zagadnieniom, jak „ Metoda fabryczna ” i „ Wstrzykiwanie zależności ”.

Caleb
źródło
25

Odpowiedź Caleba, gdy jest na dobrej drodze, jest w rzeczywistości błędna. Jego Fooklasa działa zarówno jako fasada bazy danych, jak i fabryka. Są to dwa obowiązki i nie należy ich zaliczać do jednej klasy.


To pytanie, szczególnie w kontekście bazy danych, zadawano zbyt wiele razy. Tutaj postaram się dokładnie pokazać korzyści płynące z używania abstrakcji (przy użyciu interfejsów), aby Twoja aplikacja była mniej sprzężona i bardziej uniwersalna.

Przed dalszą lekturą zalecam przeczytanie i zrozumienie podstawowego zastrzyku zależności , jeśli jeszcze go nie znasz. Możesz także sprawdzić wzorzec projektowy adaptera , który w zasadzie oznacza ukrywanie szczegółów implementacji za publicznymi metodami interfejsu.

Wstrzykiwanie zależności w połączeniu z fabrycznym wzorem projektowym jest kamieniem węgielnym i łatwym sposobem na kodowanie wzoru projektowego strategii , który jest częścią zasady IoC .

Nie dzwoń do nas, my zadzwonimy do ciebie . (AKA zasada Hollywood ).


Oddzielenie aplikacji za pomocą abstrakcji

1. Wykonanie warstwy abstrakcji

Tworzysz interfejs - lub klasę abstrakcyjną, jeśli kodujesz w języku takim jak C ++ - i dodajesz ogólne metody do tego interfejsu. Ponieważ zarówno interfejsy, jak i klasy abstrakcyjne mają takie zachowanie, że nie można ich użyć bezpośrednio, ale trzeba je zaimplementować (w przypadku interfejsu) lub rozszerzyć (w przypadku klasy abstrakcyjnej), sam kod już sugeruje, że będzie muszą mieć określone implementacje, aby wypełnić kontrakt podany przez interfejs lub klasę abstrakcyjną.

Twój (bardzo prosty przykład) interfejs bazy danych może wyglądać następująco (klasy DatabaseResult lub DbQuery byłyby własnymi implementacjami reprezentującymi operacje na bazie danych):

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

Ponieważ jest to interfejs, sam tak naprawdę nic nie robi. Potrzebujesz więc klasy do wdrożenia tego interfejsu.

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

Teraz masz klasę, która implementuje Databaseinterfejs właśnie stał się użyteczny.

2. Korzystanie z warstwy abstrakcji

Gdzieś w twojej aplikacji masz metodę, nazwijmy ją SecretMethoddla zabawy, a wewnątrz tej metody musisz użyć bazy danych, ponieważ chcesz pobrać niektóre dane.

Teraz masz interfejs, którego nie możesz utworzyć bezpośrednio (uh, jak go wtedy użyć), ale masz klasę MyMySQLDatabase, którą można zbudować za pomocą newsłowa kluczowego.

ŚWIETNY! Chcę użyć bazy danych, więc użyję MyMySQLDatabase.

Twoja metoda może wyglądać następująco:

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

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

To nie jest dobre. Bezpośrednio tworzysz klasę w ramach tej metody, a jeśli robisz to wewnątrz SecretMethod, można bezpiecznie założyć, że zrobiłbyś to samo w 30 innych metodach. Jeśli chcesz zmienić MyMySQLDatabaseklasę na inną, na przykład MyPostgreSQLDatabase, musisz ją zmienić we wszystkich 30 metodach.

Innym problemem jest to, że jeśli tworzenie się MyMySQLDatabasenie powiedzie, metoda nigdy się nie skończy, a zatem będzie nieważna.

Zaczynamy od refaktoryzacji tworzenia MyMySQLDatabasepoprzez przekazanie jej jako parametru do metody (nazywa się to wstrzykiwaniem zależności).

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

To rozwiązuje problem polegający na tym, że MyMySQLDatabaseobiekt nigdy nie może zostać utworzony. Ponieważ SecretMethodoczekuje prawidłowego MyMySQLDatabaseobiektu, jeśli coś się stanie, a obiekt nigdy nie zostanie do niego przekazany, metoda nigdy się nie uruchomi. I to jest w porządku.


W niektórych aplikacjach może to wystarczyć. Możesz być zadowolony, ale przeróbmy to jeszcze lepiej.

Cel kolejnego refaktoryzacji

Widać, teraz SecretMethodużywa MyMySQLDatabaseobiektu. Załóżmy, że przeniosłeś się z MySQL na MSSQL. Naprawdę nie masz ochoty na zmianę całej logiki wewnątrz SecretMethod, metody, która wywołuje metody BeginTransactiona CommitTransactiondla databasezmiennej przekazywanej jako parametr, więc tworzysz nową klasę MyMSSQLDatabase, która również będzie miała metody BeginTransactioni CommitTransaction.

Następnie przejdź dalej i zmień deklarację SecretMethodna następujące.

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

A ponieważ klasy MyMSSQLDatabasei MyMySQLDatabasete same metody, nie musisz zmieniać niczego innego i nadal będzie działać.

Zaczekaj!

Masz Databaseinterfejs, który MyMySQLDatabaseimplementuje, masz także MyMSSQLDatabaseklasę, która ma dokładnie takie same metody jak MyMySQLDatabase, być może sterownik MSSQL mógłby również implementować Databaseinterfejs, więc dodajesz go do definicji.

public class MyMSSQLDatabase : Database { }

Ale co, jeśli w przyszłości nie chcę MyMSSQLDatabasejuż używać , ponieważ przełączyłem się na PostgreSQL? Musiałbym ponownie zastąpić definicję SecretMethod?

Tak, zrobiłbyś. I to nie brzmi dobrze. W tej chwili wiemy, że MyMSSQLDatabasei MyMySQLDatabasemamy te same metody i oba implementują Databaseinterfejs. Przebudujesz więc, SecretMethodaby wyglądał tak.

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

Zauważ, skąd SecretMethodjuż nie wiadomo, czy używasz MySQL, MSSQL czy PotgreSQL. Wie, że korzysta z bazy danych, ale nie dba o konkretną implementację.

Teraz, jeśli chcesz utworzyć nowy sterownik bazy danych, na przykład dla PostgreSQL, nie musisz wcale go zmieniać SecretMethod. Zrobisz MyPostgreSQLDatabase, wprowadzisz Databaseinterfejs i kiedy skończysz kodować sterownik PostgreSQL i zadziała, utworzysz jego instancję i wstrzykniesz go do SecretMethod.

3. Uzyskanie pożądanego wdrożenia Database

Nadal musisz zdecydować przed wywołaniem SecretMethod, którą implementację Databaseinterfejsu chcesz (czy to MySQL, MSSQL czy PostgreSQL). W tym celu można użyć fabrycznego wzorca projektowego.

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

Jak widać, fabryka wie, jakiego typu bazy danych użyć z pliku konfiguracyjnego (znowu Configklasa może być twoją własną implementacją).

Idealnie będzie mieć DatabaseFactorywnętrze pojemnika do wstrzykiwań zależności. Twój proces może więc wyglądać następująco.

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

Zobacz, jak nigdzie w procesie nie tworzysz określonego typu bazy danych. Mało tego, w ogóle nic nie tworzysz. Wywołujesz GetDatabasemetodę na DatabaseFactoryobiekcie przechowywanym w kontenerze wstrzykiwania zależności ( _dizmienną), metodę, która zwróci ci poprawne wystąpienie Databaseinterfejsu, w zależności od konfiguracji.

Jeśli po 3 tygodniach używania PostgreSQL chcesz wrócić do MySQL, otwórz pojedynczy plik konfiguracyjny i zmień wartość DatabaseDriverpola z DatabaseEnum.PostgreSQLna DatabaseEnum.MySQL. I gotowe. Nagle reszta aplikacji ponownie poprawnie używa MySQL, zmieniając jedną linię.


Jeśli nadal nie jesteś zaskoczony, polecam Ci zanurzyć się nieco w IoC. Jak podejmować określone decyzje nie z konfiguracji, ale z danych wejściowych użytkownika. Takie podejście nazywa się wzorem strategii i chociaż może być stosowane w aplikacjach korporacyjnych, jest o wiele częściej stosowane podczas tworzenia gier komputerowych.

Andy
źródło
Uwielbiam twoją odpowiedź, David. Ale podobnie jak wszystkie takie odpowiedzi, nie opisuje, w jaki sposób można je zastosować w praktyce. Prawdziwym problemem nie jest abstrahowanie od możliwości wywoływania w różnych silnikach baz danych, problemem jest faktyczna składnia SQL. Weźmy DbQueryna przykład przedmiot. Zakładając, że ten obiekt zawierał element zapytania SQL do wykonania, w jaki sposób można uczynić to ogólnym?
DonBoitnott,
1
@DonBoitnott Nie sądzę, żebyś kiedykolwiek potrzebował wszystkiego, aby być ogólnym. Zwykle chcesz wprowadzić abstrakcję między warstwami aplikacji (domena, usługi, persytencja), możesz również wprowadzić abstrakcję dla modułów, możesz wprowadzić abstrakcję do małej, ale wielokrotnego użytku i wysoce konfigurowalnej biblioteki, którą tworzysz dla większego projektu itp. Możesz po prostu wyodrębnić wszystko z interfejsów, ale rzadko jest to konieczne. Naprawdę trudno jest udzielić odpowiedzi „wszystko za wszystko”, ponieważ niestety tak naprawdę nie ma takiej odpowiedzi i wynika ona z wymagań.
Andy
2
Zrozumiany. Ale tak naprawdę to miałem na myśli dosłownie. Kiedy masz już swoją abstrakcyjną klasę i dochodzisz do punktu, w którym chcesz zadzwonić, w _secret.SecretMethod(database);jaki sposób można pogodzić to wszystko z faktem, że teraz SecretMethodnadal muszę wiedzieć, z którą DB pracuję, aby użyć właściwego dialektu SQL ? Pracowałeś bardzo ciężko, aby większość kodu była nieświadoma tego faktu, ale o 11 godzinie znów musisz wiedzieć. Jestem teraz w tej sytuacji i próbuję dowiedzieć się, jak inni rozwiązali ten problem.
DonBoitnott,
@DonBoitnott Nie wiedziałem, co masz na myśli, teraz to widzę. Możesz użyć interfejsu zamiast konkretnych implementacji DbQueryklasy, zapewnić implementacje tego interfejsu i użyć tego zamiast tego, mając fabrykę do budowy IDbQueryinstancji. Nie sądzę, żebyś potrzebował typu ogólnego dla DatabaseResultklasy, zawsze możesz oczekiwać, że wyniki z bazy danych zostaną sformatowane w podobny sposób. Rzecz w tym, że mając do czynienia z bazami danych i surowym SQL, jesteś już na tak niskim poziomie w swojej aplikacji (za DAL i repozytoriami), że nie ma potrzeby ...
Andy
... już ogólne podejście.
Andy