Inversion of control containers allow systems to achieve dependency injection, by specifying the dependencies of objects indirectly. Typically, the IOC container will be responsible for the actual instantiation of a concrete type.
Introduction
Inversion of control containers allow systems to achieve dependency injection, by specifying the dependencies of objects indirectly. Typically, the IOC container will be responsible for the actual instantiation of a concrete object, selecting a constructor and returning only the reference type, obtaining instances of any dependencies before calling the constructor. Today, I’ve had a play at creating a minimal one of these and thought I would share it with you.
Obtaining a Type Code
Before we can safely store a way of obtaining a class subscribing to a particular interface, we should allocate a type code for this. In my implementation of the IOC container, I have a static integer variable that specifies the next type id that will be allocated, and a static local variable instance for each type, which can be accessed by calling the GetTypeID
method.
class IOCContainer
{
static int s_nextTypeId;
public:
template<typename T>
static int GetTypeID()
{
static int typeId = s_nextTypeId ++;
return typeId;
}
Obtaining Object Instances
Ok, that was easy. Now since we’ve got a type ID, we should be able to store some kind of factory object to represent the fact that we don’t know how to create the object. Since I want to store all of the factories in the same collection, I’m opting for an abstract base class which the factories will derive from, and an implementation that captures a functor to call later.
For brevity, I’ve used a std::map
to hold the factories, however you might consider other options for the sake of efficiency. If you’re building the collection once and then using it several times, then a sorted std::vector
of std::pair
would make a lot of sense.
class FactoryRoot
{
public:
virtual ~FactoryRoot() {}
};
std::map<int, std::shared_ptr<FactoryRoot>> m_factories;
template<typename T>
class CFactory: public FactoryRoot
{
std::function<std::shared_ptr<T> ()> m_functor;
public:
~CFactory() {}
CFactory(std::function<std::shared_ptr<T> ()> functor)
:m_functor(functor)
{
}
std::shared_ptr<T> GetObject()
{
return m_functor();
}
};
template<typename T>
std::shared_ptr<T> GetObject()
{
auto typeId = GetTypeID<T>();
auto factoryBase = m_factories[typeId];
auto factory = std::static_pointer_cast<CFactory<T>>(factoryBase);
return factory->GetObject();
}
Registering Instances
Now all we need to do is populate the collection. I’ve implemented a few different ways of doing this: You might want to supply a functor explicitly, or you might want to choose between a single instance, or creating new instances on demand.
template<typename TInterface, typename ...TS>
void RegisterFunctor(std::function<std::shared_ptr<TInterface>
(std::shared_ptr<TS> ...ts)> functor)
{
m_factories[GetTypeID<TInterface>()] =
std::make_shared<CFactory<TInterface>>([=]{return functor(GetObject<TS>()...);});
}
template<typename TInterface>
void RegisterInstance(std::shared_ptr<TInterface> t)
{
m_factories[GetTypeID<TInterface>()] =
std::make_shared<CFactory<TInterface>>([=]{return t;});
}
template<typename TInterface, typename ...TS>
void RegisterFunctor(std::shared_ptr<TInterface> (*functor)(std::shared_ptr<TS> ...ts))
{
RegisterFunctor(std::function<std::shared_ptr<TInterface>
(std::shared_ptr<TS> ...ts)>(functor));
}
template<typename TInterface, typename TConcrete, typename ...TArguments>
void RegisterFactory()
{
RegisterFunctor(
std::function<std::shared_ptr<TInterface> (std::shared_ptr<TArguments> ...ts)>(
[](std::shared_ptr<TArguments>...arguments) -> std::shared_ptr<TInterface>
{
return std::make_shared<TConcrete>
(std::forward<std::shared_ptr<TArguments>>(arguments)...);
}));
}
template<typename TInterface, typename TConcrete, typename ...TArguments>
void RegisterInstance()
{
RegisterInstance<TInterface>(std::make_shared<TConcrete>(GetObject<TArguments>()...));
}
};
Usage Scenario
Ok, we’re pretty much ready to go. Here, I’m going to show a toy usage scenario, where I’ve registered two objects – one with a constructor that will be called and pass in the instance of the other type. You’ll note that I have to define the static type id counter here, and give it an initial value – it’s not important what value you use here, but I just wanted it to be something that is easy to recognise.
IOCContainer gContainer;
int IOCContainer::s_nextTypeId = 115094801;
class IAmAThing
{
public:
virtual ~IAmAThing() { }
virtual void TestThis() = 0;
};
class IAmTheOtherThing
{
public:
virtual ~IAmTheOtherThing() { }
virtual void TheOtherTest() = 0;
};
class TheThing: public IAmAThing
{
public:
TheThing()
{
}
void TestThis()
{
std::cout << "A Thing" << std::endl;
}
};
class TheOtherThing: public IAmTheOtherThing
{
std::shared_ptr<IAmAThing> m_thing;
public:
TheOtherThing(std::shared_ptr<IAmAThing> thing):m_thing(thing)
{
}
void TheOtherTest()
{
m_thing->TestThis();
}
};
int main(int argc, const char * argv[])
{
gContainer.RegisterInstance<IAmAThing, TheThing>();
gContainer.RegisterFactory<IAmTheOtherThing, TheOtherThing, IAmAThing>();
gContainer.GetObject<IAmTheOtherThing>()->TheOtherTest();
return 0;
}
Final Bits
So that’s pretty much it for this time. I think there’s potentially several things that you could do to improve the container, but this is a start. If you have any comments, suggestions, or complaints, let me know! I’d love to hear from you. Thanks for reading!
History
- 13th September, 2015: Initial version
- 6th March, 2021: Initial version