Odpowiedź Caleba, gdy jest na dobrej drodze, jest w rzeczywistości błędna. Jego Foo
klasa 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 Database
interfejs właśnie stał się użyteczny.
2. Korzystanie z warstwy abstrakcji
Gdzieś w twojej aplikacji masz metodę, nazwijmy ją SecretMethod
dla 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ą new
sł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ć MyMySQLDatabase
klasę na inną, na przykład MyPostgreSQLDatabase
, musisz ją zmienić we wszystkich 30 metodach.
Innym problemem jest to, że jeśli tworzenie się MyMySQLDatabase
nie powiedzie, metoda nigdy się nie skończy, a zatem będzie nieważna.
Zaczynamy od refaktoryzacji tworzenia MyMySQLDatabase
poprzez 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 MyMySQLDatabase
obiekt nigdy nie może zostać utworzony. Ponieważ SecretMethod
oczekuje prawidłowego MyMySQLDatabase
obiektu, 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 SecretMethod
używa MyMySQLDatabase
obiektu. 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 BeginTransaction
a CommitTransaction
dla database
zmiennej przekazywanej jako parametr, więc tworzysz nową klasę MyMSSQLDatabase
, która również będzie miała metody BeginTransaction
i CommitTransaction
.
Następnie przejdź dalej i zmień deklarację SecretMethod
na następujące.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
A ponieważ klasy MyMSSQLDatabase
i MyMySQLDatabase
te same metody, nie musisz zmieniać niczego innego i nadal będzie działać.
Zaczekaj!
Masz Database
interfejs, który MyMySQLDatabase
implementuje, masz także MyMSSQLDatabase
klasę, która ma dokładnie takie same metody jak MyMySQLDatabase
, być może sterownik MSSQL mógłby również implementować Database
interfejs, więc dodajesz go do definicji.
public class MyMSSQLDatabase : Database { }
Ale co, jeśli w przyszłości nie chcę MyMSSQLDatabase
już 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 MyMSSQLDatabase
i MyMySQLDatabase
mamy te same metody i oba implementują Database
interfejs. Przebudujesz więc, SecretMethod
aby wyglądał tak.
public void SecretMethod(Database database)
{
// use the database here
}
Zauważ, skąd SecretMethod
już 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 Database
interfejs 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ę Database
interfejsu 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 Config
klasa może być twoją własną implementacją).
Idealnie będzie mieć DatabaseFactory
wnę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 GetDatabase
metodę na DatabaseFactory
obiekcie przechowywanym w kontenerze wstrzykiwania zależności ( _di
zmienną), metodę, która zwróci ci poprawne wystąpienie Database
interfejsu, 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ść DatabaseDriver
pola z DatabaseEnum.PostgreSQL
na 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.
DbQuery
na 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?_secret.SecretMethod(database);
jaki sposób można pogodzić to wszystko z faktem, że terazSecretMethod
nadal 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.DbQuery
klasy, zapewnić implementacje tego interfejsu i użyć tego zamiast tego, mając fabrykę do budowyIDbQuery
instancji. Nie sądzę, żebyś potrzebował typu ogólnego dlaDatabaseResult
klasy, 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 ...