C# - Add common behaviour in abstract class after calling individual behaviours in subclasses

Viewed 180

I'm quite new to abstract classes. Usually you can create a member in a base-class and then extend its logic by calling it in the subclasses using base.Member. But can I do it the other way around?

I have an abstract class A with a property of type IEnumerable<T>:

abstract class A
{
    IEnumerable<int> Foo { get; }
}

I want the derived classes of A to return an individual IEnumerable and want to "filter" them using .Where by the same condition. I did following:

abstract class A
{
    public IEnumerable<int> Foo => Bar.Where(x => true);
    protected abstract IEnumerable<int> Bar { get; }
}
abstract class B : A
{
    protected override IEnumerable<int> Bar {
        get { yield return 1; }
    }
}

It works, but is this the right approach? Or can I do the same thing using only one property? It feels especially clunky if I have some classes between which also have to filter it because I'd just have to add more and more members based on the amount of layers I have.

3 Answers

This approach is just fine.

You have a behavior common to all derived classes (.Where()) in base class that conforms to DRY principle.

Then you have behavior specific to each derived class in those classes.

It feels especially clunky if I have some classes between which also have to filter it because I'd just have to add more and more members based on the amount of layers I have.

Well, it's perfectly fine if you're required to have different logic for different cases.

It's a pretty standard idiom.

Consider it thus: In defining the abstract Bar you're saying "all implementations can 'Bar'", and then when you call it from Foo that makes perfect since, since all implementations can 'Bar'.

It feels especially clunky if I have some classes between which also have to filter it because I'd just have to add more and more members based on the amount of layers I have.

Do you mean that other classes are also doing Where() on the same enumerable? Don't mind that; A is doing what Foo means, and the subclass is doing what Bar means for it, and whether that's another Where() or not is not something to worry about in A. (Sometimes we do have to break that rule if it turns out to be a performance hotspot, but firstly the initial design is not a place to consider that, and secondly Enumerable.Where() is in fact already optimised for sequential calls of a Where() on the results of Where() anyway).

What you have here is effectively the Template Method Pattern. This is especially suited when you have more than one protected abstract method, where the abstract class controls the flow of those methods.

For what you are trying to do, it is fine, and you can keep it if you want. However, it is not the only solution.

The Strategy Pattern

When you only have 1 method, injecting an object that does that 1 thing is sometimes advised. This is easier to extend, since for every new B you only need to test the specifics of it, and not the parts the abstraction does.

Lastly, I think your situation is best suited for The Decorator Pattern.

class B
{
    public IEnumerable<int> Foo => yield return 1;
}
class A : B
{
    private B b;
    public IEnumerable<int> Foo => b.Foo.Where(x => true);

    public A(B b)
    {
        this.b = b;
    }
}

This is somewhat a simplification, you might want an actual interface that both A and B implements. This really scales well when you talk about adding other classes that filter as well. Your classes can then be instantiated to do the filtering you want.

Related