2

I use Angular2 DI to inject a Logger class in various services and components. Is it possible to create and inject a new object every time a Logger is requested? I tried the FactoryProvider; it instantiates a single instance once and then injects the same instance everywhere.

Registering a separate Logger provider for every component would solve this but that seems to be an overkill.

So far I found no better solution but it would make things much more convenient for me.

EXAMPLES

This is my super-convenient goal:

// logger.service.ts
export class Logger {
  static LoggerProvider: FactoryProvider = {
    provide: Logger,
    useFactory: () => {
      return new Logger();
    },
    /* Actually we need a LoggerService here, I omitted that. */
  };
  name: string;
  info(msg: string) { console.log('INFO', this.name, msg); }
}

// status.service.ts
@Injectable()
export class StatusService {
  constructor(private log: Logger) {
    this.log.name = 'StatusService';
    this.log.info('ctor'); // logs "INFO StatusService ctor"
  }
  test() {
    this.log.info('test'); // should log "INFO StatusService test"
  }
}

// home.component.ts
@Component({ /* ... */})
export class HomeComponent {
  constructor(
    private statusService: StatusService 
    private log: Logger,
  ) {
    // Got the same logger as the StatusService!!!
    this.log.name = 'HomeComponent';
    this.log.info('ctor'); // logs "INFO HomeComponent ctor"
    this.statusService.test(); // breaks and logs "INFO HomeComponent test"!!!
  }

}

This could be done now, however, it needs a little more boilerplate:

// status.service.ts
@Injectable()
export class StatusService {
  private log: Logger;
  constructor(loggerService: LoggerService) {
    this.log = loggerService.createLogger('StatusService');
    this.log.info('ctor'); // logs "INFO StatusService ctor"
  }
  test() {
    this.log.info('test'); // definitely logs "INFO StatusService test"
  }
}

MORE UPDATE

Well, I was digging in Angular2 issue tracker and found this guy (#18015) where they argue about a DI extension solving this problem and the way it would affect the whole DI mechanism. It seems that this would be the real solution but there is no conclusion yet. (@bryan60 seems to be right: currently -- 5.0.0-rc.1 -- this is NOT supported.)

Gábor Imre
  • 5,899
  • 2
  • 35
  • 48
  • Please, show the actual code for *Logger class in various services and components*. – Estus Flask Oct 04 '17 at 20:04
  • I believe what you're describing is exactly what the separate `provider` is intended for. Whereas having a provider at the app level creates a single `Logger` instance used wherever `Logger` is injected, using a provider-per-component would likewise create a `Logger` instance-per-component – Jack Koppa Oct 04 '17 at 20:06
  • Possible duplicate of [Angular2: How to inject two instances of the same service into multiple components?](https://stackoverflow.com/questions/39862994/angular2-how-to-inject-two-instances-of-the-same-service-into-multiple-componen) – Estus Flask Oct 04 '17 at 20:06
  • @estus Nope, I have a somewhat different goal: to have a new object on *every* injection. The cited question requires 2 instances, and [this question](https://stackoverflow.com/questions/38482357/angular2-how-to-use-multiple-instances-of-same-service) needs to inject twice into the same component. Examples are coming. – Gábor Imre Oct 04 '17 at 20:13
  • If you want new instance each time why can't you usr the constructor new Logger()? – Surender Khairwa Oct 04 '17 at 20:21
  • See the answer. It is totally applicable here. You need to have Logger as `useValue` service and instantiate it manually with `new`. That's the point. – Estus Flask Oct 04 '17 at 20:23
  • @SurenderKherwa The `Logger` should use DI under the hood, there is a `LoggerService` with a nice aggregation of logs and error management which needs Http. Yes, I could hack it w/o DI but that smells bad... – Gábor Imre Oct 04 '17 at 20:24
  • @estus Thanks, that would work, however, I'm trying to be super convenient by creating the logger and requesting DI in one line. Create the local `log` variable: +1 line. Inject `LoggerService` in ctor: +1 line (ok, we definitely need this one). Create the `this.log` instance from the service: +1 line. Plus you have to import both`{ Logger, LoggerService}` to make it work. – Gábor Imre Oct 04 '17 at 20:28
  • 1
    What you want is a new logger everytime it's injected, the only way to do that through DI is to provide it everywhere it's injected. This is the intended behavior of DI. You should be reviewing your design decisions to determine why you actually need this and see if you can't come up with something better. – bryan60 Oct 04 '17 at 20:28
  • 1
    See if this helps: https://stackoverflow.com/a/38473200/2015408 – Surender Khairwa Oct 04 '17 at 20:35
  • @bryan60 it's a common use case to have separate loggers per class. in most cases, even to do some component event based logging, this is needed. btw. similar scenarios are by AutoFac (in .NET supported out of the box). >>> the serviceLogger and the extra line code although adds a new possibility: to distinguish the new logger in case of component reuse, so instead getting multiple outputs like "address onInit" we could get "mailing/shipping address onInit"... – baHI Jun 04 '18 at 08:44
  • @baHI I'd argue this is flawed design if you rely on / require a new object in a certain state for your logger to properly function. I'd personally opt for a more functional design that doesn't rely on side effects and state, even if it makes my code a tad more verbose. In the stateful set up, you're required to name your logger on each instantiation, why not instead require the user of the logger to identify itself and make the logger stateless? – bryan60 Jun 04 '18 at 16:14
  • @bryan60 reason: DRY. but i'd not use the word stateful, its just a stupid constant and not changing, there are no states for the logger. for each class a new logger instance would be injcted. btw. the sample above does the same, although an extra line is needed. and those loggers with state: check out PINO, WINSTON, etc. they also support NESTED loggers, which is just an alternative for the issue above... that extra parameter is not a state, just an ID... – baHI Jun 05 '18 at 18:33
  • @baHI Angular should be DRY but not too DRY, favoring explicit over DRY. It's a different paradigm than working in a strictly typed environment where you have tighter levels of control and things are naturally more explicit. – bryan60 Jun 05 '18 at 18:49

3 Answers3

3

It's actually possible to create a Logger every time it is requested. Just think about ActivatedRoute, you will get different instances for different components which under different routes.

The idea is to create a custom injector.

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-container-outlet',
  template: '<router-outlet></router-outlet>',
})
export class AppContainerOutletComponent { }


@Directive({
  selector: 'some-root-container',
})
export class AppContainerDirective implements OnInit {
  private content: ComponentRef<any> | null;
  constructor(
    private vcr: ViewContainerRef,
    private cdr: ChangeDetectorRef,
    private resolver: ComponentFactoryResolver,
    private router: Router
  ) { }

  ngOnInit() {
    const factory = this.resolver.resolveComponentFactory(AppContainerOutletComponent);
    const injector = new ContainerInjector(this.router, this.vcr.injector);
    this.content = this.vcr.createComponent(factory, this.vcr.length, injector);
    this.cdr.markForCheck();
  }
}

class ContainerInjector implements Injector {
  constructor(
    private router: Router,
    private parent: Injector
  ) {}

  get(token: any, notFoundValue?: any): any {
    if (token === Logger) {
      return new Logger(/* You can also pass some parameters to Logger */);
    }

    return this.parent.get(token, notFoundValue);
  }
}

Then just use some-root-container instead of router-outlet(only need to replace the root one). Note that router-outlet is not required in some-root-container actually. You just need to create an component with a custom injector and make everything else be children of the component.

Then, just inject Logger as usual(do not provide it anywhere). You will get a new Logger instance every time.

The shortcoming is that ngOnDestroy on Logger(if it is implemented) will not be called.

Hope that helps.

Luo Gang
  • 37
  • 3
2

If a service needs to be instantiated multiple times without specifying it in providers each time, it should be instantiated manually:

...
{ provide: Logger, useValue: Logger }
...

...
constructor(@Inject(Logger) Logger: typeof Logger) {
  this.logger = new Logger();
}
...

If Logger class accepts other dependencies as arguments, they should be provided manually in this case.

In order to inject them automatically, additional service should be provided, like loggerFactory:

export function loggerFactory(foo: Foo, bar: Bar): Logger {
  return () => new Logger(foo, bar);
}

...
{ provide: loggerFactory, useFactory: loggerFactory, deps: [Foo, Bar] }
...

...
constructor(@Inject(loggerFactory) loggerFactory: () => Logger) {
  this.logger = loggerFactory();
}
...
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 1
    How is this `constructor(@Inject(Logger) Logger: typeof Logger) { this.logger = new Logger(); }` different with just `constructor() { this.logger = new Logger() }`? – yurzui Oct 04 '17 at 20:52
  • This supports DI (which is not shown actually). Yep, this can be used, but it's not so super-convenient. It's very close to injecting the `LoggerService` and instantiating through that. – Gábor Imre Oct 04 '17 at 20:53
  • 1
    @yurzui It's DI. We're able to switch Logger class any moment, This is highly useful in tests, or if the code isn't first-party but is supposed to be published as third-party library. – Estus Flask Oct 04 '17 at 20:58
  • @GáborImre Yes, if LoggerProvider would do deps, this would be quite close. No, there's no other way. It's either `loggerFactory` or having `providers: [Logger]` logger on every component you're using it. Notice that in the latter case you need to have Foo and Bar in module `providers` but NOT to have Logger there. – Estus Flask Oct 04 '17 at 21:03
  • I see, thanks for the explanation. I need to write more tests) – yurzui Oct 04 '17 at 21:05
  • 1
    There's a method `instantiateResolved` to get new instances using ReflectiveInjector, but there's no equivalent in StaticInjector. I created [an issue](https://github.com/angular/angular/issues/18946) requesting that feature. – Max Koretskyi Oct 05 '17 at 04:52
  • @AngularInDepth.com But ReflectiveInjector would require to register providers apart from module injector. And I guess it reinstantiates Foo and Bar too. – Estus Flask Oct 05 '17 at 06:40
  • `Logger` / `Logger` seems super confusing. Why not `ILogger` / `Logger` – Simon_Weaver Aug 13 '18 at 22:44
  • @Simon_Weaver I think `@Inject(Logger) Logger: typeof Logger` makes perfect sense once you become accustomed with how DI works. `Logger` inside constructor is same class as `Logger` outside constructor, I see no confusion here. `ILogger` name suggests that it's an interface, that would be confusing indeed because none of these `Logger` can be interface. – Estus Flask Aug 13 '18 at 22:53
  • @estus - ok I see :-) I just tend to think of interfaces as soon as anybody talks about 'switching things out'. I've also seen many examples of DI where an interface is used - in fact Angular docs show an example where you might use an interface to 'narrow' the scope of an injected object. BUT revisiting the following article shows it's not really an interface but an abstract class instead. https://angular.io/guide/dependency-injection-in-action - It's still going to always look a bit odd though! :-) – Simon_Weaver Aug 13 '18 at 23:39
  • 1
    @Simon_Weaver Any class can be used as an interface. Using abstract classes as tokens is a common practice, see https://stackoverflow.com/a/42422835/3731501 . It just doesn't make sense to have different `Logger` (abstract class) and `LoggerImplementation` (actual class) here because it's the only implementation, injected `Logger` is really `Logger` class. I'm not sure why OP wanted it to be done with DI. Could be beneficial for extensibility or testability. – Estus Flask Aug 13 '18 at 23:49
1

Amazed how few views this question has. You may want to make Logger a generic type, then you can do super clever things with it.

eg. Typescript has very powerful features (may need 2.9 for some of these)

You can make your logger factory return a generic type

export function loggerFactory<LFT>(): (t) => Logger<LFT> 
{
    return (t: LFT) => new Logger(t);
}

The logger itself can use very powerful filtering so you can create members (note this is all at compile time).

type ObservablePropertyNames<T> = { [K in keyof T]: T[K] extends Observable<any> ? never : K }[keyof T];

export class Logger<T>
{
    constructor(public t: T){
        debugger;
    }

    logMember(member: keyof T)
    {
        console.log('value of member' + member, member);
    }

    // TODO: Unsubscribe afterwards!
    watch(member: keyof Pick<T, ObservablePropertyNames<T>>)
    {
       // member is an Observable<T>
       member.subscribe((v) => {
          console.log('member changed to ' + v);
       });
    }
}

To create the logger you have to use the slightly clumsy syntax that includes the component name as a generic parameter type.

@Inject(loggerFactory) loggerFactory:(t) => Logger<FeatureComponent>) {

    const logger = loggerFactory(this);
    logger.logMember('getPortal');  // only compiles if 'getPortal' exists
    logger.watch('layout');  // only compiles if 'layout' is Observable
}

That way if your class changes it'll fail compilation if you are logging things that aren't there anymore.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689