docs(dependency injection): minor tweaks

closes #755
This commit is contained in:
Ward Bell 2016-01-26 02:00:24 -08:00
parent 12d4e17f94
commit ab6d362650
3 changed files with 75 additions and 56 deletions

View File

@ -7,7 +7,9 @@ import {Logger} from '../logger.service';
@Injectable() @Injectable()
export class HeroService { export class HeroService {
//#docregion ctor
constructor(private _logger: Logger) { } constructor(private _logger: Logger) { }
//#enddocregion ctor
getHeroes() { getHeroes() {
this._logger.log('Getting heroes ...') this._logger.log('Getting heroes ...')

View File

@ -1,4 +1,5 @@
// Examples of provider arrays // Examples of provider arrays
//#docplaster
import { Component, Host, Inject, Injectable, import { Component, Host, Inject, Injectable,
provide, Provider} from 'angular2/core'; provide, Provider} from 'angular2/core';
@ -103,10 +104,8 @@ class EvenBetterLogger {
template: template, template: template,
providers: providers:
//#docregion providers-5 //#docregion providers-5
[ [ UserService,
UserService, provide(Logger, {useClass: EvenBetterLogger}) ]
provide(Logger, {useClass: EvenBetterLogger})
]
//#enddocregion providers-5 //#enddocregion providers-5
}) })
export class ProviderComponent5 { export class ProviderComponent5 {
@ -131,11 +130,9 @@ class OldLogger {
template: template, template: template,
providers: providers:
//#docregion providers-6a //#docregion providers-6a
[ [ NewLogger,
NewLogger,
// Not aliased! Creates two instances of `NewLogger` // Not aliased! Creates two instances of `NewLogger`
provide(OldLogger, {useClass:NewLogger}) provide(OldLogger, {useClass:NewLogger}) ]
]
//#enddocregion providers-6a //#enddocregion providers-6a
}) })
export class ProviderComponent6a { export class ProviderComponent6a {
@ -156,11 +153,9 @@ export class ProviderComponent6a {
template: template, template: template,
providers: providers:
//#docregion providers-6b //#docregion providers-6b
[ [ NewLogger,
NewLogger,
// Alias OldLogger w/ reference to NewLogger // Alias OldLogger w/ reference to NewLogger
provide(OldLogger, {useExisting: NewLogger}) provide(OldLogger, {useExisting: NewLogger}) ]
]
//#enddocregion providers-6b //#enddocregion providers-6b
}) })
export class ProviderComponent6b { export class ProviderComponent6b {
@ -306,7 +301,9 @@ export class ProviderComponent10b {
log: (msg:string)=> this._logger.logs.push(msg), log: (msg:string)=> this._logger.logs.push(msg),
logs: [] logs: []
} }
// #enddocregion provider-10-logger
this._logger.log("Optional logger was not available.") this._logger.log("Optional logger was not available.")
// #docregion provider-10-logger
} }
// #enddocregion provider-10-logger // #enddocregion provider-10-logger
else { else {

View File

@ -399,38 +399,41 @@ include ../../../../_includes/_util-fns
We're likely to need the same logger service everywhere in our application We're likely to need the same logger service everywhere in our application
so we put it at the root level of the application in the `app/` folder and so we put it at the root level of the application in the `app/` folder and
we register it in the `providers` array of the metadata for our application root component, `AppComponent`. we register it in the `providers` array of the metadata for our application root component, `AppComponent`.
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-1', 'app/app.component.ts (providers)') +makeExample('dependency-injection/ts/app/providers.component.ts','providers-logger', 'app/app.component.ts (providers)')
:marked :marked
If we forget to register it, Angular will throw an exception when it first needs the logger: If we forget to register it, Angular throws an exception when it first looks for the logger:
code-example(format). code-example(format).
EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger) EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)
:marked :marked
That's telling us that the dependency injector couldn't find the *provider* for the logger That's Angular telling us that the dependency injector couldn't find the *provider* for the logger.
when it first tried to call the logger — inside the `HeroService` when the `HeroListComponent` It needed that provider to create a `Logger` to inject into a new `HeroService` which it needed to
was calling for heroes. create and inject into a new `HeroListComponent`.
The *provider* is the subject of our next section.
The chain of creations started with the `Logger` provider. The *provider* is the subject of our next section.
But wait! What if the logger is optional? But wait! What if the logger is optional?
<a id="optional"></a> <a id="optional"></a>
### Optional dependencies ### Optional dependencies
Our `HeroService` currently requires a `Logger`. What if we could get by without a logger? Our `HeroService` currently requires a `Logger`. What if we could get by without a logger?
We'd use it if we had it, ignore it if we didn't. We'd use it if we had it, ignore it if we didn't. We can do that.
First we import the `@Optional` decorator. First import the `@Optional()` decorator.
+makeExample('dependency-injection/ts/app/providers.component.ts','import-optional')(format='.') +makeExample('dependency-injection/ts/app/providers.component.ts','import-optional')(format='.')
:marked :marked
Then rewrite the constructor with that decorator to make the logger optional. Then rewrite the constructor with `@Optional()` decorator preceding the private `_logger` parameter.
That tells the injector that `_logger` is optional.
+makeExample('dependency-injection/ts/app/providers.component.ts','provider-10-ctor')(format='.') +makeExample('dependency-injection/ts/app/providers.component.ts','provider-10-ctor')(format='.')
:marked :marked
Be prepared for a null logger. If we don't register one somewhere up the line, Be prepared for a null logger. If we don't register one somewhere up the line,
the injector will inject `null`. We have a method that logs. What can we do? the injector will inject `null`. We have a method that logs.
What can we do to avoid a null reference exception?
We could substitute a *do-nothing* logger stub so that our methods continue to work: We could substitute a *do-nothing* logger stub so that calling methods continue to work:
+makeExample('dependency-injection/ts/app/providers.component.ts','provider-10-logger')(format='.') +makeExample('dependency-injection/ts/app/providers.component.ts','provider-10-logger')(format='.')
:marked :marked
Obviously we'd take a more sophisticated approach if the logger were optional Obviously we'd take a more sophisticated approach if the logger were optional
elsewhere as well. in multiple locations.
But enough about optional loggers. In our sample application, the `Logger` is required. But enough about optional loggers. In our sample application, the `Logger` is required.
We must register a `Logger` with the application injector using *providers* We must register a `Logger` with the application injector using *providers*
@ -445,18 +448,15 @@ code-example(format).
The injector relies on **providers** to create instances of the services The injector relies on **providers** to create instances of the services
that it injects into components and other services. that it injects into components and other services.
We must register *providers* with the injector or it won't know what to do. We must register a service *provider* with the injector or it won't know how to create the service.
Earlier we registered the `Logger` service in the `providers` array of the metadata for the `AppComponent` like this: Earlier we registered the `Logger` service in the `providers` array of the metadata for the `AppComponent` like this:
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-logger') +makeExample('dependency-injection/ts/app/providers.component.ts','providers-logger')
:marked :marked
The `providers` array appears to hold service classes (one service class in this example). The `providers` array appears to hold a service class.
In reality it holds instances of the [Provider](../api/core/Provider-class.html) class. In reality it holds an instance the [Provider](../api/core/Provider-class.html) class that can create that service.
In our example, when the `HeroService` constructor specifies `logger:Logger`, it's expecting There are many ways to *provide* something that looks and behaves like a `Logger`.
something that has the shape and behavior of the `Logger` class.
There are many ways to *provide* something that has the shape and behavior of a `Logger`.
The `Logger` class itself is an obvious and natural provider - it has the right shape and it's designed to be created. The `Logger` class itself is an obvious and natural provider - it has the right shape and it's designed to be created.
But it's not the only way. But it's not the only way.
@ -511,7 +511,7 @@ code-example(format).
### Aliased Class Providers ### Aliased Class Providers
Suppose there is an old component that depends upon an `OldLogger` class. Suppose there is an old component that depends upon an `OldLogger` class.
`OldLogger` has the same interface as the `NewLogger` but, for some reason, `OldLogger` has the same interface as the `NewLogger` but for some reason
we can't update the old component to use it. we can't update the old component to use it.
When the *old* component logs a message with `OldLogger`, When the *old* component logs a message with `OldLogger`,
@ -545,14 +545,15 @@ code-example(format).
Sometimes we need to create the dependent value dynamically, Sometimes we need to create the dependent value dynamically,
based on information we won't have until the last possible moment. based on information we won't have until the last possible moment.
Maybe the information can change. Maybe the information keeps changes repeatedly in the course of the browser session..
Suppose also that the injectable service has no independent access to the source of this information. Suppose also that the injectable service has no independent access to the source of this information.
This situation calls for a **factory provider** as we illustrate next. This situation calls for a **factory provider**.
Our HeroService should hide *secret* heroes from normal users. Let's illustrate by adding a new business requirement:
Only authorized users should see them. the HeroService must hide *secret* heroes from normal users.
Only authorized users should see secret heroes.
Like the `EvenBetterLogger`, the `HeroService` needs a fact about the user. Like the `EvenBetterLogger`, the `HeroService` needs a fact about the user.
It needs to know if the user is authorized to see secret heroes. It needs to know if the user is authorized to see secret heroes.
@ -560,6 +561,8 @@ code-example(format).
as when we log in a different user. as when we log in a different user.
Unlike `EvenBetterLogger`, we can't inject the `UserService` into the `HeroService`. Unlike `EvenBetterLogger`, we can't inject the `UserService` into the `HeroService`.
The `HeroService` won't have direct access to the user information to decide
who is authoriazed and who is not.
.l-sub-section .l-sub-section
:marked :marked
Why? We don't know either. Stuff like this happens. Why? We don't know either. Stuff like this happens.
@ -573,9 +576,9 @@ code-example(format).
A factory provider needs a factory function: A factory provider needs a factory function:
+makeExample('dependency-injection/ts/app/heroes/hero.service.provider.ts','factory', 'app/heroes/hero.service.provider.ts (factory)')(format='.') +makeExample('dependency-injection/ts/app/heroes/hero.service.provider.ts','factory', 'app/heroes/hero.service.provider.ts (factory)')(format='.')
:marked :marked
Although our `HeroService` knows nothing about the `UserService`, our factory function does. Although the `HeroService` has no access to the `UserService`, our factory function does.
We inject both the `Logger` and the `UserService` into the factory provider: We inject both the `Logger` and the `UserService` into the factory provider and let the injector pass them along to the factory function:
+makeExample('dependency-injection/ts/app/heroes/hero.service.provider.ts','provider', 'app/heroes/hero.service.provider.ts (provider)')(format='.') +makeExample('dependency-injection/ts/app/heroes/hero.service.provider.ts','provider', 'app/heroes/hero.service.provider.ts (provider)')(format='.')
:marked :marked
@ -586,12 +589,15 @@ code-example(format).
The `deps` property is an array of [provider tokens](#token). The `deps` property is an array of [provider tokens](#token).
The `Logger` and `UserService` classes serve as tokens for their own class providers. The `Logger` and `UserService` classes serve as tokens for their own class providers.
The injector resolves these tokens and injects the corresponding services into the matching factory function parameters.
:marked :marked
Notice that we've captured the factory provider in an exported variable, `heroServiceProvider`. Notice that we captured the factory provider in an exported variable, `heroServiceProvider`.
This extra step makes it easier for us to register our `HeroService` whereever we need it. This extra step makes the factory provider re-usable.
We can register our `HeroService` with this variable whereever we need it.
In our sample, we need it only in the `HeroesComponent` In our sample, we need it only in the `HeroesComponent`
where it replaces the previous `HeroService` registration in the metadata `providers` array: where it replaces the previous `HeroService` registration in the metadata `providers` array.
Here we see the new and the old implementation side-by-side:
+makeTabs( +makeTabs(
`dependency-injection/ts/app/heroes/heroes.component.ts, `dependency-injection/ts/app/heroes/heroes.component.ts,
dependency-injection/ts/app/heroes/heroes.component.1.ts`, dependency-injection/ts/app/heroes/heroes.component.1.ts`,
@ -606,11 +612,11 @@ code-example(format).
When we register a provider with an injector we associate that provider with a dependency injection token. When we register a provider with an injector we associate that provider with a dependency injection token.
The injector maintains an internal *token/provider* map that it references when The injector maintains an internal *token/provider* map that it references when
asked for a dependency asked for a dependency. The token is the key to the map.
In all previous examples, the dependency value has been a class *instance* and In all previous examples, the dependency value has been a class *instance* and
the class *type* served as its own lookup token. the class *type* served as its own lookup key.
Here we get a `HeroService` directly from the injector by supplying the `HeroService` type as the token. Here we get a `HeroService` directly from the injector by supplying the `HeroService` type as the key/token.
+makeExample('dependency-injection/ts/app/injector.component.ts','get-hero-service')(format='.') +makeExample('dependency-injection/ts/app/injector.component.ts','get-hero-service')(format='.')
:marked :marked
We have similar good fortune (in typescript) when we write a constructor that requires an injected class-based dependency. We have similar good fortune (in typescript) when we write a constructor that requires an injected class-based dependency.
@ -618,7 +624,7 @@ code-example(format).
service associated with that `HeroService` class token: service associated with that `HeroService` class token:
+makeExample('dependency-injection/ts/app/providers.component.ts','provider-8-ctor')(format=".") +makeExample('dependency-injection/ts/app/providers.component.ts','provider-8-ctor')(format=".")
:marked :marked
This is all especially convenient when we consider that most dependency values are provided by classes. This is especially convenient when we consider that most dependency values are provided by classes.
### Non-class Dependencies ### Non-class Dependencies
@ -626,7 +632,7 @@ code-example(format).
Sometimes the thing we want to inject is a string, a function, or an object. Sometimes the thing we want to inject is a string, a function, or an object.
Applications often define configuration objects with lots of small facts like the title of the application or the address of a web api endpoint. Applications often define configuration objects with lots of small facts like the title of the application or the address of a web api endpoint.
These configuration objects aren't always instances of a class. They're just objects ... like this one: These configuration objects aren't always instances of a class. They tend to be object hashes like this one:
+makeExample('dependency-injection/ts/app/app.config.ts','config','app/app-config.ts')(format='.') +makeExample('dependency-injection/ts/app/app.config.ts','config','app/app-config.ts')(format='.')
:marked :marked
@ -638,13 +644,19 @@ code-example(format).
// begin Typescript only // begin Typescript only
<a id="interface"></a> <a id="interface"></a>
:marked :marked
### Interfaces aren't valid tokens
The `CONFIG` constant has an interface, `Config`. Unfortunately, we The `CONFIG` constant has an interface, `Config`. Unfortunately, we
**cannot use an interface as a token** cannot use an interface as a token:
+makeExample('dependency-injection/ts/app/providers.component.ts','providers-9a-interface')(format=".") +makeExample('dependency-injection/ts/app/providers.component.ts','providers-9a-interface')(format=".")
+makeExample('dependency-injection/ts/app/providers.component.ts','provider-9a-ctor-interface')(format=".") +makeExample('dependency-injection/ts/app/providers.component.ts','provider-9a-ctor-interface')(format=".")
:marked :marked
It's not Angular's fault. An interface is a TypeScript design-time artifact. That seems strange if we're used to dependency injection in strongly-typed languages where
It disappears from the generated JavaScript so there is no interface type information for Angular to find at runtime. an interface is the preferred dependency lookup key.
It's not Angular's fault. An interface is a TypeScript design-time artifact. JavaScript doesn't have interfaces.
The TypeScript interface disappears from the generated JavaScript.
There is no interface type information left for Angular to find at runtime.
// end Typescript only // end Typescript only
<a id="string-token"></a> <a id="string-token"></a>
:marked :marked
@ -684,7 +696,9 @@ code-example(format).
.l-sub-section .l-sub-section
:marked :marked
Angular uses `OpaqueTokens` to register all of its non-class dependencies. Angular itself uses `OpaqueTokens` to register all of its own non-class dependencies. For example,
[HTTP_PROVIDERS](https://angular.io/docs/ts/latest/api/http/HTTP_PROVIDERS-let.html)
is the `OpaqueToken` associated with an array of providers for persisting data with the Angular `Http` client.
Internally, the `Provider` turns both the string and the class type into an `OpaqueToken` Internally, the `Provider` turns both the string and the class type into an `OpaqueToken`
and keys its *token/provider* map with that. and keys its *token/provider* map with that.
@ -709,8 +723,13 @@ code-example(format).
+makeExample('dependency-injection/ts/app/injector.component.ts', 'injector', +makeExample('dependency-injection/ts/app/injector.component.ts', 'injector',
'app/injector.component.ts') 'app/injector.component.ts')
:marked :marked
Angular injects the component's own `Injector` which the component uses to acquire services. The `Injector` is itself an injectable service.
The services themselves are not injected. They're retrieved via the injector.
In this example, Angular injects the component's own `Injector` into the component's constructor.
The component then asks the injected injector for the services it wants.
Note that the services themselves are not injected into the component.
They are retrieved by calling `injector.get`.
The `get` method throws an error if it can't resolve the requested service. The `get` method throws an error if it can't resolve the requested service.
We can call `getOptional` instead, which we do in one case We can call `getOptional` instead, which we do in one case
@ -718,14 +737,15 @@ code-example(format).
.l-sub-section .l-sub-section
:marked :marked
This technique is an example of the [Service Locator Pattern](https://en.wikipedia.org/wiki/Service_locator_pattern). The technique we just described is an example of the
[Service Locator Pattern](https://en.wikipedia.org/wiki/Service_locator_pattern).
We **avoid** this technique unless we genuinely need it. We **avoid** this technique unless we genuinely need it.
It encourages a careless grab-bag approach such as we see here. It encourages a careless grab-bag approach such as we see here.
It's difficult to explain, understand, and test. It's difficult to explain, understand, and test.
We can't know by inspecting the constructor what this class requires or what it will do. We can't know by inspecting the constructor what this class requires or what it will do.
It could acquire services from any ancestor component, not just its own. It could acquire services from any ancestor component, not just its own.
We're forced to spelunk the implementation and hope for the best. We're forced to spelunk the implementation to discover what it does.
Framework developers may take this approach when they Framework developers may take this approach when they
must acquire services generically and dynamically. must acquire services generically and dynamically.