Skip to content
Yohan edited this page Jul 5, 2017 · 13 revisions

Nope, I'm good, show me some advanced features.

Designing a trivial application

Say we have an application that needs to serialize and write some messages.

// We can provide an abstraction for the serializer since we only
// need to know that we want to serialize a message in a stream.
class IMessageSerializer
{
public:
    virtual ~IMessageSerializer() = default;

    virtual void serialize(const std::string& message, std::ostream& stream) = 0;
};

// We implement a basic serializer which will write the length of
// the message before writing the message itself.
class LengthPrefixedMessageSerializer : public IMessageSerializer
{
public:
    void serialize(const std::string& message, std::ostream& stream) override
    {
        stream << message.size();
        stream << message;
    }
};

// The abstraction on the writer tells us that we can pass this
// object a message and it will do the job of writing it somewhere.
class IMessageWriter
{
public:
    virtual ~IMessageWriter() = default;

    virtual void write(const std::string& message) = 0;
};

// Eventually, we implement a writer that will be responsible for serializing
// the messages in the console. You can see it is injected a serializer.
class ConsoleMessageWriter : public IMessageWriter
{
public:
    explicit ConsoleMessageWriter(std::shared_ptr< IMessageSerializer > serializer)
        : m_serializer(serializer)
    {
    }

    void write(const std::string& message) override
    {
        m_serializer->serialize(message, std::cout);
    }

private:
    std::shared_ptr< IMessageSerializer > m_serializer;
};

Now, let's configure the container to let it know about these components.

Initializing the application

Since the container is responsible for your components creation, lifetime, etc. we have to register the types the application will need very close to the beginning of the program and keep a pointer on the built container.

#include "Hypodermic/Hypodermic.h"

class Application
{
public:
    Application()
    {
        // Components are registered in a ContainerBuilder
        Hypodermic::ContainerBuilder builder;

        // What we say here is: when I need an IMessageSerializer,
        // I want you to use this implementation.
        builder.registerType< LengthPrefixedMessageSerializer >()
               .as< IMessageSerializer >();

        builder.registerType< ConsoleMessageWriter >().as< IMessageWriter >();

        // Actually build the `Container` we have just configured.
        m_container = builder.build();
    }

    // We will implement this one in a second
    void run();

private:
    std::shared_ptr< Hypodermic::Container > m_container;
}

Running the application

So how is this helping me? Well, we just have to ask the container to build an instance of IMessageWriter.

void Application::run()
{
    // Container, give us an instance of `IMessageWriter`.
    auto messageWriter = m_container->resolve< IMessageWriter >();

    // Alright then, we can write some message.
    messageWriter->write("The app is running");
}

So what is happening when we resolve IMessageWriter?

  • The container is looking for a registration matching this type
  • It sees that IMessageWriter is mapped to ConsoleMessageWriter
  • Oh but look, ConsoleMessageWriter needs a IMessageSerializer
  • So it looks for IMessageSerializer and sees it is mapped to LengthPrefixedMessageSerializer
  • It instantiates LengthPrefixedMessageSerializer and pass it to the constructor of ConsoleMessageWriter
  • ... and eventually returns a fresh instance of ConsoleMessageWriter

You no longer have to know in what order you should construct your objects.

Going further

Not enough for you? Discover what you can do by reading this section.