Disons que nous avons l'interface suivante -
interface IDatabase {
string ConnectionString{get;set;}
void ExecuteNoQuery(string sql);
void ExecuteNoQuery(string[] sql);
//Various other methods all requiring ConnectionString to be set
}
La condition préalable est que ConnectionString doit être défini / initialisé avant que l'une des méthodes puisse être exécutée.
Cette condition préalable peut être quelque peu atteinte en passant une connectionString via un constructeur si IDatabase était une classe abstraite ou concrète -
abstract class Database {
public string ConnectionString{get;set;}
public Database(string connectionString){ ConnectionString = connectionString;}
public void ExecuteNoQuery(string sql);
public void ExecuteNoQuery(string[] sql);
//Various other methods all requiring ConnectionString to be set
}
Alternativement, nous pouvons créer connectionString un paramètre pour chaque méthode, mais il semble pire que de simplement créer une classe abstraite -
interface IDatabase {
void ExecuteNoQuery(string connectionString, string sql);
void ExecuteNoQuery(string connectionString, string[] sql);
//Various other methods all with the connectionString parameter
}
Des questions -
- Existe-t-il un moyen de spécifier cette condition préalable dans l'interface elle-même? C'est un "contrat" valide, donc je me demande s'il y a une fonctionnalité ou un modèle de langage pour cela (la solution de classe abstraite est plus un hack imo en plus de la nécessité de créer deux types - une interface et une classe abstraite - à chaque fois cela est nécessaire)
- Il s'agit plus d'une curiosité théorique - cette condition préalable entre-t-elle réellement dans la définition d'une condition préalable comme dans le contexte du LSP?
c#
solid
liskov-substitution
Achille
la source
la source
Réponses:
Oui. À partir de .Net 4.0, Microsoft propose des contrats de code . Ceux-ci peuvent être utilisés pour définir des conditions préalables dans le formulaire
Contract.Requires( ConnectionString != null );
. Cependant, pour que cela fonctionne pour une interface, vous aurez toujours besoin d'une classe d'assistanceIDatabaseContract
, qui sera attachée àIDatabase
, et la condition préalable doit être définie pour chaque méthode individuelle de votre interface où elle doit se tenir. Voir ici pour un exemple détaillé d'interfaces.Oui , le LSP traite à la fois des parties syntaxiques et sémantiques d'un contrat.
la source
La connexion et l'interrogation sont deux préoccupations distinctes. En tant que tels, ils devraient avoir deux interfaces distinctes.
Cela garantit à la fois qu'il
IDatabase
sera connecté lorsqu'il est utilisé et rend le client indépendant de l'interface dont il n'a pas besoin.la source
IDatabase
interface définit un objet capable d'établir une connexion à une base de données puis d'exécuter des requêtes arbitraires. Il est l'objet qui sert de frontière entre la base et le reste du code. En tant que tel, cet objet doit conserver un état (tel qu'une transaction) qui peut affecter le comportement des requêtes. Les mettre dans la même classe est très pratique.Prenons un peu de recul et regardons la situation dans son ensemble ici.
Quelle est sa
IDatabase
responsabilité?Il a quelques opérations différentes:
En regardant cette liste, vous vous demandez peut-être: "Est-ce que cela ne viole pas le SRP?" Mais je ne pense pas. Toutes les opérations font partie d'un concept unique et cohérent: gérer une connexion avec état à la base de données (un système externe) . Il établit la connexion, garde une trace de l'état actuel de la connexion (par rapport aux opérations effectuées sur d'autres connexions notamment), il signale quand valider l'état actuel de la connexion, etc. En ce sens, il agit comme une API qui cache beaucoup de détails d'implémentation dont la plupart des appelants ne se soucient pas. Par exemple, utilise-t-il HTTP, sockets, pipes, TCP personnalisé, HTTPS? Le code d'appel ne se soucie pas; il veut juste envoyer des messages et obtenir des réponses. Ceci est un bon exemple d'encapsulation.
Sommes-nous sûrs? Ne pourrions-nous pas fractionner certaines de ces opérations? Peut-être, mais il n'y a aucun avantage. Si vous essayez de les diviser, vous aurez toujours besoin d'un objet central qui maintient la connexion ouverte et / ou gère l'état actuel. Toutes les autres opérations sont fortement couplées au même état, et si vous essayez de les séparer, elles finiront tout de même par déléguer à nouveau l'objet de connexion. Ces opérations sont naturellement et logiquement couplées à l'État, et il n'y a aucun moyen de les séparer. Le découplage est formidable lorsque nous pouvons le faire, mais dans ce cas, nous ne pouvons pas réellement. Du moins pas sans un protocole sans état très différent pour parler à la base de données, et cela rendrait en fait les problèmes très importants comme la conformité ACID beaucoup plus difficiles. De plus, dans le processus de découplage de ces opérations de la connexion, vous serez obligé d'exposer des détails sur le protocole dont les appelants ne se soucient pas, car vous aurez besoin d'un moyen d'envoyer une sorte de message "arbitraire". à la base de données.
Notez que le fait que nous ayons affaire à un protocole avec état exclut assez solidement votre dernière alternative (passer la chaîne de connexion comme paramètre).
Avons-nous vraiment besoin que la chaîne de connexion soit définie?
Oui. Vous ne pouvez pas ouvrir la connexion avant d'avoir une chaîne de connexion et vous ne pouvez rien faire avec le protocole tant que vous n'avez pas ouvert la connexion. Il est donc inutile d'avoir un objet de connexion sans un.
Comment résoudre le problème de la nécessité de la chaîne de connexion?
Le problème que nous essayons de résoudre est que nous voulons que l'objet soit à tout moment utilisable. Quel type d'entité est utilisé pour gérer l'état dans les langues OO? Des objets , pas des interfaces. Les interfaces n'ont pas d'état à gérer. Parce que le problème que vous essayez de résoudre est un problème de gestion d'état, une interface n'est pas vraiment appropriée ici. Une classe abstraite est beaucoup plus naturelle. Utilisez donc une classe abstraite avec un constructeur.
Vous pouvez également envisager d' ouvrir réellement la connexion pendant le constructeur, car la connexion est également inutile avant son ouverture. Cela nécessiterait une
protected Open
méthode abstraite car le processus d'ouverture d'une connexion peut être spécifique à la base de données. Ce serait également une bonne idée de rendre laConnectionString
propriété en lecture seule dans ce cas, car changer la chaîne de connexion une fois la connexion ouverte n'aurait aucun sens. (Honnêtement, je le ferais lire de toute façon. Si vous voulez une connexion avec une chaîne différente, créez un autre objet.)Avons-nous besoin d'une interface?
Une interface qui spécifie les messages disponibles que vous pouvez envoyer via la connexion et les types de réponses que vous pouvez obtenir pourrait être utile. Cela nous permettrait d'écrire du code qui exécute ces opérations mais n'est pas couplé à la logique d'ouverture d'une connexion. Mais c'est le point: la gestion de la connexion ne fait pas partie de l'interface de "Quels messages puis-je envoyer et quels messages puis-je retourner vers / depuis la base de données?", Donc la chaîne de connexion ne devrait même pas en faire partie. interface.
Si nous empruntons cette voie, notre code pourrait ressembler à ceci:
la source
Open
méthode devrait êtreprivate
et vous devriez exposer uneConnection
propriété protégée qui crée la connexion et se connecte. Ou exposez uneOpenConnection
méthode protégée .Je ne vois vraiment pas la raison d'avoir une interface ici. Votre classe de base de données est spécifique à SQL et vous donne vraiment juste un moyen pratique / sûr de vous assurer que vous n'interrogez pas sur une connexion qui n'est pas ouverte correctement. Si vous insistez sur une interface, voici comment je le ferais.
L'utilisation pourrait ressembler à ceci:
la source