Comment spécifier une précondition (LSP) dans une interface en C #?


11

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 -

  1. 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)
  2. 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?

2
Par "LSP" vous parlez du principe de substitution de Liskov? Le principe "si son charlatan ressemble à un canard mais a besoin de piles ce n'est pas un canard"? Parce que comme je le vois, c'est plus une violation du FAI et du SRP peut-être même de l'OCP mais pas vraiment du LSP.
Sebastien

2
Juste pour que vous le sachiez, tout ce concept de "ConnectionString doit être défini / initialisé avant qu'une des méthodes puisse être exécutée" est un exemple de couplage temporel blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling et devrait être évité si possible.
Richiban

Seemann est vraiment un grand fan de Abstract Factory.
Adrian Iftode

Réponses:


10
  1. 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'assistance IDatabaseContract, 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.

  2. Oui , le LSP traite à la fois des parties syntaxiques et sémantiques d'un contrat.


Je ne pensais pas que vous pouviez utiliser des contrats de code dans une interface. L'exemple que vous fournissez montre qu'ils sont utilisés dans les classes. Les classes sont conformes à une interface, mais l'interface elle-même ne contient aucune information sur le contrat de code (dommage, vraiment. Ce serait l'endroit idéal pour le mettre).
Robert Harvey

1
@RobertHarvey: oui, vous avez raison. Techniquement, vous avez besoin d'une seconde classe, bien sûr, mais une fois défini, le contrat fonctionne automatiquement pour chaque implémentation de l'interface.
Doc Brown

21

La connexion et l'interrogation sont deux préoccupations distinctes. En tant que tels, ils devraient avoir deux interfaces distinctes.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

interface IDatabase
{
    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
}

Cela garantit à la fois qu'il IDatabasesera connecté lorsqu'il est utilisé et rend le client indépendant de l'interface dont il n'a pas besoin.


Pourrait être plus explicite sur «c'est un modèle d'application des conditions préalables à travers les types»
Caleth

@Caleth: il ne s'agit pas d'un "schéma général d'application des conditions préalables". Il s'agit d'une solution pour cette exigence spécifique de s'assurer que la connexion se produit avant toute autre chose. D'autres conditions préalables nécessiteront des solutions différentes (comme celle que j'ai mentionnée dans ma réponse). Je voudrais ajouter pour cette exigence, je préférerais clairement la suggestion d'Euphoric à la mienne, car elle est beaucoup plus simple et ne nécessite aucun composant tiers supplémentaire.
Doc Brown

La demande spécifique que quelque chose se passe avant autre chose est largement applicable. Je pense également que votre réponse correspond mieux à cette question , mais cette réponse peut être améliorée
Caleth

1
Cette réponse manque complètement le point. L' IDatabaseinterface 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.
jpmc26

4
@ jpmc26 Aucune de vos objections n'a de sens, car l'état peut être maintenu dans la classe implémentant IDatabase. Il peut également référencer la classe parente qui l'a créée, obtenant ainsi l'accès à l'état de la base de données entière.
Euphoric

5

Prenons un peu de recul et regardons la situation dans son ensemble ici.

Quelle est sa IDatabaseresponsabilité?

Il a quelques opérations différentes:

  • Analyser une chaîne de connexion
  • Ouvrir une connexion avec une base de données (un système externe)
  • Envoyer des messages à la base de données; les messages commandent à la base de données de modifier son état
  • Recevez les réponses de la base de données et transformez-les en un format que l'appelant peut utiliser
  • Fermer la connexion

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 Openmé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 la ConnectionStringproprié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:

interface IDatabase {
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

abstract class ConnectionStringDatabase : IDatabase { 

    public string ConnectionString { get; }

    public Database(string connectionString) {
        this.ConnectionString = connectionString;
        this.Open();
    }

    protected abstract void Open();

    public abstract void ExecuteNoQuery(string sql);
    public abstract void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

Apprécierait si le votant down expliquerait sa raison d'être en désaccord.
jpmc26

D'accord, re: downvoter. C'est la bonne solution. La chaîne de connexion doit être fournie dans le constructeur à la classe concrète / abstraite. L'opération désordonnée d'ouverture / fermeture d'une connexion n'est pas une préoccupation du code utilisant cet objet, et devrait rester interne à la classe elle-même. Je dirais que la Openméthode devrait être privateet vous devriez exposer une Connectionpropriété protégée qui crée la connexion et se connecte. Ou exposez une OpenConnectionméthode protégée .
Greg Burghardt

Cette solution est assez élégante et très bien conçue. Mais je pense qu'une partie du raisonnement derrière les décisions de conception est erronée. Principalement dans les premiers paragraphes sur le PÉR. Il viole le SRP même comme expliqué dans "Quelle est la responsabilité d'IDatabase?". Les responsabilités telles que vues pour le SRP ne sont pas seulement des choses qu'une classe fait ou gère. Ce sont aussi des «acteurs» ou des «raisons de changer». Et je pense que cela viole le SRP parce que "recevoir les réponses de la base de données et les transformer en un format que l'appelant peut utiliser" a une raison très différente de changer que "analyser une chaîne de connexion".
Sebastien

Je note toujours cela.
Sebastien

1
Et BTW, SOLID ne sont pas l'évangile. Bien sûr, il est très important de les garder à l'esprit lors de la conception d'une solution. Mais vous POUVEZ les violer si vous savez POURQUOI vous le faites, COMMENT cela va affecter votre solution et COMMENT réparer les choses avec une refactorisation si cela vous cause des ennuis. Ainsi, je pense que même si la solution mentionnée ci-dessus viole le SRP, elle est la meilleure à ce jour.
Sebastien

0

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.

public interface IDatabase : IDisposable
{
    string ConnectionString { get; }
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

public class SqlDatabase : IDatabase
{
    public string ConnectionString { get; }
    SqlConnection sqlConnection;
    SqlTransaction sqlTransaction; // optional

    public SqlDatabase(string connectionStr)
    {
        if (String.IsNullOrEmpty(connectionStr)) throw new ArgumentException("connectionStr empty");
        ConnectionString = connectionStr;
        instantiateSqlProps();
    }

    private void instantiateSqlProps()
    {
        sqlConnection.Open();
        sqlTransaction = sqlConnection.BeginTransaction();
    }

    public void ExecuteNoQuery(string sql) { /*run query*/ }
    public void ExecuteNoQuery(string[] sql) { /*run query*/ }

    public void Dispose()
    {
        sqlTransaction.Commit();
        sqlConnection.Dispose();
    }

    public void Commit()
    {
        Dispose();
        instantiateSqlProps();
    }
}

L'utilisation pourrait ressembler à ceci:

using (IDatabase dbase = new SqlDatabase("Data Source = servername; Initial Catalog = MyDb; Integrated Security = True"))
{
    dbase.ExecuteNoQuery("delete from dbo.Invoices");
    dbase.ExecuteNoQuery("delete from dbo.Customers");
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.