How to get/set a property of an interface that is

2019-05-30 07:18发布

问题:

What is a good (object oriented) way of setting a property of a class which implements an interface, when that property doesn't always exist in all classes that implement that same interface?

e.g.

Let's say I have an interface

public interface IDataRepository {

    public DataStructure GetData();   // DataStructure is an arbitrary class, doesn't matter for this example

}

Now I also have two classes that inherit from this

public class DatabaseRepository : IDataRepository {

    public DataStructure GetData() 
    {
       // get data from database
    }
}

and

public class FileRepository : IDataRepository {

    public string WorkingFolder { get; set; }

    public DataStructure GetData() {
       // get data from files
    }
}

Now my client method doesn't necessarily know what the repository is but here's what I want to do...

private DataStructure ReadData(IDataRepository repository)
{
     repository.WorkingFolder = @"C:\Data"; // What is the best way of doing this?

     return repository.GetData();
}

obviously the above code won't work and I could do...

     if (repository is FileRepository) {
        ((FileRepository)repository).WorkingFolder = @"C:\Data";
     }

or add WorkingFolder as a property of the interface (and therefore all the classes that implement it) even though in most cases it's irrelevant.

but both of these (esp. the first one) seem very inelegant and not very object oriented. What is the oop way of doing this kind of thing?

Edit

The obvious question is if the method doesn't know what repository is, how can it know the correct value for WorkingFolder... But the above is an over-simplification of what I'm trying to do, so let's just say it can find out...

回答1:

Apparently your ReadData method can't actually accept any type of repository. It is only able to handle a FileRepository. That's what it expects, and that's what it needs to do its job. Given that, that's what it should actually accept as its parameter, rather than an interface that doesn't actually provide a contract that is sufficient for it to do its job.

The entire point of having an interface is so that anyone using that interface can use it without caring what the implementation is. So if you do want to use the interface you need to include enough information in the interface's definition such that it provides every operation that anyone using the interface needs, otherwise you're better off just not using it at all (at least for that specific operation).

As for the specific example given, you should probably just be providing an already configured repository, that has whatever values it needs in order to allow this method to do its work, as a parameter. It doesn't make sense for a method that's reading a value from an arbitrary repository to be configuring that repository at all. That is, if it really is reading something from an arbitrary repository.



回答2:

As others have said in the comments, you should initialise these properties in the constructor. This is where you know what type you're creating, so you also know what arguments its constructor requires / can set those there.

Once you've initialised the object, you can just pass it around / have anything using that class operate against its interface.

Example:

public void Main(string[] args)
{
    var myRepo = new FileRepository(args[0]); //Here's where we set the working directory
    var myThing = new Thing();
    var data = myThing.ReadData(myRepo);// of course, the current implementation means you could just call `myRepo.GetData()` directly, since ReadData just passes out the same response; but presumably that method adds some additional value..
    Console.WriteLine(data.ToString());
}

Supporting Code

public class DatabaseRepository : IDataRepository {
    DbConnection connection; //you may want a connection string or something else; going with this type just to illustrate that this constructor uses a different type to the FileRepo's
    public DatabaseRepository(DbConnection connection) 
    {
        this.connection = connection;
    }
    public DataStructure GetData() 
    {
       // get data from database
    }
}
public class FileRepository : IDataRepository {

    public string WorkingFolder { get; set; } //Do you need set?  Generally best to keep it constant after initialisation unless there's good reason to change it
    public FileRepository (string workingFolder)
    {
        this.WorkingFolder = workingFolder;
    }
    public DataStructure GetData() {
       // get data from files
    }
}

How do I call the code that creates the class

i.e. maybe you've implemented a really basic factory pattern like so, and want to know how to provide arguments:

public class DataRepositoryFactory
{
    Type baseType = typeof(IDataRepository);
    IDictionary<string, Type> typeMap = new Dictionary<string, Type>() { 
        {"File", typeof(FileRepository) } 
        ,{"Db", typeof(DatabaseRepository) } 
    }
    public void RegisterType(string typeName, Type type) 
    {
        if (!baseType.IsAssignableFrom(type)) throw new ArgumentException(nameof(type));
        typeMap.Add(typeName, type);    
    }

    public IDataRepository GetDataRepository(string typeName)
    {
        return (IDataRepository)Activator.CreateInstance(typeMap[typeName]);
    }
}

(For a more complex example of a factory, see https://web.archive.org/web/20140414013728/http://tranxcoder.wordpress.com/2008/07/11/a-generic-factory-in-c).

I.e. in this scenario, when you call the factory you know what type you want, but you're only giving it a string to name/identify that class. You could add a params object[] args to your GetDataRepository method, allowing you to call it like so:

var myRepo = myDataRepositoryFactory.GetDataRepository("File", "c:\somewhere\something.dat");

That's a good approach / is actually what's used on the linked example above. However, it means that your call to this code differs for different types; since if we use variables instead of hardcoded values as in the above example we can't simply do the below, since myRepoType could be set to "Db", whilst "myFilePath" would be a string:

var myRepo = myDataRepositoryFactory.GetDataRepository(myRepoType, myFilePath);

That's fixable by calling:

var myRepo = myDataRepositoryFactory.GetDataRepository(myRepoType, myArgs);

i.e. where myArgs is an object[], giving all of the values required in the desired order to initialise the type. The piece to populate object[] with the required values could then take place at the same point at which you decided you wanted the type to be a file repo vs database repo. However, this approach isn't that clean / casting to and from objects stops you from getting help from the compiler.


So how do I improve things?

There are a few options. One is to replace the need to use object[] by instead creating a type to hold your arguments. e.g.

public interface IDataRepositoryConfiguration
{
    //nothing required; this is just so we've got a common base class
}
public class FileRepositoryConfiguration: IDataRepositoryConfiguration
{
    public string WorkingFolder {get;set;}
}
public class FileRepository : IDataRepository {
    public FileRepository (IDataRepositoryConfiguration configuration)
    {
        var config = configuration as FileRepositoryConfiguration;
        if (config == null) throw new ArgumentException(nameof(configuration)); //improve by having different errors for null config vs config of unsupported type
        this.WorkingFolder = config.WorkingFolder;
    }
    //...
}

This still has some issues; i.e. we may pass a DatabaseRepositoryConfiguration as our IRepositoryConfiguration when creating a FileRepository, in which case we'd get the AgumentNullException at runtime; but this does avoid issues should parameters change order, and makes it less of a headache to code / debug.


Could it be further improved?

Dependency Injection offers one solution. This could be used along the lines of the code below (i.e. you create instances of each of your classes, providing the required arguments, and give each instance a name, so that you can later fetch that instantiation. Exactly what that code looks like would depend on the dependency injection library you used:

//setting up your repositories
var container = new Container();
container.Configure(config =>
{
    // Register stuff in container, using the StructureMap APIs...
    config.For<IDataRepository>().Add(new FileRepository("\\server\share\customers")).Named("customers");
    config.For<IDataRepository>().Add(new FileRepository("\\server\share\invoices")).Named("invoices");
    config.For<IDataRepository>().Add(new DatabaseRepository(new DbConnection(configurationString))).Named("persist");
    config.For<IDataRepository>().Use("persist"); // Optionally set a default
    config.Populate(services);
});


//then later when you need to use it...
public DataStructure ImportCustomers(IContainer container)
{
    var customerRepo = container.GetInstance<IDataRepository>("customers");
    return customerRepo.GetData();
}

I'm sure there are many other approaches, and exactly what approach to use depends on how your program will operate. Hopefully the above is enough to get you past your current problem; but if you find you're still struggling please post a new question with more detail / saying where you're still having issues having considered these points.



回答3:

If possible, I'd just put the value for that property in the constructor or create a subinterface, like others suggested.

If it's not possible, C# 7.X (don't remember the exact minor version) has a nice code structure for conditional casting:

IDataRepository repo = new FileRepository();
if (repo is FileRepository fileRepo)
{
    fileRepo.WorkingFolder = "some dir";
}

However in your case, you should probably rethink your architecture and always pass (or even better always create) a repository object which is ready to be used.



回答4:

a) Put it into the Inferface definitions. Deal with any "NotImplemented" Exceptions. You always have to expect those with Interfaces anyway.

For example, IEnumerable has a Reset() function. But in most cases it is not implemented. It is not even supposed to be implemented in most cases. Afaik it is only there for Backwards Compatabilty with some old COM stuff.

b) make a sub-interface just for the property

c) Verify the Interface is properly implemented via is checks (throw exceptions thows if nessesary, like Array.Sort will throw a InvalidOperation one), generic constraints, proper argument types and the like.



标签: c# oop