第一部分:基本概念
通过编程解决问题的过程就是使用代码对现实问题建模的过程,“面向”是说我们选择某一个视角来建模复杂的业务问题。
关注"做什么"—— 把问题分解为一系列步骤或操作
// 面向过程的思维 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 Car { brand: string; speed: number; }
方法描述了对象可以执行的操作
class Car { accelerate() { // 加速逻辑 } brake() { // 刹车逻辑 } }
“函数”与“方法”
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 是两种特殊方法,主要用来封装访问属性和修改对象属性的行为。
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 是两种特殊方法,主要用来封装访问属性和修改对象属性的行为。
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);
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 可以实现只读属性,也是类似的使用场景
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之间 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 相比于提供 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("张三")); // "用户: 张三" 访问控制修饰符用于限定类成员(属性和方法)的访问范围,是实现封装的关键工具。
OOP 的三大特点:
封装、继承、多态
先给所有属性和方法加上 private,直到有明确使用场景时再扩大权限
最大程度保护类的实现细节,只开放真正需要的访问权限
私有构造函数: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 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;
}
} 典型例子:
这种区分不是绝对的,取决于设计的抽象程度
与抽象类对应的是具体类
常见的抽象类的使用场景:
泛化的事物类别
包含部分已知特征的模板
需要统一规范的基础设施
继承和抽象类是为了代码重用,但要谨慎使用,避免不合理的抽象或过深的继承带来的维护性问题。后面会展开介绍。
继承表达的是“子类”是“父类”特例的情况,即“子类对象 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 关系。
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("我在游泳!");
}
}abstract class Bird { fly(): void { console.log("我在飞!"); } walk(): void { console.log("我在走路"); } makeSound(): void { console.log("我在叫"); } }
生活中没有鸟,鸟只是一个抽象, 我们生活中有鸡,有鸭。
我们认为它们有一些相同的地方,于是把拥有这些相同点的东西叫做鸟,但永远不确定下一个遇见的能不能算鸟, 鸟的定义要不要修改。
如果新来了橡皮鸭子,它是鸟吗?
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 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."); } }
// 定义接口:飞行能力 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."); } }
注意区分两种 interface 的概念:
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 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 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());系统设计时要考虑清楚:对象会被谁创建和使用,会在什么时机创建,什么时机销毁。
良好的生命周期管理可以帮助我们避免资源泄露,确保对象正确初始化。
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; } }
最基础的序列化和反序列化:手动转换
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);
}
}更进一步地,可以使用序列化库来简化操作:
//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 objectpackage
interface
抽象类
类
B 继承 A
B 实现 A
A 聚合 B
A 组合 B
第二部分:设计原则
Model
UI
框架/通用模块
业务模块
小心循环依赖,它暗示了模块设计可能有问题
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.");
}
} 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 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)
一般对于类的一个方法来说:
应该要么是命令,要么是查询,不应该同时是两者。
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); } }
// 抽象接口 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); // 方法注入 SOLID -
Liskov Substitution Principle (里氏替换原则)
设计模式是一套被反复使用的、经过验证的解决特定问题的最佳实践和经验总结。
设计模式通常包括以下四个基本元素:
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.
如果你是一名初级程序员,你将无法理解很多内容,而如果你很有经验,这本书只会确认你已经知道的内容。