Question

I'm currently in a situation where I have multiple derived class types(just one is shown in the example below) that need to apply some logic before and/or after calling a method implemented in a common base class. I have two scenarios below and was wondering what would be a downside and upside for each approach in this use case scenario? The client code will be calling the relevant classes as defined in the main stub. The template method design pattern that is presumably being followed correctly in scenario b seems to be more popular based on research, but from first glance the code for approach a looks to be more minimal.

Thank you.

Scenario A:

using System.IO;
using System;

interface IContract
{
  void Foo();
};

abstract class Base : IContract 
{
    protected void FooImpl()
    {
        Console.WriteLine("base work");
    }
    public abstract void Foo();
};

class Derived : Base
{
    public override void Foo()
    {
        Console.WriteLine("pre work derived");
        FooImpl();
        Console.WriteLine("post work derived");
    }
}

class Program
{
    static void Main()
    {
        IContract contract = new Derived();
        contract.Foo();
    }
}

Scenario B:

using System.IO;
using System;

interface IContract
{
  void Foo();
};

abstract class Base : IContract 
{
    protected abstract void PreFooHook();
    protected abstract void PostFooHook();

    public void Foo()
    {
        PreFooHook();
        Console.WriteLine("base work");
        PostFooHook();
    }
};

class Derived : Base
{
    protected override void PreFooHook()
    {
        Console.WriteLine("pre work derived");
    }
    
    protected override void PostFooHook()
    {
        Console.WriteLine("post work derived");
    }
}

class Program
{
    static void Main()
    {
        IContract contract = new Derived();
        contract.Foo();
    }
}

Desired Output:

pre work derived
base work
post work derived
Was it helpful?

Solution

In your first scenario, you're not enforcing any sort of behavior on the derived classes. You're counting on them calling FooImpl properly, between the hooks, but if they don't, your whole process can be broken by a misbehaving child class. And consider it from the child class developer, who may be in a different team or even a different company if the base class is part of a public framework: you want to implement a base class, but how do you know what to do? When to call FooImpl, if at all? Read the docs? Flaky, risky.

Scenario B is, IMO, cleaner, safer and easier. Child classes need to explicitly implement the hooks, which will always be called before and after FooImpl. And if you need a completely different FooImpl? Simply implement a completely different IContract.

OTHER TIPS

... in scenario b seems to be more popular ... approach a looks to be more minimal

Neither popularity nor the size of the code is an indicator of its quality. Neither it indicates what approach better suites your particular needs.

In the type A having FooImpl and for the first time using it only in Derived makes no sense. You cannot inforce classes derived directly from Base to use it. Better would be to merge Base and Derived into a single class.

I suppose that you reproduced the cases that you observed not quite correctly. In both cases A and B the template implementation of method Foo should be final. Means, Derived.Foo in the case A and Base.Foo in the case B should both be final. Thus these classes would enforce particular pattern for all their subclasses.

If you declare them as final, you will see that there is not much difference between A and B: The core implementation will be in the Foo, and subclasses will be allowed to modify only some part of it. Both A and B will be then just a specific cases of more generic case:

public abstract class Base : IContract 
{
    protected abstract void Hook1();
    protected abstract void Hook2();
    protected abstract void Hook3();
    
    public final void Foo()
    {
        ... some logic ...
        Hook1();
        ... some logic ...
        Hook2();
        ... some logic ...
        Hook3();
        ... some logic ...
        ...
    }
};

An example of such class can be sending an email. The base class can provide the core logic and subclasses could provide a hook method to determine the sender, another hoos to determine the list of receivers, a hook that provide sbject, a hook that provides message body.

But having too many hook methods makes code harder to understand and to maintain. It may be easier to provide the whole own implementation of method Foo instead of trying to understand the meaning of each hook method. That's why in reality I mainly see really only two types similar to A and B. In case subclasses need to provide the core logic, but some pre- and post-operations need to be enforced, type A is used. In case the core logic is implemented in the base class, but subclasses are allowed to participate on pre- and post-operations, type B is used.

Licensed under: CC-BY-SA with attribution
scroll top