likes
comments
collection
share

angular-或许这样继承更优雅

作者站长头像
站长
· 阅读数 62

背景

继承作为一种强大的编程工具,可以有效地实现代码复用和构建类层次结构。然而,过度或不当地使用继承可能导致一系列问题。在使用继承时,必须谨慎权衡代码复用与代码清晰性之间的关系,以避免引入耦合性、维护难度等问题。基于我个人的经验,我注意到许多情况下继承被滥用,表现为以下一些问题:

子类与父类的脱节

虽然理论上父类是子类的抽象或通用化,但实际情况却可能出现子类与父类之间的脱节。这种情况下,继承关系只是名义上的,而实际上子类并没有真正从父类继承有用的行为或属性。这种情况下,继承的价值被削弱,导致代码结构变得混乱且难以理解。

冗余参数传递

在一些情况下,子类继承了父类的构造函数,但却并不需要使用构造函数中的所有参数。然而,由于继承关系,子类仍然需要在构造函数中传递这些不必要的参数,从而造成参数传递的冗余。这不仅使代码显得冗长,还增加了维护和理解的难度。

不必要的功能继承

父类可能定义了一些子类并不需要的功能或方法,但由于继承关系,这些功能被强制性地继承到了子类中。这可能导致子类的代码变得复杂,增加了代码的理解和维护的难度。继承应该只传递有用的功能,而不应该引入不必要的复杂性。

直接依赖于基类的实现细节

子类在继承父类的同时,可能直接依赖于基类的具体实现细节。这违反了良好的设计原则,导致子类与基类之间的紧耦合。如果基类的内部实现发生变化,可能会影响所有继承了它的子类,导致代码的脆弱性。

过深的继承层次

当继承层次变得过于复杂时,会增加代码的维护难度。过多的继承关系可能导致代码结构变得难以理解,同时也限制了代码的扩展性。此外,多层次的继承关系可能违反SOLID设计原则中的单一职责原则和开闭原则。

案例

冗余参数传递

在以下示例中,我们可以看到继承导致的冗余参数传递问题:

@Directive()
export class BaseComponent implements OnDestroy {
  protected readonly destroy$ = new Subject<void>();

  constructor(public readonly injector: Injector,
              public readonly templateRef: TemplateRef<HTMLElement>,
              private readonly router: Router,
              private readonly activatedRoute: ActivatedRoute,
              private readonly route: RouterStateSnapshot) {
  }
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

@Component({
  selector: 'app-root',
  template: `
    <app-test *ngIf="show"></app-test>
    <button (click)="show = false">销毁</button>
  `
})
export class AppComponent extends BaseComponent {
  show = true;

  constructor(injector: Injector,
              templateRef: TemplateRef<HTMLElement>,
              router: Router,
              activatedRoute: ActivatedRoute,
              route: RouterStateSnapshot) {
    super(injector, templateRef, router, activatedRoute, route);
  }
}

在上述代码中,子类 AppComponent 继承了基类 BaseComponent,但是基类的构造函数包含了一些子类并不需要的参数,从而导致了参数传递的冗余,增加了代码的复杂性和可读性。

解决

为了解决冗余参数传递问题,可以考虑使用依赖注入容器来获取所需的依赖项,而不需要在子类的构造函数中传递这些参数。以下是一个可能的解决方案:

@Directive()
export class BaseComponent implements OnDestroy {
  protected readonly destroy$ = new Subject<void>();
  public readonly injector = inject(Injector);
  public readonly templateRef: TemplateRef<HTMLElement> = inject(TemplateRef);
  private readonly router = inject(Router);
  private readonly activatedRoute = inject(ActivatedRoute);
  private readonly route = inject(RouterStateSnapshot);
  
  constructor() {
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

@Component({
  selector: 'app-root',
  template: `
    <app-test *ngIf="show"></app-test>
    <button (click)="show = false">销毁</button>
  `
})
export class AppComponent extends BaseComponent {
  show = true;
}

在这个解决方案中,基类 BaseComponent 使用依赖注入容器来获取需要的依赖项,而不需要在构造函数中显式传递参数。子类 AppComponent 则继承了这种依赖注入方式,从而避免了冗余参数传递问题。这使得代码更加简洁,同时减少了不必要的耦合。

何时使用继承

案例章节中只列举了一个案例,是因为这个案例是可以直接解决的,其他问题需要规范继承的使用,知道什么时候应该使用继承才能解决

合理地使用继承是关键,因为继承在适当的情况下可以提高代码的复用性和清晰性。下面探讨了何时应该使用继承,以及在哪些情况下它是一个有益的解决方案:

  1. 共享通用行为: 当多个类具有相似的属性和方法,且这些属性和方法是可复用的通用行为时,可以考虑将这些通用行为放在一个基类中,让子类继承这些功能。例如,多个不同类型的动物可以共享“移动”和“发出声音”的方法。
  2. 定义抽象概念: 如果存在一个通用的抽象概念,可以通过基类来定义它,然后通过子类来实现具体的细节。例如,基类可以是“形状”,子类可以是“圆形”、“正方形”等。
  3. 代码复用和共享: 当多个类之间存在部分相同的功能,但也需要进行个别的扩展时,继承可以用于实现共享和复用。这样可以避免在多个地方重复编写相似的代码。
  4. 创建类层次结构: 当你需要构建一个类层次结构,其中每个子类都是基于一个通用的父类进行扩展时,继承是一个合适的选择。这种情况下,继承可以帮助你建立更清晰的继承关系。
  5. 实现接口或抽象类: 在一些编程语言中,如Java和C#, 接口和抽象类是通过继承来实现的。如果需要实现一些共同的接口或抽象方法,继承是必要的。

如何解决为了继承而继承的问题

当我们发现继承被滥用,只是为了复用逻辑而导致代码变得难以控制时,我们可以考虑采取以下一些方法来解决这个问题:

1. 组合

使用组合是一个强大的替代方案。将共享的逻辑封装成可复用的服务、简单类或函数,然后在需要的地方通过依赖注入来使用。这种方式可以避免继承带来的冗余参数传递和不必要的复杂性。

或者将共享的逻辑封装成可复封装成可复用的函数。

// counterLogic.ts
export function createCounterLogic() {
  let count = 0;

  function increment() {
    count++;
  }

  function decrement() {
    count--;
  }

  function getCount() {
    return count;
  }

  return {
    increment,
    decrement,
    getCount
  };
}

// my-component.ts
import { Component } from '@angular/core';
import { createCounterLogic } from './counterLogic';

@Component({
  selector: 'app-my-component',
  template: `
    <button (click)="increment()">增加</button>
    <button (click)="decrement()">减少</button>
    <p>当前计数:{{ getCount() }}</p>
  `
})
export class MyComponent {
  private counterLogic = createCounterLogic();

  increment() {
    this.counterLogic.increment();
  }

  decrement() {
    this.counterLogic.decrement();
  }

  getCount() {
    return this.counterLogic.getCount();
  }
}

2. 使用inject来获取依赖

使用依赖注入来传递需要的依赖项,而不是通过继承来传递参数。这可以减少参数传递的冗余,同时降低子类与基类之间的耦合。通过将依赖项注入到构造函数中,可以更清晰地管理和使用这些依赖项。

3. 使用指令和管道

如果你想要在组件中拓展功能,但又不想过多使用继承,可以考虑使用 Angular 中的指令和管道。这些功能允许你在组件中应用额外的逻辑,而无需创建新的子类。

总结

在本章中,我们深入探讨了继承的使用和滥用情况,以及如何解决为了继承而继承的问题。继承作为面向对象编程的重要概念,可以有效地实现代码复用和类层次结构。然而,不当地使用继承可能导致代码耦合性增加、维护困难等问题。

通过深思熟虑地选择何时使用继承,我们可以避免滥用继承,提高代码的可维护性和可读性。尽管继承在一些情况下是适合的,但在解决共享逻辑和代码复用时,还可以借鉴其他编程思想,如组合、依赖注入和设计模式。

在实际的项目开发中,我们应该根据具体的情况,综合考虑使用继承的利弊,遵循良好的设计原则和编程实践。通过将继承与其他代码组织方式相结合,我们可以编写出更清晰、更灵活、更可维护的代码,为项目的成功和发展做出贡献。