Eviter d’effectuer des “casts” avec Bridge et Visiteur

Lorsqu’on intervient sur du code existant, la plupart du temps, on manipule des objets sous leur forme générique, par l’intermédiaire de classes abstraites ou d’interfaces. Dans la majorité des cas, les membres exposés par la classe abstraite ou par l’interface suffisent à utiliser l’objet en faisant appel à des fonctions ou des données membres. Toutefois, dans certains cas on peut avoir la nécessité d’accéder à un membre plus spécifique qui n’est pas exposé.

Une solution directe consisterait à caster l’objet vers le type spécifique de façon à utiliser le membre spécifique. Cette solution répond au problème mais elle amène la classe consommatrice à avoir une connaissance du type précis de l’objet alors que l’objet est exposé sous une forme générique. Dans la plupart des cas, cette solution est peu satisfaisante.

Cet article tente d’apporter une solution à ce problème en exposant une méthode générale. Il existe certainement de nombreuses autres méthodes. A défaut de convenir à toutes les situations, la méthode exposée tente de montrer qu’il est possible de résoudre ce problème sans passer par un cast explicite.

Le code source présenté dans cet article se trouve sur GitHub

Mise en situation

Pour illustrer la problématique, imaginons dans un premier temps, une application dont le but est de récupérer des informations à partir d’une page web. Pour configurer cette application, on injecte un objet de configuration générique qui expose quelques membres permettant d’accéder à la page web. La classe effectuant la récupération des informations en utilisant la configuration sera appelée la classe cliente.

L’interface exposant les éléments de configuration peut être définie de cette façon:

L’implémentation de IConnectionConfiguration est:

public interface IConnectionConfiguration
{
    string Name { get; }
    string Address { get; }
    string Port { get; }
}

ConnectionConfiguration est un objet satisfaisant IConnectionConfiguration:

La définition de IConnectionConfiguration est:

public class ConnectionConfiguration : IConnectionConfiguration 
{ 
    public string Name { get; }
    public string Address { get; }
    public string Port { get; }
}

L’objet IConnectionConfiguration est utilisé dans toute l’application et suffit dans la plupart des cas d’utilisation.

On veut étendre les fonctionnalités de l’application et permettre qu’une instance de cette application fonctionne en récupérant des informations à partir d’une base de données. L’objet IConnectionConfiguration devient insuffisant car il n’expose pas d’éléments relatifs à la base de données comme:

  • Le nom d’une base de données
  • L’identifiant de connexion,
  • Un mot de passe

On crée alors la nouvelle interface IDatabaseConfiguration pour exposer des éléments de configuration plus spécifiques à la base de données:

La definition de IDatabaseConfiguration est:

public interface IDatabaseConfiguration : IConnectionConfiguration
{
    string DatabaseName { get; }
    string UserId { get; }
    string Password { get; }
}

La classe DatabaseConfiguration satisfait IDatabaseConfiguration:

L’implémentation de DatabaseConfiguration est:

public class DatabaseConfiguration : ConnectionConfiguration, IDatabaseConfiguration 
{ 
    public string DatabaseName { get; set; }
    public string UserId { get; set; }
    public string Password { get; set; }
}

De la même façon, l’objet de type IDatabaseConfiguration est injecté dans l’application.

On étend davantage les fonctionnalités de l’application et on cherche à permettre à une instance d’effectuer des connexions vers un service REST. D’autres éléments de configuration sont nécessaires:

  • Méthode HTTP utilisée: POST, GET, PUT ou DELETE
  • Format utilisé: JSON ou XML.

L’interface IRestServiceConfiguration permet d’exposer des éléments de configuration spécifiques au service REST:

La définition de IRestServiceConfiguration est:

public interface IRestServiceConfiguration : IConnectionConfiguration
{
    string RestMethod { get; }
    string RestFormat { get; }
}

On définit ensuite la classe RestServiceConfiguration satisfaisant IRestServiceConfiguration:

L’implémentation de RestServiceConfiguration est:

public class RestServiceConfiguration: ConnectionConfiguration, IRestServiceConfiguration 
{ 
    public string RestMethod { get; set; }
    public string RestFormat { get; set; }
} 

L’objet de configuration visible dans toute l’application est IConnectionConfiguration. Cependant:

  • Pour l’instance de la classe cliente s’interfaçant avec une base de données, on veut que le module de connexion à la base puisse accéder aux membres de IDatabaseConnectionConfiguration.
  • Pour l’instance s’interfaçant avec un service REST, on veut que le module de connexion au service REST accède aux membres de IRestServiceConfiguration.

On peut définir l’interface IClient commune correspondant aux objets qui doivent récupérer des données auprès de la base de données et du service REST de cette façon:

L’implémentation de IClient est:

public interface IClient 
{ 
    IData FetchData(IConfigurationHandler configurationHandler); 
}

La définition de IData n’a pas d’importance:

public interface IData {}

On définit la classe FetchedData satisfaisant IData:

public class FetchedData : IData {}

L’implémentation de la classe consommant la configuration qui satisfait IClient et se connecte à la base de données pourrait être:

public class DatabaseClient: IClient 
{ 
    public IData FetchData(IConnectionConfiguration configuration) 
    { 
         // On souhaite obtenir les informations de connexion spécifiques à la 
         // base de données dans l'objet 'configuration' 
         
         ...
         // Le reste de l'implémentation n'a pas d'importance
         // il consiste à se connecter à la base de données et à récupérer 
         // des données
         return new FetchedData();
    } 
}

De la même façon, la classe s’interfaçant avec le service REST satisfaisant IClient peut s’implémenter de cette façon:

public class RestServiceClient: IClient 
{ 
    public IData FetchData(IConnectionConfiguration configuration) 
    { 
         // On souhaite obtenir les informations de connexion spécifiques au 
         // service REST dans l'objet 'configuration' 

         ...

         // Le reste de l'implémentation n'a pas d'importance
         // il consiste à se connecter à la base de données et à récupérer 
         // des données
         return new FetchedData();
    } 
}

Maintenant que les bases du problèmes sont posées, essayons de trouver une solution pour récupérer les éléments de configuration dans les fonctions FetchData() des classes clientes DatabaseClient et RestServiceClient:

1ère solution: effectuer un “cast”

Comme indiqué plus haut, la solution la plus directe est d’effectuer un cast et de pouvoir accéder directement aux membres plus spécifiques.

Par exemple pour la classe s’interfaçant avec la base de données:

public class DatabaseClient: IClient 
{ 
    public IData FetchData(IConnectionConfiguration configuration) 
    { 
        // On effectue le "cast"
        IDatabaseConfiguration databaseConfiguration = 
            configuration as IDatabaseConfiguration; 

        // On accède aux éléments de configuration communs:
        string name = databaseConfiguration.Name;
        string address = databaseConfiguration.Address;
        string port = databaseConfiguration.Port;

        // On accède aux éléments de configuration spécifiques à la base de données: 
        string databaseName = databaseConfiguration.DatabaseName; 
        string userId = databaseConfiguration.UserId; 
        string password = databaseConfiguration.Password;

         // Le reste de l'implémentation n'a pas d'importance
         // il consiste à se connecter à la base de données et à récupérer 
         // des données
         return new FetchedData();
    } 
}

De la même façon pour la classe s’interfaçant avec le service REST:

public class RestServiceClient: IClient 
{ 
    public IData FetchData(IConnectionConfiguration configuration) 
    { 
        // On effectue le "cast"
        IRestServiceConfiguration restServiceConfiguration = 
            configuration as IRestServiceConfiguration;

        // On accède aux éléments de configuration communs:
        string name = restServiceConfiguration.Name;
        string address = restServiceConfiguration.Address;
        string port = restServiceConfiguration.Port;

        // On accède aux membres spécifiques au service REST: 
        string restMethod = restServiceConfiguration.RestMethod; 
        string format = restServiceConfiguration.Format; 
     
        // Le reste de l'implémentation n'a pas d'importance
        // il consiste à se connecter à la base de données et à récupérer 
        // des données
        return new FetchedData();
    } 
}

Le cast est une solution rapide mais elle pose de nombreux problèmes:

  • Les classes clientes doivent connaître le type précis de l’objet vers lequel le cast doit être effectué. Dans le cas de DatabaseClient c’est IDatabaseConnectionConfiguration et dans le cas de RestServiceClient c’est IRestServiceConfiguration.
  • Le type de destination du cast est écrit en dur dans chaque classe cliente. En cas de modification du type des objets IDatabaseConnectionConfiguration et IRestServiceConfiguration, il faudra modifier l’implémentation du cast, ce qui est contraire au principe ouvert/fermé de SOLID.
  • Si le type IDatabaseConnectionConfiguration ne dérive plus IConnectionConfiguration, la compilation ne va pas échouer. C’est seulement à l’exécution qu’on se rendra compte que le cast n’est plus possible ce qui peut introduire des risques de régressions.
  • Enfin si d’autres classes consomment les objets IDatabaseConnectionConfiguration ou IRestServiceConfiguration de la même façon, elles devront aussi effectuer un cast pour les atteindre à partir de IConnectionConfiguration. On dupliquera alors dans toutes les autres classes le cast ce qui est contraire au principe DRY.

Pour toutes ces raisons, on se propose de chercher une autre solution plus satisfaisante.

2e solution: encapsuler le “cast”

Pour éviter les problèmes de duplication du code correspondant au cast, on peut encapsuler le code effectuant le cast dans une classe qui aura pour but de consommer la configuration. Le pattern Bridge semble être adapté pour effectuer cette modification.

Le design pattern Bridge

Bridge est un pattern permettant de découpler l’interface d’une classe de son implémentation. Ainsi si l’implémentation change, l’interface appelée ne changera pas.

Le pattern Bridge ajoute un objet intermédiaire entre l’objet appelant et l’objet appelé pour découpler l’interface de l’objet appelé de son implémentation.

Ainsi si on considère les objets suivants:

Diagramme théorique de Bridge

Ainsi:

  • IGenericCalledObject: est l’interface générique appelée par la classe cliente. La classe cliente ne voit que cette interface.
  • RefinedCalledObject: il s’agit de l’objet qui va appeler concrètement l’objet à découpler. Cette objet n’est pas visible de la classe cliente. C’est lui qui a la connaissance de l’objet qui sera appelé.
  • IAbstractImplementation: c’est l’interface de l’objet concret possédant l’implémentation à appeler. IAbstractImplementation est visible de l’objet RefinedCalledObject.
  • ConcreteImplementation: c’est l’objet possédant l’implémentation à appeler. Il n’est, en principe, visible par aucun des autres objets de façon à ce qu’il soit facilement interchangeable.

Le pattern Bridge s’utilise dans le cas où l’implémentation de l’objet ConcreteImplementation change fréquemment et qu’il soit nécessaire de changer la classe ConcreteImplementation. Dans notre cas, on utilise ce pattern pour découpler l’implémentation concrète de l’interface consommée par la classe appelante.

Ainsi, dans le cas de notre exemple de départ, on introduit un nouvel objet qui va correspondre à l’interface générique IGenericCalledObject. L’objet est ConfigurationHandler satisfaisant IConfigurationHandler:

Exemple d’utilisation de Bridge

IConfigurationHandler se définit de cette façon:

public interface IConfigurationHandler
{
    IDatabaseConfiguration DatabaseConfiguration { get; }
    IRestServiceConfiguration RestServiceConfiguration { get; }
}

On définit ensuite l’objet correspondant à RefinedCalledObject. C’est l’objet qui va appeler les membres se trouvant dans les objets de configuration, il doit satisfaire IConfigurationHandler:

public class ConfigurationHandler : IConfigurationHandler
{ 
    public ConfigurationHandler(
        IConnectionConfiguration databaseConfiguration, 
        IConnectionConfiguration restServiceConfiguration) 
    { 
        this.DatabaseConfiguration = databaseConfiguration as IDatabaseConfiguration; 
        this.RestServiceConfiguration = restServiceConfiguration as 
           IRestServiceConfiguration; 
    }

    public IDatabaseConfiguration DatabaseConfiguration { get; private set; }
    public IRestServiceConfiguration RestServiceConfiguration { get; private set; }
}

Par suite, les interfaces IDatabaseConfiguration et IRestServiceConfiguration correspondent à l’interface IAbstractImplementation du pattern Bridge défini plus haut.
De même les classes DatabaseConnectionConfiguration et RestServiceConfiguration correspondent à la classe ConcreteImplementation du pattern Bridge.

Du point de vue des classes clientes

Si on cherche à obtenir la configuration à partir des classes clientes, en reprenant le code de ces classes, on utilise IConfigurationHandler pour récupérer les éléments de configuration spécifiques:

public class DatabaseClient: IClient 
{ 
    public IData FetchData(IConfigurationHandler configurationHandler) 
    { 
        // On accède aux éléments de configuration communs:
        string name = configurationHandler.DatabaseConfiguration.Name;
        string address = configurationHandler.DatabaseConfiguration.Address;
        string port = configurationHandler.DatabaseConfiguration.Port;

        // On accède aux éléments de configuration spécifiques à la base de données: 
        string databaseName = configurationHandler.DatabaseConfiguration.DatabaseName; 
        string userId = configurationHandler.DatabaseConfiguration.UserId; 
        string password = configurationHandler.DatabaseConfiguration.Password;

         // Le reste de l'implémentation n'a pas d'importance
         // il consiste à se connecter à la base de données et à récupérer 
         // des données
         return new FetchedData();
    } 
}

Et:

public class RestServiceClient: IClient 
{ 
    public IData FetchData(IConfigurationHandler configurationHandler) 
    { 
        // On accède aux éléments de configuration communs:
        string name = configurationHandler.RestServiceConfiguration.Name;
        string address = configurationHandler.RestServiceConfiguration.Address;
        string port = configurationHandler.RestServiceConfiguration.Port;

        // On accède aux membres spécifiques au service REST: 
        string restMethod = configurationHandler.RestServiceConfiguration.RestMethod; 
        string format = configurationHandler.RestServiceConfiguration.Format; 
     
        // Le reste de l'implémentation n'a pas d'importance
        // il consiste à se connecter à la base de données et à récupérer 
        // des données
        return new FetchedData();
    } 
}

On remarque que le cast n’est plus effectué dans les classes clientes mais dans ConfigurationHandler. Les classes clientes ne voient que l’interface IConfigurationHandler. Cette implémentation permet de résoudre quelques inconvénients de la 1ère solution:

  • Il n’y pas plus de duplication de code correspondant au cast. Les casts sont effectués seulement dans la classe ConfigurationHandler.
  • Les classes clientes ne sont plus obligées de connaître le type précis vers lequel le cast doit être effectué. Elles n’ont plus la connaissance de ce type. Cette connaissance est contenue dans une seule classe: ConfigurationHandler.

Tous les problèmes ne sont pas résolus pour autant car si une modification amène à ne plus faire dériver le type IDatabaseConfiguration de IConnectionConfiguration, la compilation ne va pas échouer. De la même façon que pour la 1ère solution, c’est à l’exécution qu’on se rendra compte que le cast n’est pas possible.

Ce dernier problème amène à devoir trouver une solution pour éliminer le cast.

3e solution: éliminer le “cast” en utilisant Visiteur

On cherche à éliminer le cast en appliquant un comportement particulier aux classes clientes en fonction du type de configuration qu’elles veulent consommer. On veut ensuite garantir que la configuration consommée appliquera tous les éléments de configuration qui lui sont spécifiques.

Par exemple, dans le cas de la classe cliente DatabaseClient, on veut que le comportement soit spécifique à la configuration de la base de données. On souhaite ensuite que tous les paramètres gérés par IDatabaseConfiguration soient appliqués quand cette objet est consommé.

Le pattern Visiteur paraît adapté pour permettre d’appliquer ce comportement. Ainsi:

  • Les classes visitées sont les classes clientes car ce sont elles qui consomment les éléments de configuration. En effet ces classes vont consommer chaque élément de configuration de façon spécifique.
  • Les classes visiteur correspondent aux classes détenant la configuration car ce sont elles qui savent quels sont les éléments de configuration à paramétrer.

Le diagramme correspondant à Visiteur est:

Diagramme théorique de Visiteur

Dans notre cas:

  • DatabaseClient et RestServiceClient sont les classes visitées et
  • IDatabaseConfiguration et IRestServiceConfiguration sont les objets visiteur.

On définit les interfaces IDatabaseConfigurationConsumer et IRestServiceConfigurationConsumer qui vont servir de base pour implémenter le pattern Visiteur. On perfectionne l’interface IConnectionConfiguration en rajoutant les méthodes:

  • VisitDatabaseConfigurationConsumer(IDatabaseConfigurationConsumer configurationConsumer): permettant d’obtenir la configuration pour la base de données
  • VisitRestServiceConfigurationConsumer(IRestServiceConfigurationConsumer configurationConsumer): pour obtenir la configuration pour le service REST.

Application de Visiteur

L’interface IDatabaseConfigurationConsumer qui désigne la classe visitée qui doit consommer la configuration correspondant à la base de données se définit de cette façon:

public interface IDatabaseConfigurationConsumer
{ 
    void AcceptConfiguration(IDatabaseConfiguration configuration); 
}

De même, on définit l’interface IRestServiceConfigurationConsumer qui désigne la classe visitée qui doit consommer la configuration correspondant au service REST:

public interface IRestServiceConfigurationConsumer
{ 
    void AcceptConfiguration(IRestServiceConfiguration configuration); 
}

On définit ensuite les interfaces pour les visiteurs c’est-à-dire IConnectionConfiguration:

public interface IConnectionConfiguration 
{ 
    string Name { get; }
    string Address { get; }
    string Port { get; }

    void VisitDatabaseConfigurationConsumer(
        IDatabaseConfigurationConsumer configurationConsumer); 
    void VisitRestServiceConfigurationConsumer(
        IRestServiceConfigurationConsumer configurationConsumer); 
}

Ensuite on modifie ConnectionConfiguration pour que cette classe devienne un visiteur. Elle doit donc satisfaire les nouvelles méthodes de IConnectionConfiguration:

public class ConnectionConfiguration : IConnectionConfiguration 
{ 
    public string Name { get; }
    public string Address { get; }
    public string Port { get; }
    
    public virtual void VisitDatabaseConfigurationConsumer(
        IDatabaseConfigurationConsumer configurationConsumer) 
    {

    } 

    public virtual void VisitRestServiceConfigurationConsumer(
        IRestServiceConfigurationConsumer configurationConsumer) 
    {

    } 
}

L’implémentation de DatabaseConfiguration doit être adaptée pour devenir un visiteur:

public class DatabaseConfiguration : ConnectionConfiguration, IDatabaseConfiguration 
{ 
    public string DatabaseName { get; set; }
    public string UserId { get; set; }
    public string Password { get; set; }
    
    public override void VisitDatabaseConfigurationConsumer(
        IDatabaseConfigurationConsumer configurationConsumer) 
    { 
        configurationConsumer.AcceptConfiguration(this); 
    } 
}

De même, l’implémentation de RestServiceConfiguration devient:

public class RestServiceConfiguration: ConnectionConfiguration, IRestServiceConfiguration 
{ 
    public string RestMethod { get; set; }
    public string RestFormat { get; set; }
    
    public override void VisitRestServiceConfigurationConsumer(
        IRestServiceConfigurationConsumer configurationConsumer) 
    { 
        configurationConsumer.AcceptConfiguration(this); 
    } 
}

On doit aussi modifier IConfigurationHandler pour permettre de récupérer les éléments de configuration spécifiques:

public interface IConfigurationHandler
{ 
    void GetDatabaseConfiguration(IDatabaseConfigurationConsumer 
        configurationConsumer);
    void GetRestServiceConfiguration(IRestServiceConfigurationConsumer 
        configurationConsumer);
}

L’implémentation de ConfigurationHandler devient:

public class ConfigurationHandler : IConfigurationHandler
{ 
    private IConnectionConfiguration databaseConfiguration; 
    private IConnectionConfiguration restServiceConfiguration; 
    public ConfigurationHandler(
        IConnectionConfiguration databaseConfiguration, 
        IConnectionConfiguration restServiceConfiguration) 
    {
        // Il n'y a plus de "cast"
        this.databaseConfiguration = databaseConfiguration; 
        this.restServiceConfiguration = restServiceConfiguration; 
    }

    public void GetDatabaseConfiguration(
        IDatabaseConfigurationConsumer configurationConsumer) 
    { 
        this.databaseConfiguration.VisitDatabaseConfigurationConsumer(
            configurationConsumer); 
    } 

    public void GetRestServiceConfiguration(
        IRestServiceConfigurationConsumer configurationConsumer) 
    { 
        this.restServiceConfiguration.VisitRestServiceConfigurationConsumer(
            configurationConsumer); 
    } 
}

Au niveau des classes clientes, DatabaseClient (respectivement RestServiceClient) doit satisfaire IDatabaseConfigurationConsumer (resp. IRestServiceConfigurationConsumer) pour devenir une classe visitée.
Pour DatabaseClient, on rajoute l’interface IDatabaseConfigurationConsumer et on effectue l’implémentation correspondante:

public class DatabaseClient: IClient, IDatabaseConfigurationConsumer
{ 
    private string name;
    private string address;
    private string port;

    private string databaseName;
    private string userId; 
    private string password;

    public void AcceptConfiguration(IDatabaseConfiguration configuration) 
    {
        // On affecte les éléments de configuration communs
        this.name = configuration.Name;
        this.address = configuration.Address;
        this.port = configuration.Port;

        // On affecte les éléments de configuration spécifiques à la base de données
        this.databaseName = configuration.DatabaseName; 
        this.userId = configuration.UserId; 
        this.password = configuration.Password; 
    }

    public IData FetchData(IConfigurationHandler configurationHandler) 
    { 
        // On récupère la configuration 
        configurationHandler.GetDatabaseConfiguration(this); 
        
         // Le reste de l'implémentation n'a pas d'importance
         // il consiste à se connecter à la base de données et à récupérer 
         // des données
         return new FetchedData();
    } 
}

De même pour RestServiceClient, on rajoute l’interface IRestServiceConfigurationConsumer et on effectue l’implémentation correspondante:

public class RestServiceClient: IClient, IRestServiceConfigurationConsumer
{ 
    private string name;
    private string address;
    private string port;

    private string restMethod; 
    private string restFormat;

    public void AcceptConfiguration(IRestServiceConfiguration configuration) 
    { 
        // On affecte les éléments de configuration communs
        this.name = configuration.Name;
        this.address = configuration.Address;
        this.port = configuration.Port;

        // On affecte les éléments de configuration spécifiques au service REST
        this.restMethod = configuration.RestMethod; 
        this.restFormat = configuration.RestFormat; 
    }

    public IData FetchData(IConfigurationHandler configurationHandler) 
    {
        // On récupère la configuration 
        configurationHandler.GetRestServiceConfiguration(this);  
        
        // Le reste de l'implémentation n'a pas d'importance
        // il consiste à se connecter à la base de données et à récupérer 
        // des données
        return new FetchedData();
    } 
}

La récupération de la configuration par DatabaseClient et de RestServiceClient peut se résumer de cette façon:

Appel à IConfigurationHandler à partir de DatabaseClient et RestServiceClient

Le code suivante permet d’exécuter cet exemple:

static void Main(string[] args)
{
    IConnectionConfiguration databaseConfiguration = new DatabaseConfiguration{ 
        Name = "Config1",
        Address = "server1",
        Port = "1433",
        DatabaseName = "MyDataBase",
        UserId = "User1",
        Password = "MyPassword"
    };

    IConnectionConfiguration restServiceConfiguration = new RestServiceConfiguration{
        Name = "Config2",
        Address = "server2.com/api/v1",
        Port = "9001",
        RestMethod = "GET",
        RestFormat = "json"
    };

    IConfigurationHandler configurationHandler = new ConfigurationHandler(
        databaseConfiguration, restServiceConfiguration);

    DatabaseClient databaseClient = new DatabaseClient();
    databaseClient.FetchData(configurationHandler);
    
    RestServiceClient restServiceClient = new RestServiceClient();
    restServiceClient.FetchData(configurationHandler);
}

On remarque que:

  • Aucune classe n’effectue de casts.
  • Les classes clientes manipulent directement un objet générique qui est IConfigurationHandler. Toutefois elles consomment la configuration plus spécifique par l’intermédiaire de la fonction AcceptConfiguration(). Elles sont donc libres de s’adapter et de récupérer les éléments de configuration qui les intéressent.

Pour conclure…

Outre l’absence de cast, le grand intérêt de la 3e solution est que si une modification amène à faire une classe de configuration satisfaire une autre interface que celle attendue par une classe cliente, la compilation va échouer. De cette façon, on voit directement les adaptations qui sont nécessaires dans toutes les classes clientes.

Par exemple, si on décide que DatabaseConfiguration ne doit plus satisfaire IDatabaseConfiguration mais une autre interface IAzureConfiguration qui se définit de cette façon:

public interface IAzureConfiguration
{
    string Provider { get; }
    string UserId { get; }
    string Password { get; }
    string InitialCatalog { get; }
    string DataSource { get; }
}

Et dorénavant:

public class DatabaseConfiguration : ConnectionConfiguration, IAzureConfiguration 
{ 
    public string DatabaseName { get; set; }
    public string Provider { get; set; }

    public string UserId { get; set; }
    public string Password { get; set; }
    public string InitialCatalog { get; set; }
    public string DataSource { get; set; }
    
    public override void VisitDatabaseConfigurationConsumer(
        IDatabaseConfigurationConsumer configurationConsumer) 
    { 
        configurationConsumer.AcceptConfiguration(this); 
    } 
}

La compilation échouera car DatabaseConfiguration ne satisfait plus IDatabaseConfiguration.

Le code source présenté dans cet article se trouve sur GitHub.

Références

Leave a Reply