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.
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?