angular / di.js

Dependency Injection Framework for the future generations...
Other
818 stars 95 forks source link

Step back with import? #41

Open VilemP opened 10 years ago

VilemP commented 10 years ago

Hi Vojta!

I watched your presentation from ng-conf and I understand the motivations for the new DI module. Still, I could not resist to write this (Sorry if this is not the right place).

Let me use your CoffeeMachine example a bit. Let's imagine I have several, let's say 10 parts from which the CoffeeMachine is build, all of them using Electricity dependency.

Let's just say that this is provided by the "PowerLineElectricity" implementation. All fine so far.

I understand that I can replace the implementation by mock by providing the mock to the injector. Still OK.

But what if I need to change the PowerLineElectricity implementation with the SolarElectricity implementation for 4 out of the 10 parts. If I got it right, I have to go to the 4 files and change the import there... With four files it is manageable, but how about 23 out of 50?

In our current project (node.js based, I admit), we are heavily using your previous version of DI framework. On top of it, we use a module which builds the dependency configuration for the injector out of the config file where we define all the dependencies. So, if I need to change 4 dependencies, I need to go to one file (knowing exactly which one) and change it for 4 items there. I admit this is "string" based so minification unfriendly but maybe there is a better way than the one we are using one which would work with minification as well.

I was wondering whether there is any possibility to have something like that off the shelf with the new DI :-) Or, what disadvantages there might be with our approach?

If you wish, we can discuss it deeper in Czech as well, if you let me know how to contact you.

best

Michal

VilemP commented 10 years ago

Sorry, one more comment as a conclusion:

What I don't like with the new approach is that the dependency "definitions" are now scattered across many different files and repeated (not DRY) so any change which requires something else than changing ALL implementations for some dependency means editing potentially many files.

vojtajina commented 10 years ago

Hi @VilemP,

yes, if 10 objects need the same dependency and then you decide you want to use a different version in 4 or them, you need to annotate them differently. Depending on how your application is designed, you might be able to use child injectors to inject different implementation for the same token.

Using this DI with Node.js is fine, it's goal is to work both in the client and in Node.js.

Can you send me a link to your project that you mentioned? I'm curious to see what you did.

Regards string ids. With this new DI, a token can be anything (object, function, string...) and thus you can use string ids if you really want to.

vojtajina commented 10 years ago

Ad your second comment.

With DI (in general), each component has to declare its dependencies somehow. In the old world that was a string token, in the new DI this token can be anything. This token is like a contract - it describes what kind of dependency it is. Then, whoever is instantiating the component has to pass in something that satisfies this contract.

We realized that in most of the cases, you don't override dependencies and just rely on the default implementations and that's why the new DI supports "default providers" which means, if the token (that describes the dependency) is a function, you don't have to provide anything for it and DI will use the token itself to create the dependency.

Another reason why I like this is that it feels like less magic. Consider this:

// without DI
var Foo = require('foo');

var Bar = function() {
  this.foo = new Foo();
};

// with DI
var Foo = require('foo');

@Inject(Foo)
var Bar = function(foo) {
  this.foo = foo;
};

I think you might check out https://github.com/angular/di.js/blob/master/docs/interfaces.md

VilemP commented 10 years ago

Hi,

sorry for the late reply.

Unfortunately the project I mentioned is not publicly available, but I will try to explain more details below. As for the dependency declaration - I do agree that most of the time you are just using default implementation. Where in my experience you might expect changes is data access, messaging etc. I also do agree that explicit declaration of dependencies makes it feeling less magic but unfortunately it also breaks the DRY principle. If I am using certain dependency 20 times, I have to declare it 20 times... So, one has to weight between these two I think.

Back to what we are doing. First, we did not want to have a notion of injector scattered across the project. Therefore, the app is initialized through app.js, which is, besides factories, the only place where we have an injector (this is also why using multiple injectors does not help in our case).

So, you would have something like:

// app.js

var dependencyConfig = require ('./dependencyConfig'); var dependencyProvider = require('./dependencyProvider'); var Injector = require('di').Injector;

var deps = dependencyProvider.getDependencies(dependencyConfig);

var injector = new Injector( deps );

var server = injector.get("server");

....

All the other injections are done while instantiating the dependencies of "server" etc. (again, with the exception of factories, but there the injector is injected as well).

You can notice the "dependencyConfig". This is the place where we declare all the dependencies in one place. So whenever you need to change or add dependency, you know exactly where to go. Also, for testing, we might have a different dependency config which substitutes heavy dependencies, database access objects, messaging etc. with appropriate mocks.

Dependency config looks usually like this:

module.exports = {

"dbClient" : { type:"value", require: "mysqlClient" }, "depA" : { type: "type", implementation: function SomeConstructor() {} }, "depB" : { type:"factory", require: "./x.js", injectables: { "depA" : "dbClient" }

}

So, it lists the dependencies and provides more instructions for their creation. E.g. we may want to create the dependency by requiring existing module, by providing the actual value which should be injected etc.

The config is passed to dependencyProvider (maybe not the best name). The job of dependencyProvider is to:

a) create a dependency configuration in format required by Injector (we only use "factory" type for injector) b) "rename" dependencies - see config, "depB"... we are telling to injector that what is originally declared as "depA" in the constructor should actually be injected with "dbClient", using the Constructor.$inject syntax. We just did not like the "all or nothing" way the $inject works - you either don't use it or you must declare all the injectables in $inject. This proved to be error prone as often, if you don't use $inject as a rule, you tend to forget to add the dependency to $inject array when you add it to constructor. So, we are only replacing what we need to change, the rest is taken as declared in the constructor c) inherit the dependencies. The big project we have is separated into many sub-projects (lego pieces). We do not want to re-declare all the dependencies in the main project's dependency config (why should we in the first place, but of course we would be exposing dependencies which might be internal to the sub project unnecessarily). So, we can declare in the dep. config that it depends on other module. Each module exports its dependency config as well, so the dependency provider can combine those before providing the config to the injector. Of course, you can override the deps of sub module by the dependency config of the main module etc.

That's basically it.

Let me know if I can be of any further help. I am not sure how well I have been explaining it in English so again, if you wish, I can retry in Czech if you have time for a chat :-)

best

Michal

vojtajina commented 10 years ago

See inlined...

On Sat, May 17, 2014 at 11:27 AM, VilemP notifications@github.com wrote:

Hi,

sorry for the late reply.

Unfortunately the project I mentioned is not publicly available, but I will try to explain more details below. As for the dependency declaration - I do agree that most of the time you are just using default implementation. Where in my experience you might expect changes is data access, messaging etc. I also do agree that explicit declaration of dependencies makes it feeling less magic but unfortunately it also breaks the DRY principle. If I am using certain dependency 20 times, I have to declare it 20 times... So, one has to weight between these two I think.

If 20 components require the same "contract", then they have to declare it 20 times, I don't think there is any way around that. How would you avoid that?

Back to what we are doing. First, we did not want to have a notion of injector scattered across the project. Therefore, the app is initialized through app.js, which is, besides factories, the only place where we have an injector (this is also why using multiple injectors does not help in our case).

That's a good approach I think. Your code should not be coupled to "Injector". The only DI specific thing should be the annotations which is just additional metadata. Managing injectors should be done in some sort of "main" method/file (eg. app.js in your case), or when using Angular, Angular should take care of that and your code should be DI agnostic - you should be able to use the same code without DI.

So, you would have something like:

// app.js

var dependencyConfig = require ('./dependencyConfig'); var dependencyProvider = require('./dependencyProvider'); var Injector = require('di').Injector;

var deps = dependencyProvider.getDependencies(dependencyConfig);

var injector = new Injector( deps );

var server = injector.get("server");

....

All the other injections are done while instantiating the dependencies of "server" etc. (again, with the exception of factories, but there the injector is injected as well).

You can notice the "dependencyConfig". This is the place where we declare all the dependencies in one place. So whenever you need to change or add dependency, you know exactly where to go. Also, for testing, we might have a different dependency config which substitutes heavy dependencies, database access objects, messaging etc. with appropriate mocks.

Dependency config looks usually like this:

module.exports = {

"dbClient" : { type:"value", require: "mysqlClient" }, "depA" : { type: "type", implementation: function SomeConstructor() {} }, "depB" : { type:"factory", require: "./x.js", injectables: { "depA" : "dbClient" }

}

I don't like this ;-) I think declaration of dependencies should be next to each component - i.e. not in a single place. What implementations should be used is a different thing - that might be in a single place. Why do you like your approach? What are the advantages?

When a component changes a dependency requirement, it should be next to the component - why should you go to some "single place" where all the dependencies (for all components) are declared?

So, it lists the dependencies and provides more instructions for their creation. E.g. we may want to create the dependency by requiring existing module, by providing the actual value which should be injected etc.

I think your "dependencyConfig" is mixing two concerns: 1/ dependency declaration (each component should declare its deps, using tokens; a token decsribes "contract") 2/ dependency resolution (which concrete implementation should be used for each token - contract)

Using default providers basically enables you to get rid off the 2/, which is nice for most of the bindings where you only use a single implementation.

The config is passed to dependencyProvider (maybe not the best name). The job of dependencyProvider is to:

a) create a dependency configuration in format required by Injector (we only use "factory" type for injector)

This is something you don't need with DI.js.

b) "rename" dependencies - see config, "depB"... we are telling to injector that what is originally declared as "depA" in the constructor should actually be injected with "dbClient", using the Constructor.$inject syntax. We just did not like the "all or nothing" way the $inject works - you either don't use it or you must declare all the injectables in $inject. This proved to be error prone as often, if you don't use $inject as a rule, you tend to forget to add the dependency to $inject array when you add it to constructor. So, we are only replacing what we need to change, the rest is taken as declared in the constructor

This is a valid requirement IMHO. I think we will need this. There are some proposal, related to private modules. Eg. let's say you wanna load some third party component which uses DI to wires its sub-components internally, but it might "rename" some bindings (tokens).

c) inherit the dependencies. The big project we have is separated into many sub-projects (lego pieces). We do not want to re-declare all the dependencies in the main project's dependency config (why should we in the first place, but of course we would be exposing dependencies which might be internal to the sub project unnecessarily). So, we can declare in the dep. config that it depends on other module. Each module exports its dependency config as well, so the dependency provider can combine those before providing the config to the injector. Of course, you can override the deps of sub module by the dependency config of the main module etc.

I believe you can achieve the same thing just by loading additional "providers" when creating injector (as they can override bindings).

var i = new Injector([sharedProviders, subcomponentProviders]);

That's basically it.

Let me know if I can be of any further help. I am not sure how well I have been explaining it in English so again, if you wish, I can retry in Czech if you have time for a chat :-)

Let's keep the discussion here so that everybody can read it and join.

best

Michal

— Reply to this email directly or view it on GitHubhttps://github.com/angular/di.js/issues/41#issuecomment-43402254 .

VilemP commented 10 years ago

Hi,

in-lined as well:

If 20 components require the same "contract", then they have to declare it 20 times, I don't think there is any way around that. How would you avoid that?

Of course - declaring what KIND of component I want 20 times is fine. If they need it then there is no way around. What I am "attacking" is also declaring what the actual implementation of that "kind" is 20 times. I want to declare that only once and have the possibility to change it for 8 out of 20 without going to 8 files. That's what we are trying to achieve.

So, I would have 20 different constructors requiring a "token" or dependency named e.g. "dataProvider", like:

function SomeClass (dataProvider) {}

function SomeOtherClass(dataProvider) {}

etc.

I don't want to import a concrete implementation of dataProvider in each file. I rather have a single config file (as described previously) where I declare, what dataProvider actually should be.

And as long as I keep the contract (i.e. interface), I can also declare in that same config file, that from now, for these 8 modules the dataProvider is something else than for those other 12 modules. (Please remember we are now talking mostly about Node.js part of the project)

Your code should not be coupled to "Injector". The only DI specific thing

should be the annotations which is just additional metadata. Managing injectors should be done in some sort of "main" method/file (eg. app.js in your case), or when using Angular, Angular should take care of that and your code should be DI agnostic - you should be able to use the same code without DI.

Well, frankly speaking, I am not probably getting the idea of multiple injectors. The way we wanted it to be was : in main method, we instantiate the "root object".. the rest of the dependencies in the "tree" are instantiated automatically by the injector. As any level below the root object has no notion of the injector (with the exception of factories, but that's different story), how can I make use of 2 or more injectors? Only if I have two or more root objects, each using a different injector, right? But we haven't had a need for more than one root yet... ???

I think declaration of dependencies should be next to each component - i.e. not in a single place. What implementations should be used is a different thing - that might be in a single place.

And that's what I meant, sorry for the misunderstanding. What dependencies the module needs is of course defined in the module. How these are going to be satisfied with implementation is in single place, as this is not a business of the module to know what implementation it is going to get.

I think your "dependencyConfig" is mixing two concerns: 1/ dependency declaration (each component should declare its deps, using tokens; a token decsribes "contract")

As explained, we are not doing that.

2/ dependency resolution (which concrete implementation should be used for each token - contract)

Using default providers basically enables you to get rid off the 2/, which is nice for most of the bindings where you only use a single implementation.

Can you be more concrete on how we would achieve the same things with default providers?

a) create a dependency configuration in format required by Injector (we only use "factory" type for injector)

This is something you don't need with DI.js.

Well, yes, of course that is dependent on what implementation of DI we use :-) So far we used the old one which required certain way of configuration.... which did not suit our needs and so we have the "adapter"

c) inherit the dependencies. The big project we have is separated into many

sub-projects (lego pieces). We do not want to re-declare all the dependencies in the main project's dependency config (why should we in the first place, but of course we would be exposing dependencies which might be internal to the sub project unnecessarily). So, we can declare in the dep. config that it depends on other module. Each module exports its dependency config as well, so the dependency provider can combine those before providing the config to the injector. Of course, you can override the deps of sub module by the dependency config of the main module etc.

I believe you can achieve the same thing just by loading additional "providers" when creating injector (as they can override bindings).

var i = new Injector([sharedProviders, subcomponentProviders]);

Well, yes and no. In fact, this is what we do in tests to replace the actual deps with mocks where needed. With the app dependencies from sub projects, it's more complex. Let's say I have a subproject A. I have its own dependency config (as described), its own tests etc. The config has "require" statements which are resolved within the subproject as normal... all fine.

If I now use the same config in a project B because project B depends on A, the require statements from the config of A would be evaluated against current working directory which is obviously wrong. So, some extra work is required to make it all work as we wish.

Also, given that I declare the dependency implementations in the config, I would like to keep it the only place which really bothers about such declarations. So I do not want to define in my main method one part (that I have sub components) and the rest in config. I want all of it in one place, where you immediately see what is being used at any given moment.

Let's keep the discussion here so that everybody can read it and join.

sure... OK.

best

Michal

vojtajina commented 10 years ago

On Wed, May 21, 2014 at 9:51 AM, VilemP notifications@github.com wrote:

Hi,

in-lined as well:

If 20 components require the same "contract", then they have to declare it 20 times, I don't think there is any way around that. How would you avoid that?

Of course - declaring what KIND of component I want 20 times is fine. If they need it then there is no way around. What I am "attacking" is also declaring what the actual implementation of that "kind" is 20 times. I want to declare that only once and have the possibility to change it for 8 out of 20 without going to 8 files. That's what we are trying to achieve.

So, I would have 20 different constructors requiring a "token" or dependency named e.g. "dataProvider", like:

function SomeClass (dataProvider) {}

function SomeOtherClass(dataProvider) {}

etc.

I don't want to import a concrete implementation of dataProvider in each file. I rather have a single config file (as described previously) where I declare, what dataProvider actually should be.

And as long as I keep the contract (i.e. interface), I can also declare in that same config file, that from now, for these 8 modules the dataProvider is something else than for those other 12 modules. (Please remember we are now talking mostly about Node.js part of the project)

Yes - you define the contract 20 times, but change the implementation in one place, once.

@Inject(SomeToken) Think about SomeToken as an interface - something that describes a contract. The fact that we use it as the default implementation is just to make things easier.

Your code should not be coupled to "Injector". The only DI specific thing

should be the annotations which is just additional metadata. Managing injectors should be done in some sort of "main" method/file (eg. app.js in your case), or when using Angular, Angular should take care of that and your code should be DI agnostic - you should be able to use the same code without DI.

Well, frankly speaking, I am not probably getting the idea of multiple injectors. The way we wanted it to be was : in main method, we instantiate the "root object".. the rest of the dependencies in the "tree" are instantiated automatically by the injector. As any level below the root object has no notion of the injector (with the exception of factories, but that's different story), how can I make use of 2 or more injectors? Only if I have two or more root objects, each using a different injector, right? But we haven't had a need for more than one root yet... ???

Again, what you are describing sounds good to me. It's possible you don't need multiple injectors.

Injector encapsulates a life scope. For instance, think of a web server - there could be two basic life scopes; "application" and "request". With this DI, you would have a root injector (representing the "application" scope) and for each incoming request you (or some framework you are using) would create a new injector - a child of the application injector. This child injector can use any instance from the parent but it can also override bindings and force new instances. Thus you can have multiple instances for the same token. Eg. let's say something asks for @Inject(RequestPath) - this value is different for each request and so you need a different instance.

I think declaration of dependencies should be next to each component - i.e. not in a single place. What implementations should be used is a different thing - that might be in a single place.

And that's what I meant, sorry for the misunderstanding. What dependencies the module needs is of course defined in the module. How these are going to be satisfied with implementation is in single place, as this is not a business of the module to know what implementation it is going to get.

I think your "dependencyConfig" is mixing two concerns: 1/ dependency declaration (each component should declare its deps, using tokens; a token decsribes "contract")

As explained, we are not doing that.

2/ dependency resolution (which concrete implementation should be used for each token - contract)

Using default providers basically enables you to get rid off the 2/, which is nice for most of the bindings where you only use a single implementation.

Can you be more concrete on how we would achieve the same things with default providers?

Not sure I understand.

Default provider is when the token itself is used to construct the instance. Check out http://pastebin.com/mTpaAqbs

a) create a dependency configuration in format required by Injector (we only use "factory" type for injector)

This is something you don't need with DI.js.

Well, yes, of course that is dependent on what implementation of DI we use :-) So far we used the old one which required certain way of configuration.... which did not suit our needs and so we have the "adapter"

c) inherit the dependencies. The big project we have is separated into many

sub-projects (lego pieces). We do not want to re-declare all the dependencies in the main project's dependency config (why should we in the first place, but of course we would be exposing dependencies which might be internal to the sub project unnecessarily). So, we can declare in the dep. config that it depends on other module. Each module exports its dependency config as well, so the dependency provider can combine those before providing the config to the injector. Of course, you can override the deps of sub module by the dependency config of the main module etc.

I believe you can achieve the same thing just by loading additional "providers" when creating injector (as they can override bindings).

var i = new Injector([sharedProviders, subcomponentProviders]);

Well, yes and no. In fact, this is what we do in tests to replace the actual deps with mocks where needed. With the app dependencies from sub projects, it's more complex. Let's say I have a subproject A. I have its own dependency config (as described), its own tests etc. The config has "require" statements which are resolved within the subproject as normal... all fine.

Actually, I think I didn't understand your original question properly. What you were asking makes sense now - I think you want

If I now use the same config in a project B because project B depends on A, the require statements from the config of A would be evaluated against current working directory which is obviously wrong. So, some extra work is required to make it all work as we wish.

I don't understand this. What extra work do you need?

Inside B, you import a token from A and annotate something in B with this token. This require() statement is just a regular Node's require - nothing to do with DI.

I think it might be nice to have "private modules" to make this better encapsulated. Then, each "submodule" could have internal bindings that are not visible to other modules - only those explicitly exported. It would also be possible for a submodule to override its internal bindings. You might simulate this by creating an injector per submodule. However I think if you use references to functions as tokens (instead of strings), this is not as big issue as you won't get into name conflicts.

Also, given that I declare the dependency implementations in the config, I would like to keep it the only place which really bothers about such declarations. So I do not want to define in my main method one part (that I have sub components) and the rest in config. I want all of it in one place, where you immediately see what is being used at any given moment.

Shoot me an email, maybe you can show me your project to help me understand these problems better. Of course, it would be better if you could separate these problems into a simple gist or something so that it's public and other people can see it too...

Let's keep the discussion here so that everybody can read it and join.

sure... OK.

best

Michal

— Reply to this email directly or view it on GitHub https://github.com/angular/di.js/issues/41#issuecomment-43723619.

larsno commented 9 years ago

One possibility would be to be able to optionally add additional information to the provider annotation specifying which modules this implementation is for. Thus if you only wanted to swap out 8 of the instances of a particular provider, you would annotate the new provider with information indicating it is only to be used for those 8 modules.