Skip to content
接口、抽象类与实体类对比
概述
接口(Interface)、抽象类(Abstract Class)与实体类(Concrete Class)是面向对象设计中处在不同抽象层次的三类工具。接口只描述“能做什么”,不含任何实现;抽象类在接口之上提供部分实现并预留扩展点;实体类补足所有细节,可以直接创建对象。它们在同一套类型体系里分层协作,而非相互替代。
基本概念
接口
接口只描述行为,不包含任何实现。一个类可以同时实现多个接口,从而组合多种契约。TypeScript 中的接口在编译后会被完全擦除,不会出现在运行时代码中。
ts
interface Logger {
log(message: string): void;
}
interface Formatter {
format(data: unknown): string;
}抽象类
抽象类无法直接用 new 实例化。它可以包含既有具体实现的方法,也有未实现的抽象方法。子类必须实现所有抽象方法,同时可以直接复用抽象类中已实现的部分。
ts
abstract class Reporter {
abstract fetchData(): Record<string, unknown>[];
generateReport(): string {
const data = this.fetchData();
return JSON.stringify(data, null, 2);
}
}实体类
实体类拥有完整的实现,可以直接 new 创建对象。它通常继承自某个抽象类,填补其中所有的抽象成员。
ts
class SalesReporter extends Reporter {
fetchData() {
return [
{ product: 'A', amount: 100 },
{ product: 'B', amount: 230 }
];
}
}工作原理
接口仅存在于 TypeScript 的编译阶段。类型检查器会验证 implements 的类是否满足接口所定义的结构(鸭子类型),不要求显式的继承声明。运行时没有接口信息。
抽象类经过编译会生成构造函数与原型链,但其中的抽象方法会被编译器完全移除,不产生任何代码。如果绕过类型检查直接实例化抽象类,原型链上找不到对应方法,调用时会直接抛出 TypeError。正常情况下编译器会阻止直接实例化,该场景只在使用类型断言或 any 绕过时出现。
实体类的实例化流程与普通类一致:new 调用构造函数,super() 初始化父类状态,原型链上的方法均可用。
基本用法
接口实现
ts
interface Named {
name: string;
}
interface Sizable {
size(): number;
}
class NamedBlob implements Named, Sizable {
name: string;
private data: Uint8Array;
constructor(name: string, data: Uint8Array) {
this.name = name;
this.data = data;
}
size() {
return this.data.byteLength;
}
}一个类实现多个接口时,必须满足所有接口规定的成员。编译器会逐个检查,缺少任意一个都会报错。
抽象类与实体类配合
抽象类中的具体方法可以调用抽象方法,形成模板方法模式。这让子类只需关注变动的部分。
ts
abstract class Validator {
abstract validate(input: string): boolean;
validateOrThrow(input: string) {
if (!this.validate(input)) {
throw new Error(`Validation failed for input: ${input}`);
}
}
}
class EmailValidator extends Validator {
validate(input: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
}
}
const validator = new EmailValidator();
validator.validateOrThrow('a@b.com'); // 通过validateOrThrow 里的逻辑对所有子类都一样,子类只负责实现具体的 validate 规则。
组合替代深继承
继承层级过深时(例如超过 3 层),排查行为和修改基类的成本会明显上升。这时可以把可变行为封装成独立的接口,通过依赖注入交给实体类使用。
ts
interface DiscountStrategy {
apply(price: number): number;
}
class PercentageDiscount implements DiscountStrategy {
constructor(private percent: number) {}
apply(price: number) {
return price * (1 - this.percent / 100);
}
}
class Order {
constructor(
private discount: DiscountStrategy,
private items: number[]
) {}
total(): number {
const sum = this.items.reduce((a, b) => a + b, 0);
return this.discount.apply(sum);
}
}
const order = new Order(new PercentageDiscount(20), [100, 200]);
console.log(order.total()); // 240Order 不会因折扣方式不同而派生出多个子类,折扣行为被抽离到独立的策略对象中,可在运行时替换。
示例
接口隔离与多态
下面的示例通过接口隔离不同职责,并在上下文中只依赖所需的部分。
ts
interface Readable {
read(): string;
}
interface Writeable {
write(data: string): void;
}
class BufferStorage implements Readable, Writeable {
private data = '';
read() { return this.data; }
write(data: string) { this.data = data; }
}
function print(source: Readable) {
console.log(source.read());
}
const storage = new BufferStorage();
storage.write('hello');
print(storage); // helloprint 仅依赖 Readable,传入的对象是否额外实现了 Writeable 不影响该函数的类型检查。这正是接口隔离带来的依赖倒置。
抽象类共享公共逻辑
ts
abstract class Character {
constructor(protected name: string, protected health: number) {}
abstract attack(): number;
takeDamage(amount: number) {
this.health -= amount;
if (this.health <= 0) {
console.log(`${this.name} defeated`);
}
}
isAlive(): boolean {
return this.health > 0;
}
}
class Warrior extends Character {
attack() { return Math.random() * 20 + 10; }
}
class Mage extends Character {
attack() { return Math.random() * 30 + 5; }
}
const w = new Warrior('Conan', 100);
const m = new Mage('Gandalf', 80);
w.takeDamage(m.attack());
console.log(w.isAlive() ? 'alive' : 'defeated');受伤计算、状态打印等通用逻辑统一放在 Character 中,子类只需实现攻击力算法。isAlive() 方法也由抽象类提供,外部不必重复判断健康值。当需要多个共享行为同时又要禁止直接实例化基类时,这种模式可以直接使用。
接口与抽象类协同
抽象类可以实现接口,并留出一部分方法让子类补充。
ts
interface Serializable {
serialize(): string;
}
abstract class ConfigSection implements Serializable {
abstract serialize(): string;
abstract validate(): boolean;
export(): string {
if (!this.validate()) {
throw new Error('Validation failed');
}
return this.serialize();
}
}
class DatabaseConfig extends ConfigSection {
constructor(private host: string, private port: number) { super(); }
validate() { return this.port > 0 && this.port < 65536; }
serialize() { return `DB:${this.host}:${this.port}`; }
}
const db = new DatabaseConfig('localhost', 5432);
console.log(db.export()); // DB:localhost:5432ConfigSection 实现了 Serializable,同时增添了验证步骤并要求子类实现 serialize 与 validate。外部使用者只需调用 export,不需要关心内部的拼接与校验。
注意点
- 接口不应退化为字段清单。 如果接口主要由 getter/setter 组成而没有行为,考虑是否仅需一个纯数据类型(TypeScript 中可用
type),或者设计上缺少应有的方法。 - 抽象类中已实现的方法占比不宜过高。 抽象类的职责是提供模板和部分实现。如果已实现方法的数量远超抽象方法,说明过多的业务逻辑进入了抽象层。此时可将公共逻辑提取到独立的工具类或服务中,通过组合注入。
- 避免深继承链。 继承层级超过 3 层时,基类的修改会波及所有子类,问题定位也需要在多个层级间跳转。优先考虑组合——将可变行为封装为独立接口并在使用时装配,而不是通过多层子类扩展。
限制
抽象类不能直接实例化,因此无法为“只需要默认实现”的场景提供零扩展的解决方案。如果需要默认实现,必须创建一个实体子类甚至匿名类。
在 TypeScript 中,interface 与 type 在很多场景可以互换,但存在以下关键差异:
interface支持声明合并(多次声明同一接口会自动合并),适合对外暴露的公共 API,便于使用方扩展。type支持联合类型、交叉类型等,适合复杂的类型运算,但不支持声明合并。implements子句可以使用interface、类名,也可以使用type定义的对象类型。例如:ts需要注意,如果type Named = { name: string }; class Person implements Named { name: string = ''; }type定义为联合类型或非对象类型(如原始类型别名),则无法直接用于implements。
因此,对外提供可扩展的 API 时适合用 interface;模块内部的类型推导、联合类型运算适合用 type。
应用
- 定义跨模块契约 —— 使用接口。零实现依赖,抽象成本最低。
- 共享部分逻辑并强制子类实现特定方法 —— 使用抽象类。兼顾代码复用与契约约束。
- 需要直接工作的功能单元 —— 使用实体类。所有实现已就绪,可直接实例化。
从演化视角看,当多个接口实现开始出现重复代码时,可以在接口和实体类之间插入抽象类来承载公共逻辑;当抽象类中的具体方法过多时,则可以将这些方法抽取为独立服务,再通过组合注入给实体类。
参考链接
- TypeScript Handbook: Everyday Types – Interfaces
- TypeScript Handbook: Classes
- TypeScript Handbook: Abstract Classes
- MDN: Inheritance and the prototype chain
- Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software.(组合优于继承的相关论述)
