How to ensure that implementations of an interface

2019-09-07 07:40发布

问题:

Imagine you've an interface like this:

public interface IPersonManager
{
     public void AddPerson(string name);
}

...and an implementation which we'll going to call DefaultPersonManager. Let's say we want to be sure that any implementation of IPersonManager won't be able to give a null or empty string as argument of AddPerson(string name). For that matter, we're going to implement a contract class as follows:

[ContractClassFor(typeof(IPersonManager))]
public abstract class IPersonManagerContract : IPersonManager
{
   public void AddPerson(string name)
   {
       Contract.Requires(!string.IsNullOrEmpty(name), "Person's name cannot be a null or empty string");
   }
}

...and we'll decorate our IPersonManager interface with the ContractClassAttribute attribute:

[ContractClass(typeof(IPersonManagerContractClass))]
public interface IPersonManager
{
     public void AddPerson(string name);
}

We talked about a DefaultPersonManager. It would look like this class:

public class DefaultPersonManager
{
    private readonly List<string> _personNames = new List<string>();

    public void AddPerson(string name)
    {   
        // "name" argument will be verified by contract class!
        _personNames.Add(name);
    }
}

Alright!

Now we need to implement a new IPersonManager implementation which differs from the DefaultPersonManager in that AddPerson should persist person names to a SQL database (i.e. SQL Server, it's just an example...). We'll call this implementation DbBackedPersonManager.

Since DbBackedPersonManager requires a connection string, we could add a pre-condition in the AddPerson method implementation of DbBackedPersonManager:

public void AddPerson(string name)
{
     Contract.Requires(ConfigurationManager.ConnectionStrings["SomeConnectionStringId"] != null, "A connection string is required in your application/web configuration file");
}

Wrong: code contracts compiler will say that AddPerson implements an interface member thus we can't add a Requires (Read this Q&A I found that was answered by Jon Skeet and it's someway related to this topic a long time ago.).

How would be able to ensure that a specific implementation mandatorily requires a connection string to work nicely?

回答1:

Add the connection string requirement to the constructor of your concrete implementation, i.e.

public class DbBackedPersonManager : IPersonManager
{
    private readonly string _connectionString;

    public DbBackPersonManager()
    {
        Contract.Requires(ConfigurationManager.ConnectionStrings["SomeConnectionStringId"] != null, "A connection string is required in your application/web configuration file");

        _connectionString = ConfigurationManager.ConnectionStrings["SomeConnectionStringId"];
    }

    [ContractInvariantMethod]
    private void ObjectInvariant()
    {
        Contract.Invariant(_connectionString != null);
    }

    // Interface implementation snipped...
}

Then you are only able to instantiate and subsequently use an instance of DbBackedPersonManager if the connection string exists.

Personally I would just take a string connectionString parameter and let the instantiator provide the value (they are going to have to read ConfigurationManager.ConnectionStrings anyway).



回答2:

Possibly an approach would be creating an unrelated interface called IWithSqlDbBackend (or any identifier of your preference...) like this:

public interface IWithSqlDbBackend
{
     string ConnectionString { get; }
     string ConnectionStringId { get; set; }
}

Later, we'll need to create a contract class like this:

[ContractClassFor(typeof(IWithSqlDbBackend))]
public abstract class IWithSqlDbBackendContract : IWithSqlDbBackend
{
    public string ConnectionString
    {
        get 
        {  
            Contract.Requires(!string.IsNullOrEmpty(ConnectionStringId), "Connection string id cannot be null or empty");
            Contract.Requires(ConfigurationManager.ConnectionStrings[ConnectionStringId] != null, "Connection string must be configured");
            Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()), "A connection string cannot be null");

            return null;
        }
    }

    public string ConnectionStringId
    {
        get
        {
            Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()), "A connection string identifier cannot be null or empty");

            return null;
        }
    }
}

...also we'll need to decorate IWithSqlDbBackend interface with the so-called ContractClassAttribute:

[ContractClass(typeof(IWithSqlDbBackendContract))]
public interface IWithSqlDbBackend
{
    ...
}

...and implement the interface in DbBackedPersonManager. I'll add here the implementation signature:

public class DbBackedPersonManager : IPersonManager, IWithSqlDbBackend

Finally, if we create an instance of DbBackedPersonManagerand we try to call AddPerson method implementation but no connection string was previously configured in the application/web configuration file (i.e. web.config or app.config...), our pre-conditions will ensure that our application, service or library isn't satisfying the contract to work with persons stored in a database backend!

Side note

This is just a sample of how a lot of other domains would be able to ensure a bunch of conditions that, due to code contracts contract classes limitations, would be impossible to verify using regular polymorphism and code contracts.