angular-或许这样继承更优雅
背景
继承作为一种强大的编程工具,可以有效地实现代码复用和构建类层次结构。然而,过度或不当地使用继承可能导致一系列问题。在使用继承时,必须谨慎权衡代码复用与代码清晰性之间的关系,以避免引入耦合性、维护难度等问题。基于我个人的经验,我注意到许多情况下继承被滥用,表现为以下一些问题:
子类与父类的脱节
虽然理论上父类是子类的抽象或通用化,但实际情况却可能出现子类与父类之间的脱节。这种情况下,继承关系只是名义上的,而实际上子类并没有真正从父类继承有用的行为或属性。这种情况下,继承的价值被削弱,导致代码结构变得混乱且难以理解。
冗余参数传递
在一些情况下,子类继承了父类的构造函数,但却并不需要使用构造函数中的所有参数。然而,由于继承关系,子类仍然需要在构造函数中传递这些不必要的参数,从而造成参数传递的冗余。这不仅使代码显得冗长,还增加了维护和理解的难度。
不必要的功能继承
父类可能定义了一些子类并不需要的功能或方法,但由于继承关系,这些功能被强制性地继承到了子类中。这可能导致子类的代码变得复杂,增加了代码的理解和维护的难度。继承应该只传递有用的功能,而不应该引入不必要的复杂性。
直接依赖于基类的实现细节
子类在继承父类的同时,可能直接依赖于基类的具体实现细节。这违反了良好的设计原则,导致子类与基类之间的紧耦合。如果基类的内部实现发生变化,可能会影响所有继承了它的子类,导致代码的脆弱性。
过深的继承层次
当继承层次变得过于复杂时,会增加代码的维护难度。过多的继承关系可能导致代码结构变得难以理解,同时也限制了代码的扩展性。此外,多层次的继承关系可能违反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
则继承了这种依赖注入方式,从而避免了冗余参数传递问题。这使得代码更加简洁,同时减少了不必要的耦合。
何时使用继承
在案例章节中只列举了一个案例,是因为这个案例是可以直接解决的,其他问题需要规范继承的使用,知道什么时候应该使用继承才能解决
合理地使用继承是关键,因为继承在适当的情况下可以提高代码的复用性和清晰性。下面探讨了何时应该使用继承,以及在哪些情况下它是一个有益的解决方案:
- 共享通用行为: 当多个类具有相似的属性和方法,且这些属性和方法是可复用的通用行为时,可以考虑将这些通用行为放在一个基类中,让子类继承这些功能。例如,多个不同类型的动物可以共享“移动”和“发出声音”的方法。
- 定义抽象概念: 如果存在一个通用的抽象概念,可以通过基类来定义它,然后通过子类来实现具体的细节。例如,基类可以是“形状”,子类可以是“圆形”、“正方形”等。
- 代码复用和共享: 当多个类之间存在部分相同的功能,但也需要进行个别的扩展时,继承可以用于实现共享和复用。这样可以避免在多个地方重复编写相似的代码。
- 创建类层次结构: 当你需要构建一个类层次结构,其中每个子类都是基于一个通用的父类进行扩展时,继承是一个合适的选择。这种情况下,继承可以帮助你建立更清晰的继承关系。
- 实现接口或抽象类: 在一些编程语言中,如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 中的指令和管道。这些功能允许你在组件中应用额外的逻辑,而无需创建新的子类。
总结
在本章中,我们深入探讨了继承的使用和滥用情况,以及如何解决为了继承而继承的问题。继承作为面向对象编程的重要概念,可以有效地实现代码复用和类层次结构。然而,不当地使用继承可能导致代码耦合性增加、维护困难等问题。
通过深思熟虑地选择何时使用继承,我们可以避免滥用继承,提高代码的可维护性和可读性。尽管继承在一些情况下是适合的,但在解决共享逻辑和代码复用时,还可以借鉴其他编程思想,如组合、依赖注入和设计模式。
在实际的项目开发中,我们应该根据具体的情况,综合考虑使用继承的利弊,遵循良好的设计原则和编程实践。通过将继承与其他代码组织方式相结合,我们可以编写出更清晰、更灵活、更可维护的代码,为项目的成功和发展做出贡献。
转载自:https://juejin.cn/post/7269763137808089149