面向对象基础
第一部分:基本概念
什么是面向对象编程
通过编程解决问题的过程就是使用代码对现实问题建模的过程,“面向”是说我们选择某一个视角来建模复杂的业务问题。
- 面向过程编程
关注"做什么"—— 把问题分解为一系列步骤或操作
// 面向过程的思维 function createUser(name: string, age: number) { validateName(name); validateAge(age); saveToDatabase(name, age); sendWelcomeEmail(name); }
什么是面向对象编程
- 面向对象编程
关注"谁来做"—— 把问题分解为一组对象,让对象之间协作
// 面向对象的思维 class User { constructor(name: string, age: number) { this.name = name; this.age = age; } validate() { this.validateName(); this.validateAge(); } save() { // 保存到数据库 } sendWelcomeEmail() { // 发送欢迎邮件 } }
数据与对其进行的操作集中在一起,更有利于抽象和复用
对象是什么
就像现实世界中的事物一样,对象包含两个基本要素:
以汽车为例:
- 属性(特征):描述对象是什么或处于什么状态
- 方法(行为):描述对象能做什么,可能会随状态变化,也可能修改状态
- 属性:品牌、颜色、速度、油量等
- 方法:启动、加速、刹车、加油等
在编程中,可以使用类(class)来描述并重复生成同一种对象。
类是什么
在编程中:
- 类定义了某一类事物应该具有的属性(特征)和方法(行为)
- 对象是类的具体实例,拥有类定义的所有特征,但具有自己的独特状态
类比汽车制造过程:
- 类(class)就像汽车的设计图纸
- 对象(object)则是根据这份图纸制造出来的具体汽车
- 同一份图纸可以制造出多辆外观、颜色不同,但基本结构相同的汽车
类的结构
- 属性 property
属性是类中用来存储数据的变量,描述了对象的特征或状态。
class Car { brand: string; speed: number; }
类的结构
- 方法 method
方法描述了对象可以执行的操作
class Car { accelerate() { // 加速逻辑 } brake() { // 刹车逻辑 } }
“函数”与“方法”
- “函数”是通用的概念:一段独立的代码块接受输入,执行特定任务并返回;
- “方法”是 OOP 里的概念,必须通过对象调用;
类的结构
- 构造函数
class Car { brand: string; speed: number; constructor(brand: string) { this.brand = brand; this.speed = 0; } }
初始化对象的特殊方法,创建对象时自动调用
除了少数特殊情况,所有属性必须在构造函数中初始化完成,否则创建出的对象将处于非法状态
类的结构
- 构造函数
class Plugin { // 使用 ! 声明该属性会延迟初始化,而不是在构造函数里初始化 private config!: Config; initialize(config: Config) { this.config = config; //... 初始化 config } loadDocument() { this.config.getSomeValue(); // ... } }
在某些具备特殊生命周期的类里,属性只在特定特定生命周期后初始化和访问。这种情况通过声明延迟初始化该属性,可以延后初始化。以 TS 为例:
使用该特性时,必须严格确保该属性只会在初始化后访问
类的结构
- 构造函数
class User { // 参数属性语法:自动创建并初始化实例属性 constructor( private name: string, // 自动创建私有属性 public age: number = 20, // 可以设置默认值 readonly id: string // 只读属性 ) {} }
TS 里,构造函数支持参数属性语法
添加任一下列修饰符,可以自动创建并初始化实例属性:public, protected, private, readonly
// 传统写法 class User { private name: string; public age: number; readonly id: string; constructor(name: string, age: number, id: string) { this.name = name; this.age = age; this.id = id; } }
类的结构
- 访问器方法 getter & setter
getter 和 setter 是两种特殊方法,主要用来封装访问属性和修改对象属性的行为。
class Person { private _age: number; // 私有属性 // getter get age(): number { return this._age; } // setter with validation set age(value: number) { if (value >= 0 && value <= 150) { this._age = value; } else { throw new Error("年龄必须在0-150之间"); } } }
类的结构
- 访问器方法 getter & setter
getter 和 setter 是两种特殊方法,主要用来封装访问属性和修改对象属性的行为。
class Person { private _age: number; // 私有属性 // getter get age(): number { return this._age; } // setter with validation set age(value: number) { if (value >= 0 && value <= 150) { this._age = value; } else { throw new Error("年龄必须在0-150之间"); } } }
const person = new Person(); // 调用 setter person.age = 25; // 调用 getter,输出25 console.log(person.age);
类的结构
-
访问器方法 getter & setter
- 使用场景1:保护内部数据,控制访问方式
class User {
private _password: string;
get password(): string {
return "********"; // 永远不返回真实密码
}
set password(value: string) {
this._password = value; // 密码只能设置,不能查看
}
} const user = new User();
user.password = "123456";
console.log(user.password); // 输出: ******** 只提供 getter 可以实现只读属性,也是类似的使用场景
类的结构
-
访问器方法 getter & setter
- 使用场景2:校验输入数据,确保符合业务规则
class Person { private _age: number; get age(): number { return this._age; } set age(value: number) { if (value < 0 || value > 150) { throw new Error("年龄必须在0-150之间"); } this._age = value; } }
const person = new Person();
person.age = 20; // 正常
person.age = -1; // 抛出错误:年龄必须在0-150之间 类的结构
-
访问器方法 getter & setter
- 使用场景3:基于其他属性动态计算的计算属性(computed)
class Rectangle { private _width: number; private _height: number; constructor(width: number, height: number) { this._width = width; this._height = height; } // 计算属性:面积 get area(): number { return this._width * this._height; } }
const rect = new Rectangle(10, 5);
console.log(rect.area); // 输出: 50 类的结构
- 访问器方法 getter & setter
相比于提供 setWidth, getWidth, getArea 等显式的方法,优势在于:
- 在具备上述能力的同时提供更自然的属性读写的语法
- 某些情况下,调用方无法将属性读写修改为显式的方法调用
类的结构
- 静态属性
class Car { static readonly MAX_SPEED = 200; static readonly DEFAULT_COLOR = 'black'; private speed: number; private color: string; constructor(color?: string) { this.speed = 0; this.color = color ?? Car.DEFAULT_COLOR; } }
属于类本身而不是实例的属性,一般用于定义与该类相关的常量或配置。
编程语言不限制定义可变的静态属性,但这种用法与全局变量近似,会带来很多问题,因此推荐只定义只读的静态属性。
// 使用示例
console.log(Car.MAX_SPEED); // 200
console.log(Car.DEFAULT_COLOR); // 'black' 类的结构
- 静态方法
class User { constructor(private name: string) {} // 工厂方法:创建特定对象 static createGuest(): User { return new User("访客"); } // 工具方法:提供通用功能 static formatName(name: string): string { return `用户: ${name}`; } }
属于类本身而不是实例的方法,一般用于定义与该类相关的工具函数,工厂方法等。
// 使用示例
const guest = User.createGuest();
console.log(User.formatName("张三")); // "用户: 张三" 类的结构
- 访问控制修饰符
访问控制修饰符用于限定类成员(属性和方法)的访问范围,是实现封装的关键工具。
-
public (TS 的默认行为)
- 对外公开的属性或方法
- 可以在任何地方访问
-
protected
- 需要在继承链中共享但不想公开的属性或方法
- 只能在类内部和子类中访问
-
private
- 类内部使用的属性或方法
- 只能在类内部访问
-
package-private(Java 特有,且是其默认行为)
- 同一个包 (package) 内的类可以访问的属性或方法
OOP 的三大特点:
封装、继承、多态
类的结构
- 访问控制修饰符
先给所有属性和方法加上 private,直到有明确使用场景时再扩大权限
最大程度保护类的实现细节,只开放真正需要的访问权限
- 接口一旦暴露出去,就会增加维护成本
- 优先使用最严格的访问级别 private
- 谨慎使用 protected 和 public
类的结构
- 访问控制修饰符
私有构造函数:TS 和 Java 都支持将构造函数声明为 private,从而限制外部创建实例。常见的使用场景包括:
- 单例模式
- 工厂模式
- 纯静态工具类
- 防止继承
类的结构
- 访问控制修饰符
私有构造函数:单例模式
class Singleton { private static instance: Singleton; // 私有构造函数,防止外部实例化 private constructor() { } // 静态方法,返回唯一实例 public static getInstance(): Singleton { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } public sayHello() { console.log("Hello from Singleton!"); } }
类的结构
- 访问控制修饰符
私有构造函数:工厂模式
class Product {
private name: string;
// 私有构造函数,外部无法直接实例化
private constructor(name: string) {
this.name = name;
}
// 静态工厂方法,用于创建类的实例
public static createProduct(name: string): Product {
return new Product(name);
}
// 获取产品名称的方法
public getName(): string {
return this.name;
}
}类的结构
- 访问控制修饰符
私有构造函数:纯静态工具类
class Utility { private constructor() {} // 私有构造函数 static add(a: number, b: number): number { return a + b; } }
在 TS 里,我们不必要完全抛弃非 OOP 写法,像这种纯静态类其实也可以定义为导出多个常规函数的 module
类的结构
- 访问控制修饰符
私有构造函数:防止继承
class Final { private constructor() {} // 私有构造函数,禁止继承或实例化 } class Derived extends Final {} // 错误:无法继承
在 Java 里可以通过 final class 实现类似的效果
类的结构
- 继承:允许一个类扩展另一个类并继承其属性和方法。子类可以重写父类的方法,或者调用父类的构造函数和方法。
class Animal { public name: string; private age: number; protected type: string; constructor(name: string, age: number) { this.name = name; this.age = age; this.type = 'animal'; } public makeSound(): void { console.log('Some sound'); } private getAge(): number { return this.age; } protected getInfo(): string { return `${this.name} is ${this.age} years old`; } }
class Dog extends Animal { constructor(name: string, age: number) { // 调用父类构造函数 super(name, age); // 可以访问 protected 属性 this.type = 'dog'; } // 重写父类方法 public makeSound(): void { console.log('Woof!'); } // 新增子类方法 public showInfo(): void { // console.log(this.age); // 错误 console.log(this.type); console.log(this.name); console.log(this.getInfo()); } }
OOP 的三大特点:
封装、继承、多态
类的结构
- 抽象类:使用 abstract class 关键字定义抽象类,用于表达不完整但具有共同特征的概念
// 抽象类 abstract class Shape { // 普通属性 protected color: string; // 构造函数 constructor(color: string) { this.color = color; } // 抽象方法:子类必须实现 abstract calculateArea(): number; abstract calculatePerimeter(): number; // 具体方法:子类可以直接使用 public getInfo(): string { return `这是一个${this.color}的图形,面积为${this.calculateArea()}`; } }
// 具体类:圆形
class Circle extends Shape {
private radius: number;
constructor(color: string, radius: number) {
super(color); // 调用父类构造函数
this.radius = radius;
}
// 实现抽象方法
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
} 类的结构
- 抽象类
- 抽象类不能被直接实例化,只能通过具体子类实例化
- 抽象类要至少包含一个 abstract 的属性或方法
- 具体类是继承或实现了所有 abstract 属性和方法的子类,对应一种确实存在,功能完整的对象
典型例子:
- 生物分类:动物、植物(抽象) → 狗、猫、树(具体)
- 食物种类:主食、零食(抽象) → 米饭、面包、薯片(具体)
- 职业类型:工人、艺术家(抽象) → 木工、画家(具体)
这种区分不是绝对的,取决于设计的抽象程度
与抽象类对应的是具体类
类的结构
- 抽象类
常见的抽象类的使用场景:
-
泛化的事物类别
- 比如"动物":动物会吃东西、运动,但"动物"本身是个抽象概念,现实中不存在"一般的动物",必须是具体的狗、猫等才能存在
-
包含部分已知特征的模板
- 比如"交通工具":它一定能运输、有速度,但具体如何运输(飞、开、游)需要具体类型来定义,汽车、飞机等才是完整的实现
-
需要统一规范的基础设施
- 比如"数据库连接":知道它需要连接、查询、关闭,但具体的连接方式取决于数据库类型,MySQL、MongoDB 等实现具体细节
继承和抽象类是为了代码重用,但要谨慎使用,避免不合理的抽象或过深的继承带来的维护性问题。后面会展开介绍。
继承的问题
- is-a 关系脆弱
继承表达的是“子类”是“父类”特例的情况,即“子类对象 is a 父类对象”,但真实世界里很少存在这样稳定的 is-a 关系。
abstract class Bird { fly(): void { console.log("我在飞!"); } walk(): void { console.log("我在走路"); } makeSound(): void { console.log("我在叫"); } }
// 鸭子继承鸟类
class Duck extends Bird {
makeSound(): void {
console.log("嘎嘎嘎");
}
}
// 鸡继承鸟类
class Chicken extends Bird {
makeSound(): void {
console.log("咯咯咯");
}
}
// 看起来工作得很好
const duck = new Duck();
duck.fly(); // 输出:我在飞!
duck.makeSound(); // 输出:嘎嘎嘎 继承的问题
- is-a 关系脆弱
继承表达的是“子类”是“父类”特例的情况,即“子类对象 is a 父类对象”,但真实世界里很少存在这样稳定的 is-a 关系。
abstract class Bird { fly(): void { console.log("我在飞!"); } walk(): void { console.log("我在走路"); } makeSound(): void { console.log("我在叫"); } }
// 企鹅出现了...
class Penguin extends Bird {
// 企鹅不会飞,但继承了 fly 方法
fly(): void {
// 这很尴尬
throw new Error("我不会飞!");
}
swim(): void {
console.log("我在游泳!");
}
}继承的问题
- is-a 关系脆弱
abstract class Bird { fly(): void { console.log("我在飞!"); } walk(): void { console.log("我在走路"); } makeSound(): void { console.log("我在叫"); } }
- 它会叫(发出声音),但声音是电子的
- 它能飘在水上,但不是游泳
生活中没有鸟,鸟只是一个抽象, 我们生活中有鸡,有鸭。
我们认为它们有一些相同的地方,于是把拥有这些相同点的东西叫做鸟,但永远不确定下一个遇见的能不能算鸟, 鸟的定义要不要修改。
如果新来了橡皮鸭子,它是鸟吗?
继承的问题
- is-a 关系脆弱
class Rectangle { private width: number; private height: number; // ...省略构造函数 public setWidth(width: number) { this.width = width; } public setHeight(height: number) { this.height = height; } public getArea() { return this.width * this.height; } }
class Square extends Rectangle {
// ...省略构造函数
public setWidth(width: number) {
// 修改宽度时,高度也要跟着改变
super.setWidth(width);
super.setHeight(width);
}
public setHeight(height: number) {
// 修改高度时,宽度也要跟着改变
super.setWidth(width);
super.setHeight(width);
}
}一个正方形是一个矩形吗?
继承的问题
- 子类与父类之间耦合
子类在重写父类属性或方法时,常常需要了解其实现细节才能不破坏其原有行为。父类为了让子类通过重写扩展其能力,不得不向其暴露更多内部细节。
// 基础订单类 class Order { protected price: number; protected quantity: number; constructor(price: number, quantity: number) { this.price = price; this.quantity = quantity; } getTotal(): number { return this.price * this.quantity; } }
// 折扣订单继承自基础订单 class DiscountOrder extends Order { private discount: number; constructor( price: number, quantity: number, discount: number ) { super(price, quantity); this.discount = discount; } getTotal(): number { // 依赖了父类的实现细节: // 假设父类就是简单的 price * quantity return super.getTotal() * (1 - this.discount); } }
继承的问题
- 单继承无法组合多个功能
在 TS 和 Java 里,子类同时只能继承一个父类,要用复用不同功能必须通过多层继承来叠加功能,这非常不灵活且难以维护。
class Animal { eat() { console.log("This animal is eating."); } } class FlyingAnimal extends Animal { fly() { console.log("This animal can fly."); } } class SwimmingAnimal extends Animal { swim() { console.log("This animal can swim."); } }
// 子类:鸟类(只能飞)
class Bird extends FlyingAnimal {}
// 子类:鱼类(只能游泳)
class Fish extends SwimmingAnimal {}
// 如果需要一个既能飞又能游泳的动物
// 需要通过多层继承
class Penguin extends SwimmingAnimal {
fly() {
console.log(
"Penguins can't fly, but let's pretend they can."
);
}
}interface
- interface 只定义契约,不提供实现
- 实现 interface 的类必须遵守接口契约
- 一个类可以同时实现多个 interface,从而灵活组合功能
// 定义接口:飞行能力 interface Flyable { fly(): void; } // 定义接口:游泳能力 interface Swimmable { swim(): void; } // 基础类:动物 class Animal { eat(): void { console.log("This animal is eating."); } }
class Bird extends Animal implements Flyable { fly(): void { console.log("This bird can fly."); } } class Fish extends Animal implements Swimmable { swim(): void { console.log("This fish can swim."); } } class Penguin extends Animal implements Flyable, Swimmable { fly(): void { console.log("This penguin can fly"); } swim(): void { console.log("This penguin can swim."); } }
interface
// 定义接口:飞行能力 type Flyable = { fly(): void; } // 基础类:动物 class Animal { eat(): void { console.log("This animal is eating."); } } class Bird extends Animal implements Flyable { fly(): void { console.log("This bird can fly."); } }
- 在 TS 里,所有类型都可以当作 interface 使用
- 一种是通用概念:OOP 里可以被 implements 的接口
- 另一种是 TS 里的关键字 interface
注意区分两种 interface 的概念:
interface
-
TS 里无法在运行时判断一个实例是否实现了某个 interface
- 不能使用 object instanceof FooInterface
- 可以基于 duck type 的特性使用自定义 type guard 代替
- Java 可以使用 object instanceof FooInterface
interface Person { name: string; age: number; } function isPerson(value: unknown): value is Person { return ( typeof value === "object" && value !== null && "name" in value && "age" in value && typeof (value as Person).name === "string" && typeof (value as Person).age === "number" ); }
接口与实现分离
-
将接口与实现分开,使程序的使用者只需要了解接口的定义,而不需要关心具体的实现细节。
- 注意:这里所说的接口是指抽象的概念,与前面的 interface 不同
- 在实际编程中,接口可以是抽象类或 interface
// 定义接口(描述行为) interface Logger { log(message: string): void; error(message: string): void; } // 使用接口而非具体实现 function doSomething(logger: Logger) { logger.log("a log message."); logger.error("an error message."); }
// 实现接口的具体类
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`Log: ${message}`);
}
error(message: string): void {
console.error(`Error: ${message}`);
}
}
// 创建具体实现并传递给函数
const logger = new ConsoleLogger();
doSomething(logger); 多态
- 多态是指同一个接口在不同的类中具有不同的实现
OOP 的三大特点:
封装、继承、多态
多态允许我们通过统一的方式调用不同对象的行为,而不需要关心这些对象的具体类型。具体来说,它有两种表现形式:
- 子类继承父类,重写其方法,提供不同行为
- 不同类实现同一个接口,并提供不同行为
// 定义一个父类
class Animal {
speak(): void {
console.log("Animal makes a sound");
}
}
// 使用多态
function makeAnimalSpeak(animal: Animal) {
animal.speak(); // 调用的是具体对象的实现
}
class Dog extends Animal {
speak(): void {
console.log("Dog barks");
}
}
class Cat extends Animal {
speak(): void {
console.log("Cat meows");
}
}
// 调用统一的方法
makeAnimalSpeak(new Dog());// Dog barks
makeAnimalSpeak(new Cat());// Cat meows 组合 Composition
- 将对象分解为更小的、可复用的组件,并通过组合这些组件来构建复杂的功能
class Car {
startEngine(): void {
console.log("Engine starts.");
}
stopEngine(): void {
console.log("Engine stops.");
}
} 使用组合改造该实现:
// 子类:电动车 class ElectricCar extends Car { chargeBattery(): void { console.log("Battery is charging."); } // 重写引擎启动方法,适配电动车的行为 startEngine(): void { console.log("Electric motor starts silently."); } } // 子类:燃油车 class GasolineCar extends Car { refuel(): void { console.log("Refueling the car."); } // 重写引擎启动方法,适配燃油车的行为 startEngine(): void { console.log("Gasoline engine starts with a roar."); } }
组合
// 引擎接口 interface Engine { start(): void; stop(): void; } // 汽车类,通过组合引擎实现功能 class Car { private engine: Engine; constructor(engine: Engine) { this.engine = engine; // 通过依赖注入传入引擎 } startEngine(): void { this.engine.start(); } stopEngine(): void { this.engine.stop(); } }
- 使用组合实现
// 燃油引擎实现
class GasolineEngine implements Engine {
start(): void {
console.log("Gasoline engine starts with a roar.");
}
stop(): void {
console.log("Gasoline engine stops.");
}
}
// 电动引擎实现
class ElectricEngine implements Engine {
start(): void {
console.log("Electric motor starts silently.");
}
stop(): void {
console.log("Electric motor stops.");
}
}
const gasolineCar = new Car(new GasolineEngine());
const electricCar = new Car(new ElectricEngine());组合
-
组合的优势
- 低耦合:通过接口或对象引用来实现功能,组件之间的耦合较低
- 更灵活:可以动态地组合不同的组件来实现不同的功能,而不需要依赖固定的继承关系
- 更好的复用性:组件独立于类的层次结构,方便在不同的上下文中复用
- 继承:类之间存在 “is-a” 关系
- 组合:类之间存在 “has-a” 关系
聚合 Aggregation
-
组合:部分不能脱离整体存在,整体被销毁时部分也会被销毁
- 引擎是汽车不可分割的一部分
- 汽车报废时,引擎作为其组成部分也会一同报废
- 引擎的生命周期完全依附于汽车
-
聚合:部分可以独立于整体而存在。即使整体被销毁,部分仍可以存在
- 图书可以在不同图书馆之间转移
- 即使图书馆关闭,图书依然可以存在
- 一本书可以被多个图书馆收藏
不同类别的对象
- 在经典的面向对象系统中,根据职责可以将对象分为以下几类:
-
实体(Entity)
- 具有唯一标识符(ID)的业务对象
- 在整个生命周期中保持 ID 不变
- 通常映射到数据库中的记录
- 包含业务逻辑和行为
- 例如: User, Order, Furniture
-
值对象(Value Object)
- 没有唯一标识符
- 通过其属性值来判断相等性
- 一般不可变(Immutable)
- 用于描述、计量或度量
- 例如: Point2d, Matrix4, Date, Color
不同类别的对象
- 在经典的面向对象系统中,根据职责可以将对象分为以下几类:
-
服务(Service)
- 封装特定的业务逻辑
- 一般无状态从而可以重复执行操作得到相同结果
- 提供操作或计算功能,处理跨实体的业务逻辑
- 例如: PaymentService, RenderService
对象的生命周期
-
创建(Creation)
- 调用构造函数
- 初始化属性
-
使用(Usage)
- 访问属性
- 调用方法
-
销毁(Destruction)
- 调用析构函数(如果有)
-
释放资源
- 断开数据库/文件读写
- 取消异步任务
- 移除事件监听
系统设计时要考虑清楚:对象会被谁创建和使用,会在什么时机创建,什么时机销毁。
良好的生命周期管理可以帮助我们避免资源泄露,确保对象正确初始化。
对象可变性
- Mutable 对象:创建后状态(属性)可以被改变
- Immutable 对象:创建后状态(属性)不能被改变,任何修改都会返回新对象
class Vector2 { constructor( private readonly x: number, private readonly y: number ) {} // 过去时态暗示返回新实例 added(other: Vector2): Vector2 { return new Vector2(this.x + other.x, this.y + other.y); } subtracted(other: Vector2): Vector2 { return new Vector2(this.x - other.x, this.y - other.y); } equals(other: Vector2): boolean { return this.x === other.x && this.y === other.y; } }
对象可变性
-
Immutable 对象的使用场景
-
值对象(Value Objects)
- 代表一个确定的值或状态,如金额、坐标、时间点,业务规则要求数据一旦创建就不应被修改
-
缓存场景:
- 可以安全地被多处代码共享和缓存,无需担心缓存失效问题
-
Java 里部分场景:
- HashMap、HashSet 等集合中作为键
- 线程安全要求
-
值对象(Value Objects)
-
代价
- 每次修改都会创建新对象,可能带来性能开销
- 某些场景下导致代码冗长
对象序列化
- 序列化:将对象转换为可传输或存储的格式
- 反序列化:将数据重新转换为可用的对象实例
- 常见的序列化格式:JSON、ProtocolBuffer 等
最基础的序列化和反序列化:手动转换
class User {
constructor(
public name: string,
public age: number,
) {}
// 添加序列化方法
toJSON() {
return {
name: this.name,
age: this.age
};
}
// 添加静态反序列化方法
static fromJSON(json: string): User {
const data = JSON.parse(json);
return new User(data.name, data.age);
}
}对象序列化
更进一步地,可以使用序列化库来简化操作:
- Java 里的 Gson 库
- TS 里的 @qunhe/json-mapper
//json object const j = { a: 1, b: "", c: { d: 3, e: -1 }, cs: [{ d: 4 }, { d: 5 }], d: 33 }
import { JsonProperty, deserialize, serialize } from '@qunhe/json-mapper';
////对应类定义:在需要进行序列化和反序列化的字段上加上注解。
class A {
@JsonProperty()
a: number;
@JsonProperty()
b: string;
@JsonProperty({ excludeToJson: true }) //这个字段不会被序列化到json中
c: B;
@JsonProperty({ class: B }) // 必须写明类型,
cs: B[]
@JsonProperty('d') //将json的映射为 md
private md: number;
}
//反序列化
const a = deserialize(A, j); //a is A
//序列化
const aj = serialize(a); // aj is an objectUML 类图

package
interface
抽象类
类

B 继承 A
B 实现 A

A 聚合 B
A 组合 B
第二部分:设计原则
依赖管理
-
一个模块应该依赖于比它更稳定的模块(Depend in the direction of stability)
- 稳定性指的是模块在系统中发生变化的可能性。
- 一个模块越稳定,意味着它越不容易被修改或破坏,通常是因为它被广泛使用或具有较高的抽象性。
Model
UI
框架/通用模块
业务模块
小心循环依赖,它暗示了模块设计可能有问题
依赖管理
- 什么是依赖
- 如果对象 Foo 需要对象 Bar 来完成其工作的一部分,那么 Bar 就被称为 Foo 的依赖(dependency)。
class DashboardController {
public execute(): void {
const recentPosts = Cache.has('recent_posts') ?
Cache.get('recent_posts') :
[];
// ...
}
}避免隐式依赖
class DashboardController { public execute(): void { const recentPosts = Cache.has('recent_posts') ? Cache.get('recent_posts') : []; // ... } }
class DashboardController { private cache: Cache; constructor(cache: Cache) { this.cache = cache; } public execute(): void { const recentPosts = this.cache.has('recent_posts') ? this.cache.get('recent_posts') : []; // ... } }
-
静态依赖(如调用静态方法或全局变量)会隐藏实际依赖,导致代码难以理解和测试。
- 解决方法是将这些依赖封装为接口,通过构造函数注入。
抽象程度要随稳定性要求增加
-
一个模块如果需要非常稳定(被许多其他模块依赖),那么它应该是抽象的,而不是具体的
- 避免因为实现细节的变化而导致依赖它的模块受到影响
// 抽象接口
interface Vehicle {
start(): void; // 启动交通工具
stop(): void; // 停止交通工具
}
// 具体类:飞机
class Airplane implements Vehicle {
start(): void {
console.log("powering up the engines.");
}
stop(): void {
console.log("deploying the landing gear.");
}
} 抽象程度要随稳定性要求增加
-
通过类来抽象和封装有特殊规则的领域对象或多个相关联的属性
-
使用两个 number 表示精度和维度.
- 缺乏语义:单独的两个浮点数无法表达其业务含义
- 规则分散:经纬度可能有特定的范围限制,如果不封装规则可能会散布在代码的多个地方。
- 定义 Location 类表达位置
-
使用两个 number 表示精度和维度.
class Location { constructor(private latitude: number, private longitude: number) { // 根据业务规则,验证合法性 } // 获取纬度 getLatitude(): number { return this.latitude; } // 获取经度 getLongitude(): number { return this.longitude; } }
模块要易于扩展
-
软件模块(类、函数等)应该对扩展开放,对修改关闭。
- 通过扩展功能而不是修改已有代码来适应需求变化,从而减少对现有代码的影响,降低引入新错误的风险。
- 主要描述的是设计目标而不是一种设计方法
- 例如:通过继承或实现接口来扩展功能,而不是直接修改类的代码来添加新功能
SOLID -
Open/Closed Principle (开放封闭原则)
SOLID 其他几条原则说明了如何保证扩展性...
接口职责要单一且明确
- 一个类应该只有一个职责
- 将不同的功能分离到不同的类中,避免一个类承担过多的职责,从而降低耦合性。
SOLID -
Single Responsibility Principle (单一职责原则)
class UserService { private users: string[] = []; addUser(user: string): void { this.users.push(user); this.log(`User added: ${user}`); } removeUser(user: string): void { this.users = this.users.filter(u => u !== user); this.log(`User removed: ${user}`); } private log(message: string): void { console.log(`[LOG]: ${message}`); } }
接口职责要单一且明确
SOLID -
Interface Segregation Principle (接口隔离原则)
- interface 应该尽量小而专一
- 对 interface 实现者:一个类不会被迫实现它不支持的接口
- 对 interface 调用者:不需要依赖它用不到的接口
// 定义多个小接口 interface Printer { print(document: string): void; } interface Scanner { scan(): string; } interface Fax { sendFax(document: string): void; }
// 多功能设备实现所有接口 class MultiFunctionDevice implements Printer, Scanner, Fax { // ... } // 单功能设备只实现需要的接口 class SimplePrinter implements Printer { /// }
-
区分查询方法和命令方法 (CQS: Command Query Separation)
- 查询(Query):返回某些信息,但不会改变对象的状态
- 命令(Command):执行某种操作,会改变对象的状态,但不返回任何值。
一般对于类的一个方法来说:
应该要么是命令,要么是查询,不应该同时是两者。
接口职责要单一且明确
public class BankAccount { private double balance; // 这个方法既修改了状态(存款),又返回了余额 public double deposit(double amount) { balance += amount; return balance; } }
接口与实现分离
SOLID -
Dependency Inversion Principle (依赖倒置原则)
低层模块
高层模块
前面介绍了依赖的方向应该是:高层模块(不稳定,具体)依赖低层模块(稳定,抽象)
框架/通用模块
业务模块
Model
UI
依赖倒置原则进一步要求:不要依赖具体实现,而是依赖于中间接口定义。
低层模块
高层模块
接口
接口与实现分离
class EmailService { public void sendEmail(String message) { System.out.println("Sending email: " + message); } } class Notification { private EmailService emailService; public Notification() { this.emailService = new EmailService(); // 高层模块直接依赖低层模块 } public void notifyUser(String message) { emailService.sendEmail(message); } }
- 高层模块受制于低层模块:Notification类(高层模块)必须知道EmailService类(低层模块)的具体实现。
- 低层模块的变化会影响高层模块:如果需要更换通知方式(比如改用短信服务),就需要修改Notification类的代码。
- 耦合性高:高层模块和低层模块紧密耦合,难以扩展和维护。
接口与实现分离
// 抽象接口 interface MessageService { void sendMessage(String message); } // 低层模块实现接口 class EmailService implements MessageService { public void sendMessage(String message) { System.out.println("Sending email: " + message); } } // 高层模块依赖接口 class Notification { private MessageService messageService; public Notification(MessageService messageService) { this.messageService = messageService; // 通过依赖注入传入具体实现 } public void notifyUser(String message) { messageService.sendMessage(message); } }
依赖注入
低层模块与高层模块互不依赖,那么谁来将其串连起来呢?
低层模块
高层模块
接口
通过依赖注入
依赖注入
interface Logger { log(message: string): void; } class ConsoleLogger implements Logger { // ... } class UserService { private logger: Logger; constructor(logger: Logger) { this.logger = logger; // 通过构造函数注入依赖 } }
// 使用时注入依赖 const logger = new ConsoleLogger(); const userService = new UserService(logger); userService.createUser("Alice");
- 构造函数注入:通过构造函数将依赖项传递给类
依赖注入
class UserService { createUser(name: string, logger: Logger): void { logger.log(`User ${name} has been created.`); } }
// 使用时注入依赖
const logger = new ConsoleLogger();
const userService = new UserService();
userService.createUser("Charlie", logger); // 方法注入 - 方法注入:通过方法参数将依赖项传递给类的方法
依赖注入
-
使用依赖注入框架:解决复杂项目里的依赖注入
-
TS:InversifyJS
- Angular 框架里也广泛使用了依赖注入
- Java:Spring, Dagger
-
TS:InversifyJS
继承或实现的基本原则
SOLID -
Liskov Substitution Principle (里氏替换原则)
-
子类必须能够替换其基类,并且保证程序的行为不变
- 接口的实现与接口之间也应遵守该原则
-
确保继承关系的正确性,避免因为子类(实现)的行为与基类(接口)不一致而导致程序出错
-
正确性体现在两方面:
- 接口类型上要兼容
- 接口语义和功能要一致
-
正确性体现在两方面:
-
实践方法:
- 避免不恰当的继承(前文提到过继承的问题和解法)
- 理解并遵守接口行为
设计模式 Design patterns
设计模式是一套被反复使用的、经过验证的解决特定问题的最佳实践和经验总结。
- 设计模式本身是个通用概念,每个人都可以总结出新的模式。
-
在 OOP 里,设计模式一般是指 GOF 提出的针对面向对象范式提出的 23 种模式,例如:
- 单例模式
- 工厂模式
- 适配器模式
- 组合模式
- 装饰器模式
- 代理模式
- 责任链模式
- 命令模式
- ...
设计模式 Design patterns
设计模式通常包括以下四个基本元素:
-
模式名称
- 一个简洁的名称,用于描述设计问题及其解决方案,方便沟通
-
问题
- 描述何时应用该模式,解释问题的背景和内容
-
解决方案
- 提供一个抽象的设计描述,说明如何通过 OOP 里的机制来解决问题
-
后果
- 描述应用该模式的优势和劣势,包括空间和时间的权衡、语言和实现问题等
If you’re a beginning programmer you won’t understand a lot of the material, and if you are experienced, the book will only confirm what you already know.
如果你是一名初级程序员,你将无法理解很多内容,而如果你很有经验,这本书只会确认你已经知道的内容。

不要过度设计,过度抽象
系统设计练习题
- 网上的题目,自己设计,与他人对比
- 与 AI 对话练习
Q&A
谢谢
面向对象基础
By yiliang_wang
面向对象基础
- 11