TypeScript 抽象类详解及应用实例(十七)

在 TypeScript 面向对象编程中,抽象类(Abstract Class)是一个重要的概念。本文将详细介绍抽象类的用途、基本语法、实例应用以及与其他类型(如接口)的区别,并通过具体的示例帮助你更好地理解和使用抽象类。

为什么需要抽象类

在面向对象编程中,我们经常需要定义一些基类,这些基类描述了子类共同的属性和方法。然而,有些方法的具体实现细节需要由子类来决定。抽象类正是为了解决这一问题而设计的。它允许我们在基类中定义方法的签名(声明),而将具体实现交给子类。

抽象类的特点

  • 不能直接实例化:抽象类不能直接使用 new 关键字创建实例,只能作为基类供其他类继承。
  • 定义公共结构:抽象类主要用于定义子类的公共属性和方法,为子类提供一个统一的结构和行为模板。
  • 包含抽象方法:抽象类可以包含抽象方法(只有声明,没有实现),子类必须实现这些抽象方法。

抽象类基础

使用 abstract 关键字声明抽象类和抽象方法。抽象类可以包含抽象方法和具象方法(已有实现的方法)。

示例

// 定义抽象类 Animal
abstract class Animal {
  // 抽象方法:子类必须实现
  abstract speak(): void;

  // 具象方法:子类可以直接使用
  move() {
    console.log('在移动');
  }
}

// 定义 Dog 类继承 Animal
class Dog extends Animal {
  speak() {
    console.log('汪汪汪!');
  }
}

// 创建 Dog 实例
const dog = new Dog();
dog.speak(); // 输出: 汪汪汪!
dog.move();  // 输出: 在移动

抽象方法

抽象方法是抽象类中只声明但不实现的方法。子类继承抽象类后,必须实现所有抽象方法,否则会编译报错。

示例

// 定义抽象类 Shape
abstract class Shape {
  // 抽象方法:子类必须实现
  abstract area(): number;
  abstract perimeter(): number;

  // 具象方法:可以调用抽象方法
  describe() {
    console.log(`面积: ${this.area().toFixed(2)}`);
  }
}

// 定义 Rectangle 类继承 Shape
class Rectangle extends Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }

  perimeter() {
    return 2 * (this.width + this.height);
  }
}

// 创建 Rectangle 实例
const rect = new Rectangle(4, 5);
rect.describe(); // 输出: 面积: 20.00
console.log(`周长: ${rect.perimeter()}`); // 输出: 周长: 18

抽象类作为类型

抽象类可以作为参数类型使用,这意味着函数可以接受任何抽象类的子类实例,从而实现多态。

示例

// 定义抽象类 Animal
abstract class Animal {
  // 抽象方法:所有动物都会发出声音
  abstract speak(): void;
}

// 定义 Cat 类继承 Animal
class Cat extends Animal {
  speak() {
    console.log('喵喵喵!');
  }
}

// 定义 Dog 类继承 Animal
class Dog extends Animal {
  speak() {
    console.log('汪汪汪!');
  }
}

// 定义函数参数类型为抽象类 Animal
function makeSpeak(animal: Animal) {
  animal.speak();
}

// 传入不同的子类实例,实现不同的行为(多态)
makeSpeak(new Cat()); // 输出: 喵喵喵!
makeSpeak(new Dog()); // 输出: 汪汪汪!

// 抽象类类型数组:可以存储不同子类实例
const animals: Animal[] = [new Cat(), new Dog()];

抽象类与接口的区别

抽象类和接口都用于定义类型规范,但它们有一些关键区别。理解这些区别有助于在实际开发中做出正确的选择。

特性抽象类接口
实例化不能直接实例化不能直接实例化
方法实现可以有具体实现只能有声明(TypeScript 3.6+ 可有默认实现)
成员修饰符可以添加 public、protected、private只能有 readonly
继承单继承(只能 extends 一个类)多实现(可以 implements 多个接口)
构造函数可以有构造函数不能有构造函数

选择建议

  • 需要共享代码:使用抽象类。
  • 需要定义规范/契约:使用接口。
  • 需要多继承:使用接口。

完整示例:支付系统

下面是一个使用抽象类实现的支付系统示例,展示了抽象类在实际项目中的应用。

示例

// 定义抽象支付类
abstract class Payment {
  // 抽象方法:处理支付,子类必须实现
  abstract process(amount: number): boolean;

  // 具象方法:验证支付信息
  validate() {
    console.log('验证支付信息');
  }
}

// 信用卡支付类
class CreditCardPayment extends Payment {
  cardNumber: string;

  constructor(cardNumber: string) {
    super();
    this.cardNumber = cardNumber;

  }

  process(amount: number): boolean {
    console.log(`处理信用卡支付: ${amount}`);
    return true;
  }
}

// PayPal 支付类
class PayPalPayment extends Payment {
  email: string;

  constructor(email: string) {
    super();
    this.email = email;
  }

  process(amount: number): boolean {
    console.log(`处理 PayPal 支付: ${amount}`);
    return true;
  }
}

// 使用多态处理不同的支付方式
const payments: Payment[] = [
  new CreditCardPayment('1234'),
  new PayPalPayment('test@example.com')
];

// 遍历处理每种支付方式
for (const payment of payments) {
  payment.validate();
  payment.process(100);
}

运行结果

验证支付信息
处理信用卡支付: 100
验证支付信息
处理 PayPal 支付: 100

应用场景

抽象类常用于框架设计和业务逻辑分层。例如支付处理、用户认证、数据持久化等场景。

注意事项

  • 不能实例化:抽象类不能直接使用 new 创建实例,必须通过子类继承。
  • 必须实现抽象方法:子类如果不实现所有抽象方法,会编译报错。
  • 可以有构造函数:抽象类可以定义构造函数,子类需要调用 super()。
  • 单继承:一个类只能继承一个抽象类(单继承)。
  • 访问修饰符:抽象方法可以使用 public、protected 修饰符。

最佳实践

当多个类需要共享代码和逻辑时,使用抽象类。当只需要定义规范和契约时,使用接口。

总结

抽象类是 TypeScript 面向对象编程的重要组成部分。通过使用 abstract 关键字,我们可以声明抽象类和方法,确保子类实现特定的行为。抽象类不仅提供了代码复用的机制,还支持多态,使得代码更加灵活和可扩展。

  • abstract 关键字:用于声明抽象类和方法。
  • 不能实例化:只能通过子类继承使用。
  • 抽象方法:子类必须实现的方法。
  • 具象方法:子类可以直接继承使用的有实现的方法。
  • 模板方法模式:抽象类定义骨架,子类提供具体实现。

在设计类层次结构时,优先考虑使用抽象类来共享代码和定义规范。希望本文能帮助你更好地理解和使用 TypeScript 中的抽象类。