Experimenting With Class Delegates And Factories In Angular 2 Beta 3
With AngularJS 1.x, we had dependency-injection (DI). In Angular 2, we also have dependency-injection; but, it's really a whole new beast with parent-child injectors, token-based class association, multi-resolutions, and no application life-cycle hooks. And, since the concept of "Angular" is being reduced within the application, at large, there's now a lot more flexibility (ie, decisions that YOU have to make) on how one class knows about another class. As such, I wanted to try and explore the new dependency-injection mechanics in the context of something that was relatively straightforward in AngularJS 1.x - class delegation (or "decoration" - not sure what the right term is).
Run this demo in my JavaScript Demos project on GitHub.
As you read this, bear in mind that Angular 2 and AngularJS 1.x are different. And, trying to shoehorn 1.x ideas into 2.x may not make any sense. In fact, just the mention of it was able to make Pascal Precht get a little nauseous.
But, even it's undesirable to think in these terms, I believe that sometimes trying to implement bad ideas is a great way to learn about how things are put together. So, caveats and warnings aside, I wanted to look at how you might decorate a class in an Angular 2 application.
What I mean when I say "decorate" is that I want to take a class that has already been instantiated by the dependency-injection framework and decorate it such that any other class that requires this class will actually get the "decorated" class. In AngularJS 1.x, this was a rather trivial idea using the .decorator() module method or the .provider() module method.
To explore this workflow, I want to create a demo in which I have a Greeter class that has a single method: sayHello(name). I then want my application to require a Greeter injectable. But, I want that Greeter instance to be a decorated version of the core Greeter class. Without the .decorator() method from AngularJS 1.x, the first thing I tried to was something like this:
function MyGreeter( greeter: Greeter ) { ... }
... which sounds right until you go provide the MyGreeter class:
provide( Greeter, { useClass: MyGreeter } )
... and blamo, you have circular dependency - MyGreeter requires Greeter, which is defined as MyGreeter... which requires Greeter ... and so on.
The next thing I tried to do was use a factory to create my decorated class:
provide( Greeter, { useFactory: function() { ... }, deps: [ Greeter ] } )
... but no luck as this leads us right back to the circular dependency.
Now, you could use a factory and then manually instantiate the core Greeter class inside the factory. But, in order to do that, I would have to know about all of the core Greeter dependencies and be able to provide them explicitly. I don't want to do that - I don't want to be responsible for instantiating the core class; I want the dependency injection framework to do its job - I just want to hook into that object and add some of my own funk (so to speak).
But, on that topic, factories are really an interesting concept because they give us two means to instantiate an object: explicitly through the "new" operator and implicitly through the dependency-injection framework. At this point, it might be tempting to create classes that relied on being explicitly "new'd". But, I think that would be a mistake. If you're building an AngularJS class, I think it should be "Angularized" (ie, have the appropriate meta-data) and not be aware of how it might be used.
The solution that I finally came up with was a combination of the two concepts above plus the idea of creating a "delegate token." In the Angular dependency-injection framework, every class is instantiated and injected based on a token. Often times, this token is the class constructor reference; but, it doesn't have to be. It could be some other string or OpaqueToken instance. As long as two classes agree on what the token is, the mechanics work out.
So, what if I create a new "delegate token" for the core Greeter class:
provide( delegate( Greeter ), { useClass: Greeter } )
If I do this, then I can still get Angular to instantiate the core Greeter class while still using it as a dependency in the provider chain:
provide( Greeter, { useFactory: function() { ... }, deps: [ delegate( Greeter ) ] } )
Here, I no longer have a circular-dependency because Greeter is no longer depending on Greeter - it's depending on delegate(Greeter), which is a totally different token.
What I finally came up with is a Greeter class that is wrapped by another class - FriendlyGreeter - and then mutated by a second class - FlirtyGreeter. I'm using two different types of "decoration" to showcase that both are possible, depending on what makes you uncomfortable.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Experimenting With Class Delegates And Factories In Angular 2 Beta 3
</title>
</head>
<body>
<h1>
Experimenting With Class Delegates And Factories In Angular 2 Beta 3
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-polyfills.min.js"></script>
<!-- CAUTION: Some features do not work with the minified UMD code. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ "AppComponent", "Greeter", "FriendlyGreeter", "FlirtyGreeter", "delegate" ],
function run( AppComponent, Greeter, FriendlyGreeter, FlirtyGreeter, delegate ) {
ng.platform.browser.bootstrap(
AppComponent,
[
// For the "core" Greeter, I don't want to instantiate it manually
// since I don't "own" it. Instead, I want Angular to handle the
// instantiation including the dependency-injection. BUT, I can't
// call it "Greeter" otherwise I'll get a circular dependency. So,
// instead, I have to call it a "delegate" Greeter, which is what
// the factory will have to depend on.
ng.core.provide(
delegate( Greeter ),
{
useClass: Greeter
}
),
// Notice that this factory function for "Greeter" depends on the
// "delegate" Greeter. This will get Angular to instantiate the
// core Greeter and pass it into the factory where we can then
// proceed to overwrite the provider for Greeter.
ng.core.provide(
Greeter,
{
useFactory: function( greeter ) {
// return( greeter );
// return( new FriendlyGreeter( greeter ) );
return( new FlirtyGreeter( new FriendlyGreeter( greeter ) ) );
},
deps: [ delegate( Greeter ) ]
}
),
// Providing these classes, for general use.
// Greeter, // -- To revert back to the non-delegate class.
FriendlyGreeter,
FlirtyGreeter
]
);
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a method that creates delegate tokens using the given class
// reference. This can be used so that coordinating classes only have to agree
// on the core class token, not the delegate token.
define(
"delegate",
function registerDelegate() {
// I hold the collection of delegate token associations.
var delegates = [];
return( delegate );
// I return the delegate token associated with the given class. If one
// does not exist, it is created and returned.
function delegate( coreClass ) {
// If we have an existing token for the given class, just return
// the existing token.
for ( var i = 0, length = delegates.length ; i < length ; i++ ) {
if ( delegates[ i ].coreClass === coreClass ) {
return( delegates[ i ].opaqueToken );
}
}
// If we made it this far, this is the first time that a delegate
// token is trying to be registered for this class. Let's create
// a new delegate entry and return the new token.
var newToken = new ng.core.OpaqueToken( coreClass.toString() );
delegates.push({
coreClass: coreClass,
opaqueToken: newToken
});
return( newToken );
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"AppComponent",
function registerAppComponent() {
var Greeter = require( "Greeter" );
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
template:
`
<strong>Greeting</strong>: {{ greeting }}
`
})
.Class({
constructor: AppController
})
;
AppController.parameters = [ new ng.core.Inject( Greeter ) ];
return( AppController );
// I control the App component.
function AppController( greeter ) {
var vm = this;
// Prepare the greeting for the view. At this point, greeter may be
// an instance of Greeter, an instance of FriendlyGreeter, or an
// instance of FlirtyGreeter - we don't have to care since they all
// use the same API. The decision was made by the registered providers.
vm.greeting = greeter.sayHello( "Sarah" );
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a greeter that can say hello.
define(
"Greeter",
function registerGreeter() {
return( Greeter );
// I provide an API for greeting people.
function Greeter() {
// Return the public API.
return({
sayHello: sayHello
});
// ---
// PUBLIC METHODS.
// ---
// I return a greeting for the given name.
function sayHello( name ) {
return( "Hello " + name + "." );
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a friendly greeter that builds on top of the Greeter.
// --
// NOTE: This class does not have any idea that it is being used as part of a
// class delegation workflow - it only knows that it depends on Greeter being
// in the provider chain in the case of dependency-injection.
define(
"FriendlyGreeter",
function registerFriendlyGreeter() {
var Greeter = require( "Greeter" );
FriendlyGreeter.parameters = [ new ng.core.Inject( Greeter ) ];
return( FriendlyGreeter );
// I provide an API for greeting people.
function FriendlyGreeter( greeter ) {
// Return the public API.
return({
sayHello: sayHello
});
// ---
// PUBLIC METHODS.
// ---
// I return a greeting for the given name.
function sayHello( name ) {
return( greeter.sayHello( name ) + " I hope you are well." );
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a flirty greeter that builds on top of the Greeter.
// --
// NOTE: This class does not have any idea that it is being used as part of a
// class delegation workflow - it only knows that it depends on Greeter being
// in the provider chain in the case of dependency-injection.
define(
"FlirtyGreeter",
function registerFlirtyGreeter() {
var Greeter = require( "Greeter" );
FlirtyGreeter.parameters = [ new ng.core.Inject( Greeter ) ];
return( FlirtyGreeter );
// I provide an API for greeting people.
function FlirtyGreeter( greeter ) {
var coreSayHello = greeter.sayHello;
// In this version of the decorator, we are actually altering the
// core functionality of the greeter delegate.
// --
// NOTE: This is more like the $delegate usage you might see in
// AngularJS 1.x.
greeter.sayHello = function( name ) {
var coreResult = coreSayHello.call( greeter, name );
return( coreResult + " You sure look lovely this morning." );
};
// Return the ORIGINAL greeter as the "flirty" greeter. In JavaScript,
// you can return anything from a constructor function and, generally
// speaking, return a totally new object instance reference.
return( greeter );
}
}
);
</script>
</body>
</html>
As you can see, the FriendlyGreeter is acting as a proxy to Greeter while FlirtyGreeter is actually mutating the underlying Greeter instance. Regardless of the approach, the Greeter instance, injected into the Application component, now exhibits the behaviors of both decorators on top of the core Greeter class:
Things I like about this approach:
- The Application component only knows about the core Greeter token.
- The core Greeter class is being instantiated by the dependency-injection framework (not by me explicitly).
- The FriendlyGreeter and FlirtyGreeter both know that they rely on the Greeter interface, but do not have any insight into how things are being wired together.
What I don't like about this approach is that I have to explicitly "new" my FlirtyGreeter and FriendlyGreeter classes. But, I think that this discomfort is simply me adjusting to the new flexibility in Angular 2. In AngularJS 1.x, I didn't really have that option (not without violating other best practices). But, in an Angular 2 context, "new" is not as dirty a word as it used to be. The trick, I think, is finding the right balance of implicit and explicit instantiation while still leaning heavily on the provider chain.
Even if you think this idea of class delegation / decoration is insane, it definitely made me think deeply about how dependency-injection works in Angular 2. I think, perhaps, that I could have also accomplished something similar with a parent-child injector. But, I didn't want my component tree to have to know about it. That approach could be a fun exploration for another blog post.
Want to use code from this post? Check out the license.
Reader Comments
This is a great article because I also ran into the same issue of circular dependency I wish you would just write in typescript it would be a lot easier to follow
can you explain how is it when using a proxy, fixes the cyclic dependency?
Wouldn't that cyclic dependency still be there just through the proxy?
tx Sean
@Sean,
Most definitely. I think this would be easiest to convey with a graphic. Maybe I can get something up tomorrow.
@Sean,
It's definitely a messy, complicated topic - but I tried my best to explain it with some diagrams:
www.bennadel.com/blog/3027-exploring-dependency-injection-tokens-using-pictures-in-angular-2.htm
Good luck ;)
tx as always for the feedback...
Sean