面向对象工程实践:用类构建可扩展的业务模块
需求场景
订单系统需要一套可扩展的通知模块。产品要求支持三种通知渠道:邮件、短信、站内信。每种渠道有各自的发送逻辑,但共享统一的日志记录、重试机制和序列化接口。未来还可能新增企业微信、钉钉等渠道。
这是一个典型的面向对象设计问题:需要定义抽象的通知基类、约定公共契约、控制内部实现的可见性、支持灵活扩展。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 设置不同的访问修饰符(如
publicgetter +protectedsetter),该特性仍在社区讨论中。
静态成员
静态成员通过类名访问,不属于任何实例:
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"
静态成员也可以使用访问修饰符,public 和 protected 的静态成员会被子类继承:
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");
抽象类和抽象成员的规则:
- 抽象成员只能存在于抽象类中
- 抽象成员只声明签名,不提供实现
- 抽象成员不能标记为
private,因为子类需要访问并实现它 - 一个类只能继承一个抽象类(不支持多重继承)
接口实现(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(); // 通过:结构相同
但含有 private 或 protected 成员时,必须来自同一声明才兼容:
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