Structuring projects in a solution for interfaces

  softwareengineering

I had this idea that I would achieve some good automation and separation of concerns as follows:

  1. Define an interface, IDataProvider, in a class in a DataMuncher project that needs to both consume and output data.
  2. In separate projects, have different implementations of that interface, e.g., in LunarData.csproj would be LunarDataProvider : IDataProvider, and in SolarData.csproj would be SolarDataProvider : IDataProvider.
  3. In DataMuncher, reflect over the assembly and linked assemblies and find all classes implementing IDataProvider.
  4. Create instances of the IDataProvider-implementing classes that were found, ask them what data they can provide, and if their data matches up with what was similarly learned from IDataConsumer classes, request the appropriate data, and send it onward to the consumers.

The idea is that if I need to add a new data provider, I can do so with only the barest touch on any other projects. I just create a new project, add a class that implements IDataProvider, and whether I’m manufacturing the data (as might occur in a unit test), reading from a text file, connecting to a database, or calling a web API, or any other means of getting the data, as long as I return it in the form required by the interface, it can be consumed. Thus, the quirks of the data are not exposed to the rest of the system and no proper nouns are propagated to other parts when they shouldn’t be.

However, I’ve run into a wee bit of a problem where the provider classes need to depend on the DataMuncher, but the DataMuncher needs to depend on the provider classes (in order to do what ultimately amounts to self dependency-injection).

So for this scenario to work I need at least 3 projects: one where the interface is defined, one where it’s implemented (dependent), and one that is dependent on both of them that does what amounts to dependency injection, call this the DataHub project. But that seems like a lot of complexity.

One suggestion was made to just put the interface definition and the data source implementations all in the same project. This would enable the implementing project to have no outside dependencies, and would avoid the need for another coordinating project to which many projects must refer. It also reduces the number of projects in the solution, which is also a possible benefit. (E.g., DataProviders.cs would have all three: IDataProvider plus LunarDataProvider and SolarDataProvider.

What would you do in this scenario? I saw value in having one project per implementation, but I have to admit that I don’t have a plethora of experience with various project structures that would have given me an idea of what works best. I also saw value in the division between projects forcing the separation of implementation internals from other projects, so that no one implementation would ever, even by mistake, use code or classes from another implementation. With one project each, a review of the project reference dependency graph could easily establish that the separate implementations are properly isolated from each other. This makes sure the code is truly modular and properly encapsulated.

Another benefit of separate projects is that if the data source doesn’t need to access, say, a database, there are no references to database libraries. Perhaps it’s file-based, and doesn’t need any dependencies. Then, the reference graph can be examined for correctness and anything strange will stick out much better: rather than “hmmm, I guess one of the 10 implementations uses that library”, “why is X using Y!?!”

I also considered, still with three projects, defining the interface in the DataHub, but this would prevent it, due to circular reference problems, from doing the reflection to find data sources—the recipient of the benefit of the interface, DataMuncher, would have to perform that task, and instead of LunarData having a reference to DataMuncher, it would be the other way around. This is now more references, and the DataHub class now also has to have a lot of projects referencing it (again, instead of the other way around). All these extra references suggest that perhaps the single project with interface and implementations is better.

Some guidance or ideas would be appreciated.

Hmmmm … it just occurred to me, 20 minutes after posting, that perhaps I should simply use a dependency injection library. That would use a pattern that is well established, and would remove some of the questions by reducing the number of reasonable ways to structure things. Still thinking on this …

For Reference

First we go fetch all the types available in all referenced assemblies that implement IDataProvider:

var types = GetType()
   .Assembly
   .GetReferencedAssemblies()
   .SelectMany(assemblyName => Assembly.Load(assemblyName).GetTypes())
   .Where(type => (typeof (IDataProvider)).IsAssignableFrom(type))
   .ToList();

Then we can create instances of these:

var instantiatedTypes = types
   .Select(Activator.CreateInstance)
   .ToList()
   .AsReadOnly();

This list of instantiated types then need to be passed to the core of the system, as a parameter to the DataMuncher class (or perhaps a method).

public sealed class DataMuncher {
   public DataMuncher(
      IEnumerable<IDataProvider> dataProviders,
      IEnumerable<IDataConsumer> dataConsumers
   ) {
      var consumersAndData = dataConsumers
         .Select(consumer => new { consumer, dataspec = consumer.GetDesiredDataSpecification() })
         .Select(cas => new {
            cas.consumer,
            cas.dataspec,
            data = dataProviders
               .Select(provider => provider.RetrieveData(cas.dataspec))
         })
         .ToList();
      consumersAndData.ForEach(cas => cas.consumer.ReceiveData(cas.data));
   }
}

This is very rough, just to show the idea. Don’t bother code reviewing this particular code, it is only here to show the basic idea and get assistance in how to structure the interfaces.

Note: I have been reading .Net solution structure of an enterprise application and it is possibly helping, but I don’t have a conclusion yet.

3

You can have one project holding the DataMuncher and the IDataProvider. Reference it from one or more Data Provider projects, implement the IDataProvider there.

From your application’s composite root have the concrete Data Provider injected into your DataMuncher before use.

What is not clear from your post is why DataMuncher was/is dependant on the Data Providers. If you invert this dependency properly things would get simple. DataMuncher defines the contract, the Data Provider project(s) provide an implementation.

Your app’s composition root configures when to use which implementation.

1

LEAVE A COMMENT