La réponse de Caleb, alors qu'il est sur la bonne voie, est en fait fausse. Sa Foo
classe agit à la fois comme façade de base de données et comme usine. Ce sont deux responsabilités et ne devraient pas être regroupées dans une seule classe.
Cette question, en particulier dans le contexte de la base de données, a été posée trop souvent. Ici, je vais essayer de vous montrer en détail l'avantage d'utiliser l'abstraction (en utilisant des interfaces) pour rendre votre application moins couplée et plus polyvalente.
Avant de poursuivre la lecture, je vous recommande de lire et d'acquérir une compréhension de base de l' injection de dépendance , si vous ne la connaissez pas encore. Vous pouvez également vouloir vérifier le modèle de conception de l' adaptateur , qui est essentiellement ce que signifie masquer les détails d'implémentation derrière les méthodes publiques de l'interface.
L'injection de dépendances, couplée au modèle de conception Factory , est la pierre angulaire et un moyen facile de coder le modèle de conception Strategy , qui fait partie du principe IoC .
Ne nous appelez pas, nous vous appellerons . (AKA le principe d'Hollywood ).
Découplage d'une application à l'aide de l'abstraction
1. Réalisation de la couche d'abstraction
Vous créez une interface - ou une classe abstraite, si vous codez dans un langage comme C ++ - et ajoutez des méthodes génériques à cette interface. Parce que les interfaces et les classes abstraites ont le comportement de ne pas pouvoir les utiliser directement, mais vous devez les implémenter (en cas d'interface) ou les étendre (en cas de classe abstraite), le code lui-même le suggère déjà, vous le ferez besoin d'avoir des implémentations spécifiques pour remplir le contrat donné par l'interface ou la classe abstraite.
Votre (exemple très simple) interface de base de données pourrait ressembler à ceci (les classes DatabaseResult ou DbQuery respectivement seraient vos propres implémentations représentant les opérations de base de données):
public interface Database
{
DatabaseResult DoQuery(DbQuery query);
void BeginTransaction();
void RollbackTransaction();
void CommitTransaction();
bool IsInTransaction();
}
Parce que c'est une interface, elle ne fait rien en elle-même. Vous avez donc besoin d'une classe pour implémenter cette interface.
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
}
}
Vous avez maintenant une classe qui implémente le Database
, l'interface est juste devenue utile.
2. Utilisation de la couche d'abstraction
Quelque part dans votre application, vous avez une méthode, appelons-la SecretMethod
, juste pour le plaisir, et à l'intérieur de cette méthode, vous devez utiliser la base de données, car vous voulez récupérer des données.
Vous avez maintenant une interface, que vous ne pouvez pas créer directement (euh, comment puis-je l'utiliser alors), mais vous avez une classe MyMySQLDatabase
, qui peut être construite à l'aide du new
mot - clé.
GÉNIAL! Je souhaite utiliser une base de données, je vais donc utiliser le MyMySQLDatabase
.
Votre méthode pourrait ressembler à ceci:
public void SecretMethod()
{
var database = new MyMySQLDatabase(new CSharpMySQLDriver());
// you will use the database here, which has the DoQuery,
// BeginTransaction, RollbackTransaction and CommitTransaction methods
}
Ce n'est pas bien. Vous créez directement une classe à l'intérieur de cette méthode, et si vous le faites à l'intérieur de SecretMethod
, il est sûr de supposer que vous feriez de même dans 30 autres méthodes. Si vous vouliez changer le MyMySQLDatabase
pour une classe différente, par exemple MyPostgreSQLDatabase
, vous devriez le changer dans toutes vos 30 méthodes.
Un autre problème est que si la création de MyMySQLDatabase
échouait, la méthode ne se terminerait jamais et ne serait donc pas valide.
Nous commençons par refactoriser la création de la MyMySQLDatabase
en la passant comme paramètre à la méthode (c'est ce qu'on appelle l'injection de dépendance).
public void SecretMethod(MyMySQLDatabase database)
{
// use the database here
}
Cela vous résout le problème, que l' MyMySQLDatabase
objet n'a jamais pu être créé. Étant donné que le SecretMethod
attend un MyMySQLDatabase
objet valide , si quelque chose s'est produit et que l'objet ne lui est jamais transmis, la méthode ne s'exécute jamais. Et c'est tout à fait bien.
Dans certaines applications, cela peut suffire. Vous pouvez être satisfait, mais refactorisons-le pour qu'il soit encore meilleur.
Le but d'un autre refactoring
Vous pouvez voir, en ce moment, SecretMethod
utilise un MyMySQLDatabase
objet. Supposons que vous êtes passé de MySQL à MSSQL. Vous ne vous sentez pas vraiment envie de changer toute la logique dans votre SecretMethod
, une méthode qui appelle BeginTransaction
et CommitTransaction
méthodes sur la database
variable passée en paramètre, vous créez une nouvelle classe MyMSSQLDatabase
, qui aura également la BeginTransaction
et CommitTransaction
méthodes.
Ensuite, vous allez de l'avant et modifiez la déclaration de SecretMethod
ce qui suit.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
Et parce que les classes MyMSSQLDatabase
et MyMySQLDatabase
ont les mêmes méthodes, vous n'avez pas besoin de changer quoi que ce soit d'autre et cela fonctionnera toujours.
Oh, attendez!
Vous avez une Database
interface, qui MyMySQLDatabase
implémente, vous avez également la MyMSSQLDatabase
classe, qui a exactement les mêmes méthodes que MyMySQLDatabase
, peut-être que le pilote MSSQL pourrait également implémenter l' Database
interface, alors vous l'ajoutez à la définition.
public class MyMSSQLDatabase : Database { }
Mais que se passe-t-il si, à l'avenir, je ne veux MyMSSQLDatabase
plus l'utiliser , parce que je suis passé à PostgreSQL? Je devrais, encore une fois, remplacer la définition du SecretMethod
?
Oui, vous le feriez. Et cela ne sonne pas bien. En ce moment , nous savons que MyMSSQLDatabase
et MyMySQLDatabase
ont les mêmes méthodes et à la fois mettre en œuvre l' Database
interface. Donc, vous refactorisez le SecretMethod
pour ressembler à ceci.
public void SecretMethod(Database database)
{
// use the database here
}
Remarquez, comment le SecretMethod
ne sait plus, si vous utilisez MySQL, MSSQL ou PotgreSQL. Il sait qu'il utilise une base de données, mais ne se soucie pas de l'implémentation spécifique.
Maintenant, si vous vouliez créer votre nouveau pilote de base de données, pour PostgreSQL par exemple, vous n'aurez pas besoin de changer du SecretMethod
tout. Vous ferez un MyPostgreSQLDatabase
, lui ferez implémenter l' Database
interface et une fois que vous aurez fini de coder le pilote PostgreSQL et qu'il fonctionnera, vous créerez son instance et l'injecterez dans le SecretMethod
.
3. Obtenir la mise en œuvre souhaitée de Database
Vous devez encore décider, avant d'appeler le SecretMethod
, quelle implémentation de l' Database
interface vous souhaitez (que ce soit MySQL, MSSQL ou PostgreSQL). Pour cela, vous pouvez utiliser le modèle de conception d'usine.
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 fabrique, comme vous pouvez le voir, sait quel type de base de données utiliser à partir d'un fichier de configuration (encore une fois, la Config
classe peut être votre propre implémentation).
Idéalement, vous aurez l' DatabaseFactory
intérieur de votre conteneur d'injection de dépendance. Votre processus peut alors ressembler à ceci.
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);
}
}
Regardez, comment nulle part dans le processus, vous créez un type de base de données spécifique. Non seulement cela, vous ne créez rien du tout. Vous appelez une GetDatabase
méthode sur l' DatabaseFactory
objet stocké à l'intérieur de votre conteneur d'injection de dépendance (la _di
variable), une méthode, qui vous renverra l'instance d' Database
interface correcte , en fonction de votre configuration.
Si, après 3 semaines d'utilisation de PostgreSQL, vous souhaitez revenir à MySQL, vous ouvrez un seul fichier de configuration et modifiez la valeur du DatabaseDriver
champ de DatabaseEnum.PostgreSQL
à DatabaseEnum.MySQL
. Et vous avez terminé. Du coup, le reste de votre application utilise à nouveau correctement MySQL, en changeant une seule ligne.
Si vous n'êtes toujours pas étonné, je vous recommande de plonger un peu plus dans l'IoC. Comment vous pouvez prendre certaines décisions non pas à partir d'une configuration, mais à partir d'une entrée utilisateur. Cette approche est appelée le modèle de stratégie et bien qu'elle puisse être et est utilisée dans les applications d'entreprise, elle est beaucoup plus fréquemment utilisée lors du développement de jeux informatiques.
DbQuery
objet, par exemple. En supposant que cet objet contenait un membre pour une chaîne de requête SQL à exécuter, comment pourrait-on rendre cela générique?_secret.SecretMethod(database);
comment réconcilier tout cela avec le fait que maintenant monSecretMethod
doit encore savoir avec quelle base de données je travaille afin d'utiliser le dialecte SQL approprié ? Vous avez travaillé très dur pour que la majorité du code ignore ce fait, mais à la 11e heure, vous devez à nouveau le savoir. Je suis dans cette situation maintenant et j'essaie de comprendre comment d'autres ont résolu ce problème.DbQuery
classe, fournir des implémentations de ladite interface et utiliser celle-ci à la place, en ayant une fabrique pour construire l'IDbQuery
instance. Je ne pense pas que vous auriez besoin d'un type générique pour laDatabaseResult
classe, vous pouvez toujours vous attendre à ce que les résultats d'une base de données soient formatés de manière similaire. Le problème ici est que, lorsque vous traitez avec des bases de données et du SQL brut, vous êtes déjà à un niveau si bas sur votre application (derrière DAL et les référentiels), qu'il n'y a pas besoin de ...