1

I have a class which aggregates some temporal data from a database and provides a bunch of methods to query said data. It looks like this:

public class InfoProvider
{
    public IEnumerable<Contract> GetContracts() => GetContracts(DateTime.Today);
    public IEnumerable<Contract> GetContracts(DateTime at) => GetContracts(at, at);
    public IEnumerable<Contract> GetContracts(DateTime from, DateTime to) { /* query contracts */ }

    public IEnumerable<Job> GetJobs() => GetJobs(DateTime.Today);
    public IEnumerable<Job> GetJobs(DateTime at) => GetJobs(at, at);
    public IEnumerable<Job> GetJobs(DateTime from, DateTime to) { /* query jobs */ }

    /* more methods like above ... */
}

Now I need to extend my application to support different representations of the data in the database. Those representations are identical for the most part but have some subtle differences. For example GetContracts in representation 1 should return contracts of type A, B or C while in representation 2 it should return contracts only of type A or B. On the contrary, GetJobs would be the same in both representations.

I want to do this extension by refactoring InfoProvider into an abstract class and derive concrete classes, InfoProviderRepresentation1 and InfoProviderRepresentation2 for each representation. But I waver where to put the existing code. I see those two options:

  1. Leave it in the original class
public abstract class InfoProvider
{
    public IEnumerable<Contract> GetContracts() => GetContracts(DateTime.Today);
    public IEnumerable<Contract> GetContracts(DateTime at) => GetContracts(at, at);
    public virtual IEnumerable<Contract> GetContracts(DateTime from, DateTime to) { /* query contracts */ }

    public IEnumerable<Job> GetJobs() => GetJobs(DateTime.Today);
    public IEnumerable<Job> GetJobs(DateTime at) => GetJobs(at, at);
    public virtual IEnumerable<Job> GetJobs(DateTime from, DateTime to) { /* query jobs */ }
}

public class InfoProviderRepresentation1 : InfoProvider
{
}

public class InfoProviderRepresentation2 : InfoProvider
{
    public override IEnumerable<Contract> GetContracts(DateTime from, DateTime to) { /* ... */ }
}
  1. Move the code to an other abstract class and derive my concrete classes from there:
public abstract class InfoProvider
{
    public IEnumerable<Contract> GetContracts() => GetContracts(DateTime.Today);
    public IEnumerable<Contract> GetContracts(DateTime at) => GetContracts(at, at);
    public abstract IEnumerable<Contract> GetContracts(DateTime from, DateTime to);

    public IEnumerable<Job> GetJobs() => GetJobs(DateTime.Today);
    public IEnumerable<Job> GetJobs(DateTime at) => GetJobs(at, at);
    public abstract IEnumerable<Job> GetJobs(DateTime from, DateTime to);
}

public abstract class DefaultInfoProvider
{
    public virtual IEnumerable<Contract> GetContracts(DateTime from, DateTime to) { /* query contracts */ }
    public virtual IEnumerable<Job> GetJobs(DateTime from, DateTime to) { /* query jobs */ }
}

public class InfoProviderRepresentation1 : DefaultInfoProvider
{
}

public class InfoProviderRepresentation2 : DefaultInfoProvider
{
    public override IEnumerable<Contract> GetContracts(DateTime from, DateTime to) { /* ... */ }
}

I find option 2 a bit more readable. On the other hand it needs more boilerplate code than option 1. Does option 2 have any more advantages over option 1?

0

3 Answers 3

3

This is the kind of question that you can ask a dozen different people and get a dozen different answers. I say do the simplest thing that solves your problem: if all you need to do is override GetContracts, don't even bother creating an abstract class, just derive from your existing class and override it. If you find yourself doing this a whole bunch and things start getting messy, then you can look into other strategies such as interface segregation and composition.

2

I would propose option 3: Create an interface

public interface IInfoProvider
{
    IEnumerable<Contract> GetContracts(DateTime from, DateTime to);
    IEnumerable<Job> GetJobs(DateTime from, DateTime to);
}

This is fairly similar to option 2, but is a little bit more flexible since it uses interfaces instead of abstract base classes. I would prefer this design since it makes things like decorators more natural since it avoids potential unneeded dependencies and removes the risk of forgetting to override a method.

Either move the overloads to extension methods of the interface, or use default interface implementations.

3
  • I don't think that an interface would help much here. The shared implementation for both representations would still have to go somewhere. Default interface implementations could help but unfortunately I'm tied to C# 7 Commented Sep 7, 2021 at 14:00
  • when you wrote “more flexible”, did you mean less constraints regarding inheritance ? or something else?
    – Christophe
    Commented Sep 8, 2021 at 6:34
  • @Christophe Yes, the primary advantage would be less constraints regarding inheritance, and this might make things like mocking frameworks easier to use. But you can also consider how it it interpreted. For me, an abstract base class communicates "implementation inheritance" rather than "interface inheritance".
    – JonasH
    Commented Sep 8, 2021 at 7:55
0

Look up the Gang of Four book on architecture. In that seminal book they mostly conclude one thing; "favor composition over inheritance". Coupled with the SOLID principle we find that two constructs are superior 1) containing properties and 2) using interfaces as parameters. When we add decorator patterns in, and keep in mind the Interface Segregation Principal we find most everything is solvable using just properties and parameters as interfaces.

How does this apply to this question? There will be a base Interface Def. As new Interface types are needed we are able to extend/decorate the base Interface with the new requirements. We then just add the implementation of the 'super Interface ' as an additional property to the class. This means that the class supports two Interface type with complete segregation. This follows the Single responsibility pattern which if strictly followed ensures changes in one part never affects other parts which are always unrelated.

If we strictly follow these concepts we achieve a much more 'pure functional style. Our code will merely compose pure functions. Which can all be added to, deleted from and decorated with zero impact to any other function within the program. It leads to ultimate maintainability which often means the effort down the road becomes much easier.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.