google / fruit

Fruit, a dependency injection framework for C++

Home Page:https://github.com/google/fruit/wiki

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Handling lifetimes

IrisPeter opened this issue · comments

In the codebase I'm working on, I wish to introduce some DI to replace our Business Objects system.

We had a factory class that handed out Business objects, sometimes the object handed out would be the same one every time.

However for other objects there were a variety of std::maps that were used, each map would have its own key struct, if the key provided was new then a Business Object would be created, and then stored as the value_type of the map using a referenced counted smart pointer. If the key had been seen before then a smart pointer to the object stored in the map would be handed out.

I had a look at the Server example because on Stackoverflow I saw an answer that said that the scope is supposed to be maintained by injectors and it pointed to the fruit wiki and that server example.

The OP for the stackoverflow question, was looking to have DI hand out a singleton, and he comes up with his own way of doing that with fruit after reading about injectors. I'm not sure whether the solution he came up with is the correct one, for those who want the same object handed out each time its requested.

His solution looked like this:

fruit::Component<SingletonInterface> CreateSingletonComponent()
{
    static MySingleton instance;
    return fruit::createComponent().bindInstance((SingletonInterface&)instance);
}

However whilst we do sometimes have a need for the same object handed out each time, we really do need to be able to manage the lifetimes of other objects in such a way that the key handed to the factory function would govern whether the same object came back came back, or whether a new one did based on the key provided.

I had a look at the Server example, but I did not understand it enough to be able to adapt it to our needs.

Our original looks something like the following:

void someMethod()
{
    auto licenseInfo = CBusinessObjectBroker::GetLicenseInformation();//returns license information singleton - query for details of the license
    int clientKey = 25;
    auto clientStatement = CBusinessObjectBroker::GetStatement(clientKey);//returns statement for client, on first call object created, subsequent calls same object will be returned
}

CStatement::ptr CBusinessObjectBroker::GetStatement(int clientKey)
{
    CBusinessObjectBroker& broker = CBusinessObjectBroker::Instance()// get the broker singleton
    if(broker.m_statements.find(clientKey) == broker.m_statements.end())
    {
        auto clientStatement = CStatement::Create(clientKey);//Create Business Object         
        broker.m_statements[clientKey] = clientStatement;//Store object in map
    }
    return broker.m_statements[key];//return smart pointer to the required object
}

Our existing code loads certain objects from a SQL Server database, but as the business objects system does not use DI, we can't for example swap out to something that say loads from JSON for unit testing.

Any help would be much appreciated

If the set of keys is fixed at compile time, you could use annotated injection for this.
If not (as in the example above with a client ID) then you could have Fruit inject the map but then insert entries yourself when you want to.
When injecting a T& multiple times from the same injector, Fruit will always return the same instance.

If you have a per-process injector (as the first injector created in the server example) then you can have Fruit construct the singleton for you in that injector, instead of using a static var as above.
In production the 2 approaches are equivalent, but in tests the injected approach allows you to have a fresh singleton for each test case if you want (and it's probably a good idea), while in the static approach you'd have to either run each test case in a new process or have to reset the singleton between test cases (which would require exposing that impl detail potentially several layers of abstraction higher than it should be).

I would recommend against the static singleton approach unless you really can't use injection for it for some reason (and I can't imagine a good reason).

So you'd inject:

  • &CBusinessObjectBroker (wrapper around the map), and
  • std::function<std::unique_ptr<CStatement>()> so that you can inject things into CStatement if you want

I hope that helps, lmk if you have more questions.

@poletti-marco On the per-process injector which you say is the first injector created in the server example. Is that requestDispatcherNormalizedComponent on line 47 of server.cpp?

Or is it inside worker_thread_main?

The set of keys is created at runtime rather than compile time. I assume that to be able to have similar lifetime control to the GetStatement method described by me above, that I will again need to check the map to see if the key is present, and then if it isn't I need to get a CStatement::ptr using a Injector<CStatement>, perhaps one using the NormalizedComponent class, and then insert that into the map?

In our business objects ptr was a typedef for an in house intrusive pointer template class called counted_ptr which dealt with reference counts, for our new equivalent of CBusinessObjectBroker we will be using the std library, and so I think we need to use - std::function<std::shared_ptr<IStatement>(int)>;

Is the server example going to be the best one for me to be looking at to come up with a similar mechanism that is in our CBusinessObjectBroker?

Your first bullet point "So you'd inject &CBusinessObjectBroker (wrapper around the map)" - I assume you mean:

fruit::Injector<CBusinessObjectBroker> injector(getBusinessObjectBrokerComponent);
CBusinessObjectBroker::Ptr brokerObjectPtr(injector);

correct?


The set of keys is created at runtime rather than compile time.> > Or is it inside worker_thread_main?

This is surprising. Then maybe those objects represent individual data items?

The majority of our business objects are loaded from the SQL Database and are connected to clients registered with the system (i.e. stored in a database table, and loadable by the user via a client selector dialog) which is why the objects are defined at runtime rather than compile time.

I need to preserve the existing lifetime functionality of each business object, and so the CBusinessObjectBroker will need to maintain the same mechanism for handing out each object that the current object supports (See Object support further down).

Ptr typdef

In the existing code base the majority of business objects have a typdef counted_ptr<CObjectName> CObjectName_ptr; in the parent namespace that the business objects class resided in.

I noticed that for one of these objects the writer of the object instead placed a typedef within the class itself to the counted_ptr<T> called ptr, I decided to do the same but for every object I duplicate from the original business objects library I would call the typedef ::Ptr, and so CObjectName_ptr becomes CObjectName::ptr

I've introduced a template class to provide the relevant typedefs using CTRP, in the case of CBusinessObjectBroker::Ptr is the same as CBusinessObjectBroker*, the broker will be handing out std::shared_ptr<T>

Object Support

CBusinessObjectBroker either a) hands out objects either directly from member variables within CBusinessObjectBroker, or b) from maps, where the keys are classes encapsulating database keys, sometimes these keys are just ints representing tax years.

In the original Business Tax Objects library we were handing out objects using our own internal intrusive pointer type called counted_ptr, which was a template class that was used similarly to shared_ptr, but whose history dates back to some time before 2007.

In the case of a) the member variables where counted_ptr, and so the objects are constructed on demand, b) the maps again store counted_ptr and objects are either handed out from the map if they exist already or constructed on demand and stored in the relevant map, before being handed out.

For every business object that currently hands out of these counted_ptr<T> intrusive pointers, I will replace these by std::shared_ptr<T>

The CBusinessObjectBroker is created as a singleton and is retrieved by CTaxBusinessObjectBroker& CTaxBusinessObjectBroker::Instance()
and is what we use to create all our business objects.


Your first bullet point "So you'd inject &CBusinessObjectBroker (wrapper around the map)" - I assume you mean:

I don't know what CBusinessObjectBroker::Ptr is, I would have expected CBusinessObjectBroker& there. And the injector would probably have other types too (CBusinessObjectBroker may not be one of the toplevel types at all). But maybe that was an intentional simplification as an example.

From your reply, I think you are saying the same as me when you said:

while using some other mechanism (e.g. a map or some other data structure that might live inside one of the object that Fruit manages) to hold any other objects that are more dynamic.

which seems similar to when I said:

The set of keys is created at runtime rather than compile time. I assume that to be able to have similar lifetime control to the GetStatement method described by me above, that I will again need to check the map to see if the key is present, and then if it isn't I need to get a CStatement::ptr using a Injector, perhaps one using the NormalizedComponent class, and then insert that into the map?


You could still use Fruit to inject a factory to create those objects, that would return e.g. a unique_ptr of that object but then leave it to you to store those in a map/etc and cache/reuse objects when desired.

So this tutorial is probably more relevant than the server one for this: https://github.com/google/fruit/wiki/tutorial:-assisted-injection And this part of the reference documentation: https://github.com/google/fruit/wiki/quick-reference#factories-and-assisted-injection

Could you expand on this, none of the examples seem to use registerFactory, the Wiki page declares getMyClassComponent, but then doesn't seem to show its usage?

Oh I see in component.h shows what seems to be missing from the Wiki page, that is creating the injector that is then passed to the factory declaration

Injector<std::function<std::unique_ptr<MyClass>(int)>> injector(getMyClassComponent);

std::function<std::unique_ptr<MyClass>(int)> myClassFactory(injector);
std::unique_ptr<MyClass> x = myClassFactory(15);

However I'm still a little confused, why is .RegisterFactory used in the above example, it looks very similar to the ScalerFactory in the scaling doubles example, which didn't seem to need RegisterFactory, is it because the ScalerFactory just takes non injected parameters whereas MyClass also needs a FOO* injected into it?

In my message on Friday I mentioned NormalizedComponent it seems to be some sort of optimisation for when using factories that are expensive in some way, now that you've pointed me away from the server example for my use case, does that mean this is not going to be of use to me?

Then again I also see the use of NormalizedComponent in the testing example. What is the criteria should I look for on deciding whether I need to use this?

Looking in the Testing example I see you have a createInjector method, whether we are going to be using NormalizedComponent or not. What are the criteria I should look for when deciding whether I need have a createInjector method?

The cached_greeter_test[_with_normalized_component].cpp example is very helpful as I see that it shows me how I will replace our business objects standard loading from DB with loading from JSON, in the same way that getFakeKeyValueStorageComponent swaps out the standard behaviour of the storage component with one that uses a std::map to simulate what the real component will be using when under test.

On the per-process injector which you say is the first injector created in the server example. Is that requestDispatcherNormalizedComponent on line 47 of server.cpp?
Or is it inside worker_thread_main?

Oh it looks like there's a single injector there, per-request.
If you wanted a per-process injector you could define one e.g. in main or in a closely related function that is called only once.

The set of keys is created at runtime rather than compile time.

This is surprising.
Then maybe those objects represent individual data items?
I'd recommend using Fruit/DI to compose the modules of your system and the more static components that you can describe with a fixed set of keys, while using some other mechanism (e.g. a map or some other data structure that might live inside one of the object that Fruit manages) to hold any other objects that are more dynamic.

You could still use Fruit to inject a factory to create those objects, that would return e.g. a unique_ptr of that object but then leave it to you to store those in a map/etc and cache/reuse objects when desired.

So this tutorial is probably more relevant than the server one for this: https://github.com/google/fruit/wiki/tutorial:-assisted-injection
And this part of the reference documentation: https://github.com/google/fruit/wiki/quick-reference#factories-and-assisted-injection

Your first bullet point "So you'd inject &CBusinessObjectBroker (wrapper around the map)" - I assume you mean:

I don't know what CBusinessObjectBroker::Ptr is, I would have expected CBusinessObjectBroker& there.
And the injector would probably have other types too (CBusinessObjectBroker may not be one of the toplevel types at all). But maybe that was an intentional simplification as an example.

However I'm still a little confused, why is .RegisterFactory used in the above example, it looks very similar to the ScalerFactory in the scaling doubles example, which didn't seem to need RegisterFactory, is it because the ScalerFactory just takes non injected parameters whereas MyClass also needs a FOO* injected into it?

If the std::function just needs to call a constructor with a mix of injected params + the ones provided to the std::function's operator(), then you can use the ASSISTED(...) approach, it's simpler.
registerFactory is a generalization of that that allows to call arbitrary code (not just the constructor of the type returned by the factory) when the std::function is called.

Then again I also see the use of NormalizedComponent in the testing example. What is the criteria should I look for on deciding whether I need to use this?

NormalizedComponent is useful if you want to create many independent injectors using the same get*Component function.
Using that you can move some of the cost of the injector creation to the process startup, and share it across all injectors that are created.
If you're going to create just 1 injector there's no point using this.

E.g. this is appropriate for servers where you might want to isolate the handling of each request so that each request uses a separate injector (with separate instances of all objects).
From what you said, this doesn't seem to be the case here?
Or at least, this is orthogonal to the discussion above on how to model CBusinessObjectBroker, since AFAICT for that the goal is to share instances so it sounds like you want only 1 CBusinessObjectBroker.

Looking in the Testing example I see you have a createInjector method, whether we are going to be using NormalizedComponent or not. What are the criteria I should look for when deciding whether I need have a createInjector method?

createInjector is not a special name, the testing example just happens to define a helper function with that name to call that in all tests, to avoid duplicating the code in all of them. You could call it createMyInjector or inline it, it doesn't matter.

This is not like the get*Component functions where you do need a function returning Component and you can't just inline that.

Sorry for simplicity's sake I probably should have said:

Your first bullet point "So you'd inject &CBusinessObjectBroker (wrapper around the map)" - I assume by that you mean:

fruit::Injector<CBusinessObjectBroker> injector(getBusinessObjectBrokerComponent);
CBusinessObjectBroker* brokerObjectPtr(injector);

correct?

On your other question:

And the injector would probably have other types too (CBusinessObjectBroker may not be one of the toplevel types at all). But maybe that was an intentional simplification as an example.

No in this particular case I did mean what I said, in that there is only one CBusinessObjectBroker it was not a simplification, but yes as described in my previous message whilst CBusinessObjectBroker is a top level type, it is mostly the mechanism to get a specific business object (non top-level types in your taxonomy) whilst most of the time they are provided by the broker each object has a public static method Create which will return a counted_ptr. Instead of counted_ptr I will be using std::shared_ptr as I think its better to use smart pointers from the standard library rather than these internal types.

Hi, is there some remaining question here?
The discussion got quite long, if you're still waiting for an answer on something can you please mention that here?
Or if your questions here were addressed please close this.