继承和依赖注入

136

我有一组Angular2组件,它们都需要注入某个服务。我的第一个想法是创建一个超类并在超类中注入该服务。然后,任何一个组件都可以扩展该超类,但这种方法不可行。

简化示例:

export class AbstractComponent {
  constructor(private myservice: MyService) {
    // Inject the service I need for all components
  }
}

export MyComponent extends AbstractComponent {
  constructor(private anotherService: AnotherService) {
    super(); // This gives an error as super constructor needs an argument
  }
}

我可以通过在每个组件中注入MyService并将其用作super()调用的参数来解决这个问题,但这绝对是一种荒谬的方法。

如何正确组织我的组件,以便它们从超类继承一个服务?


这不是重复的问题。所引用的问题是关于如何构建一个派生类,该类可以访问已由已定义的超类注入的服务。我的问题是如何构建一个超类,使得派生类可以继承该服务。它只是相反的方向。 - maxhb
你在问题中的回答让我感到困惑。这样做会创建一个与 Angular 应用程序使用的注入器无关的注入器。而使用 new MyService() 而不是注入,则可以获得完全相同的结果(除了更高效外)。如果你想要在不同服务和/或组件之间共享同一服务实例,这种方法将行不通。每个类都将获得另一个 MyService 实例。 - Günter Zöchbauer
你说得完全正确,我的代码会生成许多myService实例。我找到了一个避免这种情况但需要在派生类中添加更多代码的解决方案... - maxhb
当需要在许多地方注入多个不同的服务时,注入注射器只是一种改进。您还可以注入具有依赖关系的其他服务,并使用getter(或方法)提供它们。这样,您只需要注入一个服务,但可以使用一组服务。您的解决方案和我提出的替代方案都有一个缺点,即它们使得更难看到哪个类依赖于哪个服务。我更愿意创建工具(例如WebStorm中的实时模板),以使创建样板代码更容易并明确依赖项。 - Günter Zöchbauer
8个回答

94

我可以通过在每个组件中注入MyService并使用该参数调用super()来解决这个问题,但这显然有些荒谬。

这并不荒谬。这就是构造函数和构造函数注入的工作原理。

每个可注入类都必须将其依赖项声明为构造函数参数,如果父类也有依赖项,则需要在子类的构造函数中列出这些依赖项,并使用super(dep1, dep2)调用将它们传递给父类。

通过传递一个注入器并即时获取依赖项存在严重缺陷。

它隐藏了依赖项,使代码更难阅读。
它违反了熟悉Angular2 DI工作方式的人的期望。
它破坏了离线编译,后者生成静态代码以替换声明式和命令式DI,以改善性能并减少代码大小。


8
为了明确一点:我需要它到处都能用。尝试将这个依赖项移动到我的超类中,以便于每个派生类都可以访问该服务,而不需要单独向每个派生类注入它。 - maxhb
9
他自问的答案是一个丑陋的技巧。这个问题已经展示了应该如何解决。我会再详细地阐述一些。 - Günter Zöchbauer
8
这个答案是正确的。提问者回答了自己的问题,但在这样做时违反了很多惯例。你列出实际的缺点也是有帮助的,我会支持它——我也在考虑同样的问题。 - dudewad
6
我真的很想(并将继续)使用这个答案而不是OP的“hack”。但我必须说,这似乎远非DRY,并且在我想向基类添加依赖项时非常麻烦。我刚刚不得不向20多个类中添加ctor注入(以及相应的super调用),而这个数字在未来只会增长。所以有两件事:1)我不想看到“大型代码库”这样做;和2)谢天谢地有vim的“q”和vscode的“ctrl + .”。 - user4275029
6
不方便并不意味着是不好的做法。构造函数之所以不方便,是因为很难可靠地完成对象初始化。我认为更糟糕的做法是构建一个需要“注入15个服务且被6个类继承的基类”的服务。 - Günter Zöchbauer
显示剩余19条评论

81

更新的解决方法,使用全局注入器来防止生成多个myService实例。

import {Injector} from '@angular/core';
import {MyServiceA} from './myServiceA';
import {MyServiceB} from './myServiceB';
import {MyServiceC} from './myServiceC';

export class AbstractComponent {
  protected myServiceA:MyServiceA;
  protected myServiceB:MyServiceB;
  protected myServiceC:MyServiceC;

  constructor(injector: Injector) {
    this.settingsServiceA = injector.get(MyServiceA);
    this.settingsServiceB = injector.get(MyServiceB);
    this.settingsServiceB = injector.get(MyServiceC);
  }
}

export MyComponent extends AbstractComponent {
  constructor(
    private anotherService: AnotherService,
    injector: Injector
  ) {
    super(injector);

    this.myServiceA.JustCallSomeMethod();
    this.myServiceB.JustCallAnotherMethod();
    this.myServiceC.JustOneMoreMethod();
  }
}

这将确保MyService可以在任何扩展AbstractComponent的类中使用,而无需在每个派生类中注入MyService。
这种解决方案存在一些缺点(请参见我的原始问题下面@Günter Zöchbauer的Ccomment):
- 注入全局注入器仅在有多个不同服务需要在许多位置注入时才是一种改进。如果只有一个共享服务,则在派生类中注入该服务可能更好/更容易。 - 我的解决方案和他提出的替代方案都具有缺点,它们使得更难看到哪个类依赖于哪个服务。
有关Angular2中依赖注入的非常好的解释,请参阅此博客文章,该文章对我解决问题非常有帮助:http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular-2.html

7
这使得很难理解哪些服务实际上被注入了进去。 - Simon Dufour
1
应该是this.myServiceA = injector.get(MyServiceA);等等吧? - jenson-button-event
11
@Gunter Zochbauer的回答是正确的。这不是正确的做法,违反了许多Angular惯例。也许相对于编写所有这些注入调用来说更简单,但如果您想要牺牲编写构造函数代码以维护大型代码库,则会自食其果。在我看来,这个解决方案是不可扩展的,并且将在未来引起许多混乱的错误。 - dudewad
3
不存在同一服务的多个实例的风险。您只需要在应用程序的根目录提供一个服务,以防止在应用程序的不同分支上可能出现的多个实例。将服务通过继承传递 不会 创建类的新实例。@Gunter Zochbauer的答案是正确的。 - ktamlyn
@maxhb 你有没有考虑过全局扩展 Injector 来避免将任何参数链接到 AbstractComponent 上?顺便说一下,我认为通过将属性注入依赖项到广泛使用的基类中以避免混乱的构造函数链接是通常规则的完全有效的例外。 - quentin-starin
显示剩余2条评论

7

我创建了一个提供服务的类,而不是手动注入所有服务,例如,它会自动注入服务。然后将此类注入到派生类中,并传递给基类。

派生类:

@Component({
    ...
    providers: [ProviderService]
})
export class DerivedComponent extends BaseComponent {
    constructor(protected providerService: ProviderService) {
        super(providerService);
    }
}

基础类:

export class BaseComponent {
    constructor(protected providerService: ProviderService) {
        // do something with providerService
    }
}

服务提供类:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}

3
问题在于,你有可能会创建一个“杂物抽屉”式的服务,这个服务本质上只是一个针对注入器服务的代理。 - kpup

5
据我所知,使用Angular v14中的inject()https://angular.io/api/core/inject非常简单,因为您现在可以在“字段初始化器”中设置依赖项,而完全不需要构造函数。
我使用了一些简单的令牌依赖项,例如DOCUMENTLOCALE_ID作为示例。
import { DOCUMENT } from '@angular/common';
import { inject } from '@angular/core';

export abstract class AbstractComponent {
  abstractDependency = inject(DOCUMENT);
}

import { Component, inject, LOCALE_ID } from '@angular/core';
import { AbstractComponent } from './abstract.component';

@Component({
  selector: 'my-app',
  template: '{{ abstractDependency }} {{ myDependency }}',
})
export class MyComponent extends AbstractComponent {
  myDependency = inject(LOCALE_ID);
}

实时范例:https://stackblitz.com/edit/angular-ivy-zvlx1d?file=src/app/my.component.ts

该链接提供了一个Angular应用程序的实时示例,您可以在其中查看和编辑my.component.ts文件。这是一个很好的学习资源,以便更好地理解Angular框架的使用方法。

3
没错!我在想是否可以注入省略构造函数 :) - KGROZA
这个从v14版本开始就可以工作了! - Matthieu Riegler
完美。谢谢。 - Octane

2

不要像这样注入一个有所有其他服务作为依赖项的服务:

class ProviderService {
    constructor(private service1: Service1, private service2: Service2) {}
}

class BaseComponent {
    constructor(protected providerService: ProviderService) {}

    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

我建议跳过这个额外的步骤,直接在BaseComponent中注入所有服务,像这样:
class BaseComponent {
    constructor(protected service1: Service1, protected service2: Service2) {}
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        this.service1;
        this.service2;
    }
}

这个技巧假设两件事情:
  1. 你的问题完全与组件继承有关。很可能,你来到这个问题是因为在每个派生类中需要重复的非DRY(WET?)代码太多了。如果你想要所有组件和服务的一个统一入口,你需要进行额外的步骤。

  2. 每个组件都扩展了BaseComponent

如果你决定使用派生类的构造函数,也会有一个缺点,那就是你需要调用 super() 并传入所有依赖项。虽然我并不真正看到必须使用 constructor 而不是 ngOnInit 的用例,但完全有可能存在这样的用例。


2
基类然后依赖于其任何子级需要的所有服务。ChildComponentA 需要 ServiceA?那么现在 ChildComponentB 也会得到 ServiceA。 - knallfrosch

1
据我所了解,要继承基类,首先需要实例化它。为了实例化它,您需要通过super()调用将其构造函数所需的参数从子类传递给父类,这样才有意义。当然,注入器是另一种可行的解决方案。

1

丑陋的黑客

不久前,我的一些客户想将两个大型Angular项目合并到昨天(将Angular v4合并到Angular v8)。项目v4为每个组件使用BaseView类,并包含tr(key)方法进行翻译(在v8中,我使用ng-translate)。因此,为了避免切换翻译系统并编辑数百个模板(在v4中),或者同时设置2个翻译系统,我使用以下丑陋的黑客(我并不为此感到自豪)- 在AppModule类中,我添加了以下构造函数:

export class AppModule { 
    constructor(private injector: Injector) {
        window['UglyHackInjector'] = this.injector;
    }
}

现在您可以使用AbstractComponent
export class AbstractComponent {
  private myservice: MyService = null;

  constructor() {
    this.myservice = window['UglyHackInjector'].get(MyService);
  }
}

-1

如果父类已经来自第三方插件(且您无法更改源代码),则可以这样做:

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  constructor(
    protected injector: Injector,
    private anotherService: AnotherService
  ) {
    super(injector.get(MyService));
  }
}

或者更好的方式(只在构造函数中保留一个参数):

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  private anotherService: AnotherService;

  constructor(
    protected injector: Injector
  ) {
    super(injector.get(MyService));
    this.anotherService = injector.get(AnotherService);
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接