跳到主要内容
预计阅读 35 分钟

面向对象工程实践:用类构建可扩展的业务模块

需求场景

订单系统需要一套可扩展的通知模块。产品要求支持三种通知渠道:邮件、短信、站内信。每种渠道有各自的发送逻辑,但共享统一的日志记录、重试机制和序列化接口。未来还可能新增企业微信、钉钉等渠道。

这是一个典型的面向对象设计问题:需要定义抽象的通知基类、约定公共契约、控制内部实现的可见性、支持灵活扩展。TypeScript 在 JavaScript 的 class 语法基础上增加了访问修饰符、抽象类、接口实现等能力,让这种架构设计成为可能。

ES6 class 语法基础

先快速确认 JavaScript 类的基本用法,这些是 TypeScript 扩展的基础。

class Notification {
  channel: string;

  constructor(channel: string) {
    this.channel = channel;
  }

  describe(): string {
    return `Notification via ${this.channel}`;
  }
}

const msg = new Notification("email");
console.log(msg.describe()); // "Notification via email"

继承通过 extends 实现,子类用 super 调用父类:

class EmailNotification extends Notification {
  constructor() {
    super("email");
  }

  describe(): string {
    return "Email: " + super.describe();
  }
}

存取器(Getter/Setter)提供属性访问拦截:

class Account {
  private _balance: number = 0;

  get balance(): number {
    return this._balance;
  }

  set balance(val: number) {
    if (val < 0) throw new Error("Balance cannot be negative");
    this._balance = val;
  }
}

静态成员属于类本身,不属于实例:

class IdGenerator {
  static current: number = 0;

  static next(): number {
    return ++IdGenerator.current;
  }
}

console.log(IdGenerator.next()); // 1
console.log(IdGenerator.next()); // 2

以上都是标准 JavaScript。接下来进入 TypeScript 的扩展领域。

访问修饰符

TypeScript 为类成员提供了三种可见性控制。

public

public 是默认修饰符,成员可以在任何位置被访问:

class NotificationLog {
  public timestamp: number;
  public message: string;

  public constructor(msg: string) {
    this.timestamp = Date.now();
    this.message = msg;
  }

  public format(): string {
    return `[${this.timestamp}] ${this.message}`;
  }
}

const log = new NotificationLog("sent");
console.log(log.message); // 外部可访问

private

private 限制成员只能在当前类内部访问,实例对象和子类都无法触及:

class SmsGateway {
  private apiKey: string;

  constructor(key: string) {
    this.apiKey = key;
  }

  send(phone: string, content: string): void {
    console.log(`Sending via key=${this.apiKey} to ${phone}`);
  }
}

const gw = new SmsGateway("sk_test_123");
console.log(gw.apiKey); // 编译错误:私有属性

class ExtendedGateway extends SmsGateway {
  debug(): void {
    console.log(this.apiKey); // 编译错误:子类也无法访问
  }
}

TypeScript 的 private 是编译时约束。如果需要运行时真正的私有性,使用 ES2022 的 # 语法:

class SecureVault {
  #secretToken: string;

  constructor(token: string) {
    this.#secretToken = token;
  }

  verify(input: string): boolean {
    return input === this.#secretToken;
  }
}

const vault = new SecureVault("abc");
// vault.#secretToken; // 运行时也会报错

protected

protected 允许当前类和子类访问,但实例对象不能访问:

class BaseNotifier {
  protected recipientList: string[] = [];

  protected addRecipient(addr: string): void {
    this.recipientList.push(addr);
  }
}

class BulkNotifier extends BaseNotifier {
  addBatch(addresses: string[]): void {
    addresses.forEach(addr => this.addRecipient(addr)); // 子类可以访问
  }

  getCount(): number {
    return this.recipientList.length; // 子类可以访问
  }
}

const notifier = new BulkNotifier();
notifier.addBatch(["a@test.com", "b@test.com"]);
console.log(notifier.getCount()); // 2
// notifier.recipientList; // 编译错误:外部不可访问

对比表

修饰符类内部子类实例对象
public
protected不可
private不可不可

readonly 修饰符

readonly 使属性在初始化后不可修改,只能在声明时或构造函数中赋值:

class ServiceEndpoint {
  readonly protocol: string = "https";
  readonly host: string;
  readonly port: number;

  constructor(host: string, port: number) {
    this.host = host;
    this.port = port;
  }
}

const ep = new ServiceEndpoint("api.example.com", 443);
ep.host = "other.com"; // 编译错误:只读属性

readonly 可以与访问修饰符组合:

class Invoice {
  constructor(
    public readonly invoiceNo: string,
    public amount: number
  ) {}
}

const inv = new Invoice("INV-001", 500);
inv.amount = 600;     // 通过
inv.invoiceNo = "X";  // 编译错误

构造函数参数属性

TypeScript 提供了一种简写:在构造函数参数前添加修饰符,自动声明同名属性并赋值:

// 传统写法
class OrderItemVerbose {
  public sku: string;
  private unitPrice: number;
  protected warehouse: string;
  readonly lineId: number;

  constructor(sku: string, unitPrice: number, warehouse: string, lineId: number) {
    this.sku = sku;
    this.unitPrice = unitPrice;
    this.warehouse = warehouse;
    this.lineId = lineId;
  }
}

// 参数属性简写
class OrderItemConcise {
  constructor(
    public sku: string,
    private unitPrice: number,
    protected warehouse: string,
    public readonly lineId: number
  ) {}
}

两种写法完全等价,简写方式显著减少了样板代码。

存取器进阶

存取器不仅可以拦截读写,还可以实现计算属性和数据校验:

class PriceCalculator {
  private _basePrice: number;
  private _taxRate: number;

  constructor(base: number, tax: number) {
    this._basePrice = base;
    this._taxRate = tax;
  }

  get basePrice(): number {
    return this._basePrice;
  }

  set basePrice(val: number) {
    if (val <= 0) throw new Error("Price must be positive");
    this._basePrice = val;
  }

  // 只读计算属性
  get totalWithTax(): number {
    return this._basePrice * (1 + this._taxRate);
  }

  get taxAmount(): number {
    return this._basePrice * this._taxRate;
  }
}

const calc = new PriceCalculator(100, 0.13);
console.log(calc.totalWithTax); // 113
console.log(calc.taxAmount);    // 13
calc.basePrice = 200;
console.log(calc.totalWithTax); // 226
// calc.totalWithTax = 300;     // 编译错误:只有 getter 的属性自动为 readonly

TypeScript 中存取器的类型规则随版本演进逐步放宽:

版本getter/setter 类型关系说明
< 4.3必须相同getter 返回类型与 setter 参数类型必须一致
4.3+setter 可更宽(Separate Write Types)getter 返回类型必须可赋值给 setter 参数类型。例如 setter 接受 string | number,getter 返回 number
5.1+可完全不相关不再要求赋值兼容性,getter 和 setter 类型互相独立。但需要为 getter 或 setter 提供显式类型注解

此外,只有 getter 没有 setter 的属性自动推断为 readonly

注意:截至目前,TypeScript 不支持对同一属性的 getter 和 setter 设置不同的访问修饰符(如 public getter + protected setter),该特性仍在社区讨论中。

静态成员

静态成员通过类名访问,不属于任何实例:

class NotificationTracker {
  static sentCount: number = 0;
  static failedCount: number = 0;

  static recordSuccess(): void {
    NotificationTracker.sentCount++;
  }

  static recordFailure(): void {
    NotificationTracker.failedCount++;
  }

  static getSummary(): string {
    return `Sent: ${this.sentCount}, Failed: ${this.failedCount}`;
  }
}

NotificationTracker.recordSuccess();
NotificationTracker.recordSuccess();
NotificationTracker.recordFailure();
console.log(NotificationTracker.getSummary()); // "Sent: 2, Failed: 1"

静态成员也可以使用访问修饰符,publicprotected 的静态成员会被子类继承:

class BaseService {
  static serviceName: string = "base";
  protected static version: number = 1;
  private static internalFlag: boolean = true;
}

class OrderService extends BaseService {
  static show(): void {
    console.log(this.serviceName);  // 继承的 public
    console.log(this.version);      // 继承的 protected
    // console.log(this.internalFlag); // 编译错误:private
  }
}

注意:静态成员不能使用泛型类型参数。

抽象类

抽象类不能被直接实例化,只能作为基类被继承。它定义了子类必须实现的方法签名,同时可以包含具体的公共逻辑。

abstract class NotificationChannel {
  abstract readonly channelName: string;
  abstract deliver(recipient: string, content: string): boolean;

  // 公共逻辑:所有渠道共享的日志记录
  log(action: string): void {
    console.log(`[${this.channelName}] ${action} at ${new Date().toISOString()}`);
  }

  // 公共逻辑:带重试的发送
  sendWithRetry(recipient: string, content: string, maxAttempts: number = 3): boolean {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      this.log(`Attempt ${attempt}`);
      if (this.deliver(recipient, content)) {
        this.log("Success");
        return true;
      }
    }
    this.log("All attempts failed");
    return false;
  }
}

// const channel = new NotificationChannel(); // 编译错误:不能实例化抽象类

class EmailChannel extends NotificationChannel {
  readonly channelName = "Email";

  deliver(recipient: string, content: string): boolean {
    console.log(`Sending email to ${recipient}: ${content}`);
    return true; // 模拟成功
  }
}

class SmsChannel extends NotificationChannel {
  readonly channelName = "SMS";

  deliver(recipient: string, content: string): boolean {
    console.log(`Sending SMS to ${recipient}: ${content}`);
    return Math.random() > 0.3; // 模拟偶发失败
  }
}

const email = new EmailChannel();
email.sendWithRetry("user@test.com", "Your order has been shipped");

const sms = new SmsChannel();
sms.sendWithRetry("13800138000", "Verification code: 8642");

抽象类和抽象成员的规则:

  1. 抽象成员只能存在于抽象类中
  2. 抽象成员只声明签名,不提供实现
  3. 抽象成员不能标记为 private,因为子类需要访问并实现它
  4. 一个类只能继承一个抽象类(不支持多重继承)

接口实现(implements)

implements 让类承诺实现某个接口定义的契约,弥补了不支持多重继承的不足:

interface Serializable {
  toJSON(): string;
}

interface Trackable {
  getTraceId(): string;
}

class ShipmentRecord implements Serializable, Trackable {
  constructor(
    private traceId: string,
    public carrier: string,
    public trackingNo: string
  ) {}

  toJSON(): string {
    return JSON.stringify({
      carrier: this.carrier,
      trackingNo: this.trackingNo,
    });
  }

  getTraceId(): string {
    return this.traceId;
  }
}

重要:implements 仅执行类型校验,不会自动注入任何成员。 你需要在类中逐一实现接口规定的每个属性和方法:

interface Identifiable {
  entityId: number;
  getId(): number;
}

class Product implements Identifiable {
  entityId: number; // 必须自己声明

  constructor(id: number) {
    this.entityId = id;
  }

  getId(): number { // 必须自己实现
    return this.entityId;
  }
}

结合继承和接口实现,可以构建清晰的类型层次:

interface Cacheable {
  getCacheKey(): string;
  ttl: number;
}

interface Loggable {
  writeLog(entry: string): void;
}

abstract class Repository {
  abstract readonly tableName: string;

  persist(data: unknown): void {
    console.log(`Persisting to ${this.tableName}`);
  }
}

class OrderRepository extends Repository implements Cacheable, Loggable {
  readonly tableName = "orders";
  ttl = 3600;

  getCacheKey(): string {
    return `cache:${this.tableName}`;
  }

  writeLog(entry: string): void {
    console.log(`[${this.tableName}] ${entry}`);
  }
}

类的类型兼容性

TypeScript 对类也采用结构化类型原则。两个类只要结构相同,实例就互相兼容:

class PointA {
  x: number = 0;
  y: number = 0;
}

class PointB {
  x: number = 0;
  y: number = 0;
}

const pa: PointA = new PointB(); // 通过:结构相同

但含有 privateprotected 成员时,必须来自同一声明才兼容:

class Alpha {
  private tag: string = "a";
}

class Beta {
  private tag: string = "b";
}

// const x: Alpha = new Beta(); // 编译错误:private 来源不同

class Gamma extends Alpha {}
const y: Alpha = new Gamma(); // 通过:private 来自同一声明

理解类名在类型上下文中的含义:

class Widget {
  constructor(public width: number, public height: number) {}
}

// Widget 作为类型 => 实例的类型
const w: Widget = new Widget(100, 50);

// typeof Widget => 类本身的类型(含构造函数)
function createWidget(Ctor: typeof Widget, w: number, h: number): Widget {
  return new Ctor(w, h);
}

override 关键字

TypeScript 4.3 引入 override,显式标记子类中覆写父类方法的意图:

class BaseFormatter {
  format(val: string): string {
    return val.trim();
  }
}

class UpperFormatter extends BaseFormatter {
  override format(val: string): string {
    return val.trim().toUpperCase();
  }

  // override nonExistent() {} // 编译错误:父类中不存在
}

配合 tsconfig.json 中的 noImplicitOverride: true,可以强制所有覆写都使用 override 关键字。这能防止父类重命名方法后子类「空转」的问题:

{
  "compilerOptions": {
    "noImplicitOverride": true
  }
}

开启后,子类中覆写父类方法但没有标记 override 的地方都会报错,帮助你在编译期发现”以为覆写了但实际没生效”的隐蔽问题。

综合实战:通知调度器

interface DeliveryReport {
  channelName: string;
  recipient: string;
  success: boolean;
  sentAt: number;
}

interface Disposable {
  dispose(): void;
}

abstract class Channel implements Disposable {
  abstract readonly name: string;
  abstract send(to: string, body: string): boolean;

  dispose(): void {
    console.log(`${this.name} channel disposed`);
  }
}

class InAppChannel extends Channel {
  readonly name = "InApp";

  send(to: string, body: string): boolean {
    console.log(`Push notification to user ${to}: ${body}`);
    return true;
  }
}

class Dispatcher {
  private channels: Channel[] = [];
  private reports: DeliveryReport[] = [];

  register(channel: Channel): void {
    this.channels.push(channel);
  }

  broadcast(recipients: string[], body: string): DeliveryReport[] {
    const batch: DeliveryReport[] = [];
    for (const ch of this.channels) {
      for (const to of recipients) {
        const ok = ch.send(to, body);
        const report: DeliveryReport = {
          channelName: ch.name,
          recipient: to,
          success: ok,
          sentAt: Date.now(),
        };
        batch.push(report);
      }
    }
    this.reports.push(...batch);
    return batch;
  }

  shutdown(): void {
    this.channels.forEach(ch => ch.dispose());
    this.channels = [];
  }
}

// EmailChannel 和 SmsChannel 来自上方"抽象类"小节的定义
const dispatcher = new Dispatcher();
dispatcher.register(new EmailChannel());
dispatcher.register(new SmsChannel());
dispatcher.register(new InAppChannel());

const results = dispatcher.broadcast(
  ["user-a", "user-b"],
  "Your order ORD-300 has been shipped"
);
dispatcher.shutdown();

本章回顾

概念说明关键语法
访问修饰符控制成员可见性public / private / protected
readonly初始化后不可修改readonly prop: Type
参数属性构造函数参数自动成为属性constructor(public name: string)
存取器拦截属性读写get prop() / set prop(val)
static属于类本身的成员static member
abstract不可实例化的基类abstract class / abstract method()
implements类实现接口契约class A implements B, C
override显式标记方法覆写override method()
结构化类型结构相同即兼容TypeScript 核心原则
类名作类型类名表示实例类型typeof ClassName 表示类本身类型

购买课程解锁全部内容

告别类型错误:12 章掌握 TypeScript 工程实战

¥29.90