La risposta di Caleb, mentre è sulla buona strada, è in realtà sbagliata. La sua Foo
classe funge sia da facciata di database che da fabbrica. Queste sono due responsabilità e non dovrebbero essere inserite in un'unica classe.
Questa domanda, specialmente nel contesto del database, è stata posta troppe volte. Qui proverò a mostrarti a fondo il vantaggio di usare l'astrazione (usando le interfacce) per rendere la tua applicazione meno accoppiata e più versatile.
Prima di leggere oltre, ti consiglio di leggere e ottenere una conoscenza di base dell'iniezione di dipendenza , se non lo conosci ancora. Potresti anche voler controllare il modello di progettazione dell'adattatore , che è fondamentalmente ciò che significa nascondere i dettagli dell'implementazione dietro i metodi pubblici dell'interfaccia.
L'iniezione di dipendenza, unita al modello di progettazione di fabbrica , è la pietra miliare e un modo semplice per codificare il modello di progettazione di strategia , che fa parte del principio IoC .
Non chiamarci, ti chiameremo . (AKA il principio di Hollywood ).
Disaccoppiamento di un'applicazione mediante l'astrazione
1. Realizzare lo strato di astrazione
Si crea un'interfaccia - o una classe astratta, se si sta codificando in un linguaggio come C ++ - e si aggiungono metodi generici a questa interfaccia. Poiché entrambe le interfacce e le classi astratte hanno il comportamento di non poterle utilizzare direttamente, ma è necessario implementarle (in caso di interfaccia) o estenderle (in caso di classe astratta), il codice stesso suggerisce già, si è necessario disporre di implementazioni specifiche per completare il contratto dato dall'interfaccia o dalla classe astratta.
L'interfaccia del tuo database (esempio molto semplice) potrebbe assomigliare a questa (le classi DatabaseResult o DbQuery rispettivamente sarebbero le tue implementazioni che rappresentano le operazioni del database):
public interface Database
{
DatabaseResult DoQuery(DbQuery query);
void BeginTransaction();
void RollbackTransaction();
void CommitTransaction();
bool IsInTransaction();
}
Poiché questa è un'interfaccia, essa stessa non fa davvero nulla. Quindi hai bisogno di una classe per implementare questa interfaccia.
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
}
}
Ora hai una classe che implementa il Database
, l'interfaccia è diventata utile.
2. Utilizzo dello strato di astrazione
Da qualche parte nella tua applicazione, hai un metodo, chiamiamo il metodo SecretMethod
, solo per divertimento, e all'interno di questo metodo devi usare il database, perché vuoi recuperare alcuni dati.
Ora hai un'interfaccia, che non puoi creare direttamente (uh, come la uso allora), ma hai una classe MyMySQLDatabase
, che può essere costruita usando la new
parola chiave.
GRANDE! Voglio usare un database, quindi userò il MyMySQLDatabase
.
Il tuo metodo potrebbe apparire così:
public void SecretMethod()
{
var database = new MyMySQLDatabase(new CSharpMySQLDriver());
// you will use the database here, which has the DoQuery,
// BeginTransaction, RollbackTransaction and CommitTransaction methods
}
Questo non è un bene. Stai creando direttamente una classe all'interno di questo metodo e, se lo stai facendo all'interno di SecretMethod
, è sicuro supporre che faresti lo stesso in altri 30 metodi. Se si desidera modificare il tipo MyMySQLDatabase
in una classe diversa, ad esempio MyPostgreSQLDatabase
, è necessario modificarlo in tutti i 30 metodi.
Un altro problema è che, se la creazione MyMySQLDatabase
non è riuscita, il metodo non finirà mai e quindi non sarebbe valido.
Iniziamo con il refactoring della creazione del MyMySQLDatabase
passandolo come parametro al metodo (questo si chiama iniezione di dipendenza).
public void SecretMethod(MyMySQLDatabase database)
{
// use the database here
}
Questo ti risolve il problema, che l' MyMySQLDatabase
oggetto non potrebbe mai essere creato. Poiché si SecretMethod
aspetta un MyMySQLDatabase
oggetto valido , se accadesse qualcosa e l'oggetto non gli sarebbe mai passato, il metodo non verrebbe mai eseguito. E va benissimo.
In alcune applicazioni questo potrebbe essere sufficiente. Potresti essere soddisfatto, ma rifattiamolo per essere ancora migliore.
Lo scopo di un altro refactoring
Puoi vedere, in questo momento SecretMethod
usa un MyMySQLDatabase
oggetto. Supponiamo che tu sia passato da MySQL a MSSQL. Non hai davvero voglia di cambiare tutta la logica dentro di te SecretMethod
, un metodo che chiama a BeginTransaction
e CommitTransaction
metodi sulla database
variabile passati come parametro, quindi crei una nuova classe MyMSSQLDatabase
, che avrà anche i metodi BeginTransaction
e CommitTransaction
.
Quindi vai avanti e modifica la dichiarazione SecretMethod
come segue.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
E perché le classi MyMSSQLDatabase
e MyMySQLDatabase
hanno gli stessi metodi, non è necessario cambiare niente altro e si continuerà a funzionare.
Oh aspetta!
Hai Database
un'interfaccia, che MyMySQLDatabase
implementa, hai anche la MyMSSQLDatabase
classe, che ha esattamente gli stessi metodi di MyMySQLDatabase
, forse il driver MSSQL potrebbe anche implementare l' Database
interfaccia, quindi la aggiungi alla definizione.
public class MyMSSQLDatabase : Database { }
E se, in futuro, non volessi più utilizzare MyMSSQLDatabase
, perché sono passato a PostgreSQL? Dovrei, ancora una volta, sostituire la definizione di SecretMethod
?
Sì, lo faresti. E questo non suona bene. In questo momento sappiamo, che MyMSSQLDatabase
e MyMySQLDatabase
hanno gli stessi metodi ed entrambi implementare l' Database
interfaccia. Quindi rifattori SecretMethod
per assomigliare a questo.
public void SecretMethod(Database database)
{
// use the database here
}
Si noti come SecretMethod
non si sa più se si utilizza MySQL, MSSQL o PotgreSQL. Sa che utilizza un database, ma non si preoccupa dell'implementazione specifica.
Ora, se si desidera creare il nuovo driver del database, ad esempio per PostgreSQL, non è necessario modificarlo SecretMethod
affatto. Farai un MyPostgreSQLDatabase
, lo farai implementare l' Database
interfaccia e una volta che avrai finito di codificare il driver PostgreSQL e funzionerà, creerai la sua istanza e la inietterai nel SecretMethod
.
3. Ottenere l'implementazione desiderata di Database
Devi ancora decidere, prima di chiamare il SecretMethod
, quale implementazione Database
dell'interfaccia desiderata (se si tratta di MySQL, MSSQL o PostgreSQL). Per questo, è possibile utilizzare il modello di progettazione di fabbrica.
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;
}
}
La fabbrica, come puoi vedere, sa quale tipo di database usare da un file di configurazione (di nuovo, la Config
classe potrebbe essere la tua implementazione).
Idealmente, avrai l' DatabaseFactory
interno del contenitore per l'iniezione delle dipendenze. Il tuo processo potrebbe quindi apparire così.
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);
}
}
Guarda come da nessuna parte nel processo stai creando un tipo di database specifico. Non solo, non stai creando nulla. Stai chiamando un GetDatabase
metodo DatabaseFactory
sull'oggetto memorizzato nel tuo contenitore di iniezione di dipendenza (la _di
variabile), un metodo, che ti restituirà l'istanza corretta Database
dell'interfaccia, in base alla tua configurazione.
Se, dopo 3 settimane di utilizzo di PostgreSQL, vuoi tornare a MySQL, apri un singolo file di configurazione e cambi il valore del DatabaseDriver
campo da DatabaseEnum.PostgreSQL
a DatabaseEnum.MySQL
. E il gioco è fatto. All'improvviso il resto dell'applicazione utilizza correttamente MySQL, cambiando una sola riga.
Se non sei ancora sorpreso, ti consiglio di immergerti un po 'di più nell'IoC. Come è possibile prendere determinate decisioni non da una configurazione, ma da un input dell'utente. Questo approccio è chiamato modello di strategia e sebbene possa essere ed è utilizzato nelle applicazioni aziendali, è molto più frequentemente utilizzato durante lo sviluppo di giochi per computer.
DbQuery
oggetto, per esempio. Supponendo che l'oggetto contenesse un membro per l'esecuzione di una stringa di query SQL, come si può renderlo generico?