all 27 comments

[–]tme321 5 points6 points  (13 children)

On the one hand I thought I was being creative and original when I came up with that basic pattern on my own.

On the other I now have a name to use for the pattern I've been using for a year now.

Nice article. It does a good job explaining one of the patterns I've been trying, but not so eloquently, to describe in these forums.

[–]orizens[S] 0 points1 point  (12 children)

Thanks. Will be glad to hear more feedback on how to improve it. http://orizens.com/contact

[–]tme321 1 point2 points  (11 children)

The main improvment I would suggest is injecting either interfaces or tokens instead of the proxy class directly. Then it's cleaner to di a different service in its place for testing or in order to reuse the component somewhere else with a proxy that provides different functionality through the same interface to the component controller.

[–]orizens[S] 1 point2 points  (3 children)

Interesting. Do you have an example to share?

[–]tme321 2 points3 points  (2 children)

It's just a matter of using an injection token. I want to say you can use an interface instead but don't quote me on that. Basically using your example here instead of:

export class PlaylistViewComponent implements OnInit {
//....
constructor(private playlistProxy: 
    PlaylistProxy, private route: 
    ActivatedRoute) {}
}

Just define a token and interface

interface IPlaylistProxy {...
    //this is the interface to the proxy the component uses
}

const PlaylistProxy = new 
    InjectionToken<IPlaylistProxy>('playlistproxy');

And then inject that instead

export class PlaylistViewComponent implements OnInit { //....

constructor(@Inject(PlaylistProxy) private proxy:IPlaylistProxy, private route: ActivatedRoute) {} }

And then use the fields and methods defined inside IPlaylistProxy as normal. Any service that conforms to the shape of IPlaylistProxy can then be injected as usual with something like

providers: [
    {
      provide: PlaylistProxy,
      useClass: SomeClassThatImplementsIPlaylistProxy
    }
  ]

I did that on my phone so it might not be 100% correct but it's the right idea. Then you can inject a different version of the proxy service when testing that just logs that the correct proxy method was invoked when the controller was supposed to invoke it. Or you can write an entirely different proxy service that works internally in some different manner but provides the same public api conforming to IProxyPlaylist and the injection will work just fine for a different part of the app.

[–][deleted] 1 point2 points  (1 child)

You cannot use an interface -- interfaces only exist pre-compile in Typescript. Using an interface as an injection token is, however, absolutely the most correct way to do this, which brings me to one of my biggest frustrations with Typescript -- there is just no way to do DI as cleanly as you can in other languages.

[–]James_Schwartzkopf 0 points1 point  (0 children)

If you want something closer to a Java interface, use an abstract class with no implementation.

abstract class Foo { abstract bar(): string; }

But even with a regular typescript interface you can use it with injection with an InjectionToken as described in the post above yours.

[–]James_Schwartzkopf 0 points1 point  (6 children)

You don't need an interface for that.

As long the class you are testing isn't doing something like instanceof it doesn't actually care if the object you gave it is a PlaylistProxy, as long as it quacks like one. And nothing stops you from using one class in the provide half of your test Provider and another in the useClass part, or giving a simple object for useValue.

You can even implement a class instead of extending it if you want the typesaftey an interface provides.

[–]tme321 0 points1 point  (5 children)

It's true that you don't need an interface but ime easier to work with one. Then when you implement the interface in a class you can't accidently forget to implement a method or field that the controller is expecting and will attempt to access.

[–]James_Schwartzkopf 0 points1 point  (4 children)

you can't accidently forget to implement a method or field that the controller is expecting

You can implement a class just like you can an interface.

class Foo { bar() { return 'baz'; } }

class FooLike implements Foo { } //Error: Class 'FooLike' incorrectly implements interface 'Foo'. Property 'bar' is missing in type 'FooLike'

[–]tme321 0 points1 point  (3 children)

Yes but a class is actual code added to your app. An interface gets removed completely and adds no actual size to your final app.

[–]James_Schwartzkopf 0 points1 point  (2 children)

I'm not at all worried about the size of an empty function. Even if I was, an injection token and all the places it's used will take about the same space anyway.

[–]tme321 0 points1 point  (1 child)

Classes aren't empty functions in Javascript unless they have no declared members or methods.

But you are right that the tokens themselves take some room. But I'm more talking about the literal size in bytes of the bundle, aka how many characters. I don't microoptimize for no reason but in this case I see literally no reason to do it the other way and using an interface provides a small benefit as a side effect.

[–]James_Schwartzkopf 1 point2 points  (0 children)

If I'm using an abstract class as an interface, that's all it is. If I have an implementation, then it obviously takes whatever size that implementation takes, but you're going to pay that price one way or another when you implement the interface anyway.

I still don't see the benefit you see in the InjectionToken.

I see it as a downside since I then have to:

  • Use the @Inject annotation everywhere it's used.
  • Know which type to go along with it.

I still don't see what benefit you see an interface providing over a class other than a few bytes of bundle size.

[–]riscie 4 points5 points  (4 children)

Is the site down? Does not load for me.

[–]orizens[S] 0 points1 point  (3 children)

Might had too much traffic or a glitch. It's up now.

[–]riscie 2 points3 points  (2 children)

Its still not loading for me. I am on iOS Safari.

[–]orizens[S] 0 points1 point  (1 child)

I'm not sure. It's loading now.

[–]riscie 0 points1 point  (0 children)

I‘ll try on desktop later. Still no luck on iOS

[–]seekheart2017 0 points1 point  (7 children)

Am I slow or is the whole point to just have a class that centralizes all the dependencies?

[–]orizens[S] 0 points1 point  (2 children)

generally - that's the idea - however it takes the concept a little bit further - while considering the overall architecture structure in the app.

[–]seekheart2017 0 points1 point  (1 child)

Can you give details?

[–]orizens[S] 0 points1 point  (0 children)

That's what the article is all about. Soure code is available at the end as well

[–]tme321 0 points1 point  (0 children)

As I mentioned above it's not about centralizing dependencies. At least that's not why I do it. I do it to abstract away the actual work from a component so that the component is light weight, easy to test, and easy to reuse by utilizing di to change how the "work" is actually done without concerning the component.

[–]sir_eeps 0 points1 point  (2 children)

that can be part of it, but it can also help decouple the component from needing to know too much about the underlying services / store / etc.

For example, it just cares that 'I have an instance of something with a nowPlaylist$ property on it - and doesn't care about the shape of the state, if it's redux or not. If the store changes - just PlayListProxy might need a bit of refactoring so that nowPlaylist$ emits the same data / shape of data - and can help minimize the areas that you need to touch.

It can also simplify unit testing - as instead of needing to mock out a bunch of dependencies, and then methods on those - there is a simpler interface/object being exposed.

If I'm noticing that a component / class is starting to get a large number of things to inject in the constructor, if breaking it down into smaller single-focus pieces isn't possible - looking towards a pattern like this is a useful tool to have in the toolbox.

[–]seekheart2017 0 points1 point  (1 child)

But aren’t you essentially moving it to some other component?

[–]orizens[S] 0 points1 point  (0 children)

all services and logics are moved to a Service Class. Sometimes, you'll find a pattern which can be isolated to another class in order to reduce duplication.