-
Notifications
You must be signed in to change notification settings - Fork 83
Regarding the shared pointers
From day one, Hypodermic has been using std::shared_ptr
when some other libraries allow to inject std::unique_ptr
, references, etc. It may look like a limitation but we do think it is something good.
What if we know that the instance of a component should be non-shared across other components, that is, created every time the container is asked to resolve it? It is true that sometimes we would like to write things like this:
class Bus
{
public:
Bus(std::unique_ptr< IDispatcher >&& dispatcher)
: m_dispatcher(std::move(dispatcher))
{}
private:
std::unique_ptr< IDispatcher > m_dispatcher;
};
And configure the container kind of like this:
builder.registerType< Bus >().singleInstance();
builder.registerType< Dispatcher >().as< IDispatcher >();
The registration of Dispatcher
is not configured to be a single instance.
So that's it? Why not letting the container infer that Dispatcher
needs to be passed a unique pointer and even safely checks the configuration we provided?
Well, as the application is growing more and more, we would like the dispatcher to expose some metrics so we can publish them periodically from another component, for instance.
So we add an interface and reconfigure the container like so:
builder.registerType< Bus >().singleInstance();
builder.registerType< Dispatcher >()
.as< IDispatcher >()
.as< IMessageCountProvider >();
builder.registerType< MessageCountPublisher >();
and
class MessageCountPublisher
{
public:
MessageCountPublisher(std::unique_ptr< IMessageCountProvider >&& messageCountProvider)
: m_messageCountProvider(std::move(messageCountProvider))
{}
private:
std::unique_ptr< IMessageCountProvider > m_messageCountProvider;
};
What is the container supposed to do here? Inject another instance of Dispatcher
? Yes, it would definitely do that but that would break the intent here because IMessageCountProvider
and IDispatcher
don't share the same state.
So let's make it a single instance:
builder.registerType< Bus >().singleInstance();
builder.registerType< Dispatcher >()
.as< IDispatcher >()
.as< IMessageCountProvider >()
.singleInstance();
builder.registerType< MessageCountPublisher >();
Now what should we do? This is a single instance, typically transported as a shared pointer. But this time, we have to change our components' constructor to accept a shared_ptr
. Not cool... It breaks the idea of configuring the container at one place and let it control the lifetime of your objects sparing you the cost of maintenance.
This example works/breaks in other ways. We supposedly configured some component to be a single instance but what if we are injected this component as a shared_ptr
(legit) somewhere and as a unique_ptr
somewhere else? Should the container remind you by throwing an exception that you did configure the component to be shared? We think that it could...
... But... again, we think that it is easier to focus on a single point of configuration and preventing your components from having to know the lifetime of the dependencies they are injected. Having shared_ptr
of instances keeps you free to use them as transient/non-shared objects under the hood, the contrary is harder.