Introducing seams across a hierarchy

As part of our team efforts to increase quality across our codebase, we have been ramping up our execution of swarms and pair programming exercises on important refactorings.

After the latest refactoring swarm on part of our codebase, I was tasked to follow-up and introduce some changes into the abstract Initializer class (which is located very deep into our core product library) so that it would be easier to use derived instances in our Component/Integration tests.

The Problem

Let's start diving into our situation. Instances derived from Initializer class are responsible for initializing specific components of the application following a dependency order, as part of the startup phase of the application. During these startup phase of the program, the application has access to a global list of Initializer instances, but since it is a list of the instances of the base class, currently it is not capable of recognizing different types of initializers.

As part of our tests startup, we have access to the global initializer list as follows:

std::vector<Initializer*> initializers = InitializersList::GetInitializers();

To make our integration tests more similar to the actual usage of the product, we would want to be able to selectively run some of these instances as part of our setup for the tests.

Based on this, we needed to figure it out how to introduce a seam into our hierarchy of classes in order to differentiate whether we want a particular instance to run or not.

Before looking into the alternatives, an example definition of the Initializer class is shown below:

// Initializer.h

class Initializer
{
public:
    virtual ~Initializer();
    
    virtual bool Initialize() = 0;
    
    // Rest of declaration
};

Introducing a new class

After a brainstorming analysis, we decided that best solution would be to add an intermediate class definition to signal that instances derived from this class should be initialized in the tests environment. Thus, we can introduced a new derived class as follows:

// IntegrationInitializer
class IntegrationInitializer : public Initializer
{
};

Let's say we have an EnvironmentInitializer class that we would like to run in the integration tests setup.

Based on the proposed solution, we would now change the EnvironmentInitializer declaration to inherit from IntegrationInitializer instead of directly from Initializer.

// EnvironmentInitializer.h
class EnvironmentInitializer: public IntegrationInitializer
{
public:
    EnvironmentInitializer();
    virtual ~EnvironmentInitializer();
    bool Initialize() override;
    
    // Rest of the class
};

Now that we have successfully introduced our desired seam in the class, it is time to figure out what options we have to run Initialize method from an IntegrationInitializer instance in the global list of initializers.

Finding the type

Below we are going to have a brief overview of our alternatives and the dive in more detail in the implemented solution.

typeid operator

From the docs, the typeid operator allows us to determine the type of an object at runtime. It cannot be any object though, it has to be an instance of a polymorphic type, i.e. a class with virtual functions.

Unfortunately, using typeid won't help us identify whether an Initializer instance is also derived from IntegrationInitializer. Applying the typeid operator will either return information of the most derived type, e.g, EnvironmentInitializer or information of the static class of the object, in this case symply Initializer.

dynamic_cast operator

A dynamic_cast conversion, safely converts pointers and references between classes up, down and sideways in the inheritance hierarchy.

Let's see below how could we implement our solution using the dynamic_cast operator in our test startup process:

static void RunInitializersForTestStartup()
{
    for (auto initializer : InitializersList::GetInitializers())
    {
        auto integrationInit = dynamic_cast<IntegrationInitializer*>(initializer);  
        
        if (integrationInit != nullptr)
        {
            integrationInit.Initialize();
        }    
    }
}

As we can see in the code above, we use dynamic_cast to try to perform a conversion from the base type Initializer to the intermediate one IntegrationInitializer. If the conversion is successful, which the integrationInit variable is not nullptr the we can safely call the Initialize method.

Visitor pattern

With the Visitor pattern, we are going to move away from directly asking for the type to rely instead in the facilities of type dispatching to perform the action based on the runtime type of the object.

In order to apply the Visitor pattern, we need to open the Initializer and IntegrationInitializer classes for modification and add a new Accept method that will receive an instance of the visitor implementation.

class InitializerVisitor // Root visitor class
{
public: 
    virtual void Visit(Initializer& initializer)
    { }
    
    virtual void Visit(IntegrationInitializer& initializer)
    { }
};

class Initializer
{
public:
    virtual void Accept(InitializerVisitor& visitor) inline
    {
        visitor.Visit(*this);
    }
    
    // Rest of the class declaration
};

class IntegrationInitializer : public Initializer
{
public:
    virtual void Accept(InitializerVisitor& visitor) inline override
    {
        visitor.Visit(*this);
    }    
};

Now that we have the plumbing in place, we can do the following in our test environment:

class InitializerInTestVisitor : public InitializerVisitor
{
public:
    virtual void Visit(IntegrationInitializer& initializer) override
    {
        initializer.Initialize();
    }
};

static void RunInitializersForTestStartup()
{
    InitializerInTestVisitor visitor;
    
    for (auto initializer : InitializersList::GetInitializers())
    {
        initializer.Accept(visitor); 
    }
}

As we can see, this is the most aggressive solution, because besides the new type introduced as seam, IntegrationInitializer, we also need to modify the base class to add a new method Accept. With this implementation, there is one if statement less in the code than with the dynamic_cast solution, which reduces the cyclomatic complexity of the code. Also, this solution is more powerful and extensible as new behaviors can be implemented in visitor classes instead of further modifying the initializer classes.

Conclusion

With this post, we went over a refactoring problem and analyze different strategies to use to solve it. First, we stated the problem and how we need to introduce a seam, in this case a type seam, to be able to identify at runtime the desired instances to initialize. Then, we took a pass over the different ways we can implement solutions that depend on type dispatching for objects in C++ to identify the seam and whether they would be suitable for our task.

Finally, thanks you so much for reading this post. Hope you liked reading it as much as I did writing it. Stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Introducing seams across a hierarchy https://bitsofknowledge.net/2018/07/14/introducing-seams-across-a-hierarchy/ As part of our team efforts to increase quality across our codebase, we have been ramping up our execution of swarms and pair programming exercises on

Rene Hernandez

Read more posts by this author.

Ready to not miss any bit. Subscribe below!