SOLID原则是面向对象编程和设计的五个基本原则,包括单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖反转原则(DIP),旨在提升代码的可维护性、可扩展性和可读性。
S – 单一职责原则 (SRP)
一个类应该只对一个"参与者"或用户组负责,这意味着该类应该只有一个改变的理由
在我看来,这是最容易理解和记忆的。SRP经常与"只有一个工作"混淆,虽然这通常是正确的,也不是一个糟糕的经验法则,但该原则的真正意图是一个类(或方法)应该只被_一种类型的参与者_改变。此外,我个人对这个原则的解释还包括,一个类或方法也应该只对单一类型的领域数据负责,在可能的情况下。
假设你有一个运行一些常见企业逻辑的_BusinessManager_类:
//业务管理器有太多职责
class BusinessManager {
processEmployeePayrolls(): void {
console.log("处理员工工资...");
}
generateCustomerInvoices(): void {
console.log("生成客户销售发票...");
}
}这个类负责两个不同的关注点:员工工资和客户开票。它可能受到两个独立系统参与者(人力资源和财务团队)请求的更改,或两个不同数据领域(员工/客户)活动的影响。
将这种逻辑分离到它们自己的特定类中是有益的,给它们对各自领域的明确责任:
//员工管理器处理工资
class EmployeeManager {
processPayrolls(): void {
console.log("处理员工工资...");
}
}
//客户管理器处理发票生成
class CustomerManager {
generateInvoices(): void {
console.log("生成客户销售发票...");
}
}在现实中,你更可能有工资服务和发票服务,它们会适当地接受员工类或客户类,但为了解释我们的原则,我们保持简单。
O – 开闭原则 (OCP)
软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭
这个原则经常被误解,因为它听起来矛盾。我们如何在不修改的情况下扩展某些东西?答案是通过抽象和多态性。
让我们看一个例子:
//违反OCP的代码
class Rectangle {
width: number;
height: number;
}
class Circle {
radius: number;
}
class AreaCalculator {
calculateArea(shapes: any[]): number {
let area = 0;
for (let shape of shapes) {
if (shape instanceof Rectangle) {
area += shape.width * shape.height;
} else if (shape instanceof Circle) {
area += Math.PI * shape.radius * shape.radius;
}
}
return area;
}
}这个设计违反了OCP,因为每次我们想要添加一个新的形状时,我们都必须修改AreaCalculator类。如果我们添加一个三角形,我们需要添加另一个if语句。
符合OCP的解决方案:
//符合OCP的代码
interface Shape {
calculateArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
calculateArea(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
constructor(private radius: number) {}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
class AreaCalculator {
calculateArea(shapes: Shape[]): number {
let area = 0;
for (let shape of shapes) {
area += shape.calculateArea();
}
return area;
}
}现在,如果我们想要添加一个新的形状(比如三角形),我们只需要创建一个新的类来实现Shape接口,而不需要修改AreaCalculator。
为什么这很重要?
OCP帮助我们创建更灵活和可维护的代码。它减少了修改现有代码的风险,这可能导致意外的副作用。它还促进了代码重用,因为新的功能可以通过扩展而不是修改来添加。
我如何记住? - 外面很冷
想象一下,你在一辆货车里,外面很冷。货车是封闭的(对修改关闭),但你可以通过窗户或门向外看(对扩展开放)。窗户和门是扩展点,允许你与外部世界交互,而不需要修改货车本身的结构。
L – 里氏替换原则 (LSP)
子类型必须可以替换其基类型,而不改变程序的正确性
这个原则确保继承被正确使用。它说,如果你有一个基类的对象,你应该能够用该基类的任何子类的对象替换它,而程序应该继续正常工作。
违反LSP的例子:
class Bird {
fly(): void {
console.log("飞行中...");
}
}
class Penguin extends Bird {
fly(): void {
throw new Error("企鹅不能飞!");
}
}
function makeBirdFly(bird: Bird) {
bird.fly(); // 如果传入企鹅,这会抛出错误!
}这个例子违反了LSP,因为Penguin不能替换Bird而不改变程序的正确性。
符合LSP的解决方案:
interface Flyable {
fly(): void;
}
class Bird implements Flyable {
fly(): void {
console.log("飞行中...");
}
}
class Penguin {
// 企鹅不实现Flyable接口,因为它不能飞
swim(): void {
console.log("游泳中...");
}
}
function makeBirdFly(bird: Flyable) {
bird.fly(); // 只有能飞的鸟才能传入
}为什么这很重要?
LSP确保继承被正确使用,并防止创建脆弱的基类。它帮助我们创建更健壮和可预测的代码。
我如何记住? - 代课老师
想象一下,你有一个代课老师。代课老师应该能够做原老师能做的一切,而不改变课堂的正常运行。如果代课老师不能做原老师能做的事情,或者以不同的方式做事情,那么课堂就会出问题。同样,子类应该能够做基类能做的一切,而不破坏程序。
I – 接口隔离原则 (ISP)
客户端不应该被迫依赖它们不使用的接口
这个原则说,我们应该创建小而专注的接口,而不是大而臃肿的接口。客户端应该只依赖它们实际使用的接口。
违反ISP的例子:
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class Robot implements Worker {
work(): void {
console.log("机器人工作...");
}
eat(): void {
throw new Error("机器人不需要吃东西!");
}
sleep(): void {
throw new Error("机器人不需要睡觉!");
}
}这个例子违反了ISP,因为Robot被迫实现它不需要的方法。
符合ISP的解决方案:
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class Human implements Workable, Eatable, Sleepable {
work(): void {
console.log("人类工作...");
}
eat(): void {
console.log("人类吃东西...");
}
sleep(): void {
console.log("人类睡觉...");
}
}
class Robot implements Workable {
work(): void {
console.log("机器人工作...");
}
}为什么这很重要?
ISP帮助我们创建更灵活和可维护的代码。它减少了客户端之间的耦合,并促进了代码重用。
我如何记住? - 隔离燃料类型!
想象一下加油站。不同类型的车辆需要不同类型的燃料。汽油车需要汽油,柴油车需要柴油,电动车需要电力。如果加油站只有一个通用的燃料泵,它会很复杂,而且很多车辆会得到它们不需要的燃料。同样,我们应该为不同的客户端创建专门的接口,而不是一个通用的接口。
D – 依赖倒置原则 (DIP)
高级模块不应该依赖低级模块。两者都应该依赖抽象。抽象不应该依赖细节。细节应该依赖抽象。
这个原则有两个部分:
1. 高级模块不应该依赖低级模块。两者都应该依赖抽象。
2. 抽象不应该依赖细节。细节应该依赖抽象。
让我们看一个例子:
//违反DIP的代码
class ProductService {
private repository = new ProductRepository();
listProducts(): string[] {
return this.repository.getAllProducts();
}
}
class ProductRepository {
getAllProducts(): string[] {
return ["TV", "Laptop", "Phone"];
}
}这个例子违反了DIP,因为ProductService直接依赖于ProductRepository的具体实现。
符合DIP的解决方案:
interface IProductRepository {
getAllProducts(): string[];
}
class ProductRepository implements IProductRepository {
getAllProducts(): string[] {
return ["TV", "Laptop", "Phone"];
}
}
class ProductService {
constructor(private repo: IProductRepository) {} // 现在依赖于抽象
listProducts(): string[] {
return this.repo.getAllProducts();
}
}现在ProductService依赖于IProductRepository接口,而不是具体的实现。这意味着我们可以轻松地交换不同的实现。
为什么这很重要?
DIP帮助我们创建更灵活和可测试的代码。它减少了模块之间的耦合,并促进了代码重用。
我如何记住? - 可靠的插头适配器
想象一下,你有一个通用的插头适配器。你的笔记本电脑不关心它连接到哪个具体的电源插座,只关心它符合你的笔记本电脑能理解的接口。这就是依赖倒置。
让我们总结一下
所以,我们有了它,我理解和记住SOLID的记忆术如下:
- S - 单一职责原则 - 不要给狗压力!
- O - 开闭原则 - 外面很冷
- L - 里氏替换原则 - 代课老师
- I - 接口隔离原则 - 隔离燃料类型!
- D - 依赖倒置原则 - 可靠的插头适配器
如果你觉得这篇文章有帮助,我鼓励你为SOLID创建自己的记忆术,我很想听听你想出了什么。如果你有其他帮助记住抽象概念的技术或方法,我也很想听听。
快乐编码 - Joe
深入理解SOLID原则
1. 单一职责原则 (SRP) 详解
什么是"单一职责"?
// 违反SRP的例子
class UserManager {
// 用户管理
createUser(userData: any): void {}
updateUser(userId: string, userData: any): void {}
deleteUser(userId: string): void {}
// 邮件发送
sendWelcomeEmail(userId: string): void {}
sendPasswordResetEmail(userId: string): void {}
// 数据验证
validateEmail(email: string): boolean {}
validatePassword(password: string): boolean {}
// 日志记录
logUserAction(userId: string, action: string): void {}
}这个类有多个职责:用户管理、邮件发送、数据验证和日志记录。每个职责都可能因为不同的原因而改变。
符合SRP的重构
// 用户管理
class UserService {
constructor(
private userRepository: IUserRepository,
private emailService: IEmailService,
private validator: IValidator,
private logger: ILogger
) {}
createUser(userData: any): void {
if (this.validator.validateUserData(userData)) {
const user = this.userRepository.create(userData);
this.emailService.sendWelcomeEmail(user.email);
this.logger.log('USER_CREATED', user.id);
}
}
}
// 邮件服务
class EmailService implements IEmailService {
sendWelcomeEmail(email: string): void {}
sendPasswordResetEmail(email: string): void {}
}
// 验证器
class UserValidator implements IValidator {
validateEmail(email: string): boolean {}
validatePassword(password: string): boolean {}
validateUserData(userData: any): boolean {}
}
// 日志记录器
class Logger implements ILogger {
log(action: string, userId: string): void {}
}2. 开闭原则 (OCP) 实践
支付系统示例
// 违反OCP的支付处理器
class PaymentProcessor {
processPayment(paymentType: string, amount: number): void {
if (paymentType === 'credit_card') {
// 处理信用卡支付
console.log('处理信用卡支付...');
} else if (paymentType === 'paypal') {
// 处理PayPal支付
console.log('处理PayPal支付...');
} else if (paymentType === 'bitcoin') {
// 处理比特币支付
console.log('处理比特币支付...');
}
}
}符合OCP的重构
interface PaymentMethod {
process(amount: number): void;
}
class CreditCardPayment implements PaymentMethod {
process(amount: number): void {
console.log(`处理信用卡支付: ${amount}`);
}
}
class PayPalPayment implements PaymentMethod {
process(amount: number): void {
console.log(`处理PayPal支付: ${amount}`);
}
}
class BitcoinPayment implements PaymentMethod {
process(amount: number): void {
console.log(`处理比特币支付: ${amount}`);
}
}
class PaymentProcessor {
processPayment(paymentMethod: PaymentMethod, amount: number): void {
paymentMethod.process(amount);
}
}
// 使用
const processor = new PaymentProcessor();
processor.processPayment(new CreditCardPayment(), 100);
processor.processPayment(new PayPalPayment(), 50);3. 里氏替换原则 (LSP) 深入
集合类示例
// 违反LSP的例子
class Rectangle {
protected width: number;
protected height: number;
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number): void {
this.width = width;
this.height = width; // 正方形保持宽高相等
}
setHeight(height: number): void {
this.width = height; // 正方形保持宽高相等
this.height = height;
}
}
// 这个函数期望Rectangle的行为
function testRectangle(rectangle: Rectangle): void {
rectangle.setWidth(5);
rectangle.setHeight(4);
// 期望面积是20,但如果传入Square,面积会是16
console.log(`期望面积: 20, 实际面积: ${rectangle.getArea()}`);
}
// 测试
testRectangle(new Rectangle()); // 正确
testRectangle(new Square()); // 违反LSP!符合LSP的解决方案
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(protected width: number, protected height: number) {}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(protected side: number) {}
getArea(): number {
return this.side * this.side;
}
}
function testShape(shape: Shape): void {
console.log(`面积: ${shape.getArea()}`);
}4. 接口隔离原则 (ISP) 应用
打印机接口示例
// 违反ISP的大接口
interface Machine {
print(document: string): void;
scan(document: string): void;
fax(document: string): void;
}
// 老式打印机被迫实现不需要的方法
class OldPrinter implements Machine {
print(document: string): void {
console.log('打印文档...');
}
scan(document: string): void {
throw new Error('老式打印机不能扫描!');
}
fax(document: string): void {
throw new Error('老式打印机不能传真!');
}
}符合ISP的重构
interface Printable {
print(document: string): void;
}
interface Scannable {
scan(document: string): void;
}
interface Faxable {
fax(document: string): void;
}
// 老式打印机只实现它需要的接口
class OldPrinter implements Printable {
print(document: string): void {
console.log('打印文档...');
}
}
// 现代多功能打印机实现所有接口
class ModernPrinter implements Printable, Scannable, Faxable {
print(document: string): void {
console.log('打印文档...');
}
scan(document: string): void {
console.log('扫描文档...');
}
fax(document: string): void {
console.log('传真文档...');
}
}5. 依赖倒置原则 (DIP) 实现
数据库访问示例
// 违反DIP的代码
class UserService {
private database = new MySQLDatabase(); // 直接依赖具体实现
getUser(id: string): User {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
class MySQLDatabase {
query(sql: string): any {
// MySQL特定实现
console.log('执行MySQL查询:', sql);
return { id: '1', name: 'John' };
}
}符合DIP的重构
interface IUserRepository {
getUser(id: string): User;
saveUser(user: User): void;
}
class MySQLUserRepository implements IUserRepository {
getUser(id: string): User {
console.log('从MySQL获取用户:', id);
return { id, name: 'John' };
}
saveUser(user: User): void {
console.log('保存用户到MySQL:', user);
}
}
class PostgreSQLUserRepository implements IUserRepository {
getUser(id: string): User {
console.log('从PostgreSQL获取用户:', id);
return { id, name: 'John' };
}
saveUser(user: User): void {
console.log('保存用户到PostgreSQL:', user);
}
}
class UserService {
constructor(private userRepository: IUserRepository) {} // 依赖抽象
getUser(id: string): User {
return this.userRepository.getUser(id);
}
saveUser(user: User): void {
this.userRepository.saveUser(user);
}
}
// 使用依赖注入
const mysqlService = new UserService(new MySQLUserRepository());
const postgresService = new UserService(new PostgreSQLUserRepository());6. SOLID原则的组合应用
完整的用户管理系统
// 领域模型
interface User {
id: string;
name: string;
email: string;
}
// 存储库接口
interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// 验证接口
interface IUserValidator {
validate(user: User): boolean;
}
// 通知接口
interface IUserNotifier {
notifyUserCreated(user: User): Promise<void>;
}
// 日志接口
interface ILogger {
log(message: string): void;
}
// 具体实现
class UserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
// 数据库实现
return null;
}
async save(user: User): Promise<void> {
// 保存到数据库
}
async delete(id: string): Promise<void> {
// 从数据库删除
}
}
class UserValidator implements IUserValidator {
validate(user: User): boolean {
return user.email.includes('@') && user.name.length > 0;
}
}
class EmailNotifier implements IUserNotifier {
async notifyUserCreated(user: User): Promise<void> {
console.log(`发送欢迎邮件给 ${user.email}`);
}
}
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
// 用户服务 - 符合所有SOLID原则
class UserService {
constructor(
private userRepository: IUserRepository,
private userValidator: IUserValidator,
private userNotifier: IUserNotifier,
private logger: ILogger
) {}
async createUser(userData: Partial<User>): Promise<User> {
const user: User = {
id: this.generateId(),
name: userData.name!,
email: userData.email!
};
if (!this.userValidator.validate(user)) {
throw new Error('无效的用户数据');
}
await this.userRepository.save(user);
await this.userNotifier.notifyUserCreated(user);
this.logger.log(`用户已创建: ${user.id}`);
return user;
}
private generateId(): string {
return Math.random().toString(36).substr(2, 9);
}
}总结
SOLID原则是面向对象编程的基石,它们帮助我们创建:
关键收益:
- 可维护性:代码更容易理解和修改
- 可扩展性:新功能可以通过扩展而不是修改来添加
- 可测试性:代码更容易进行单元测试
- 可重用性:组件可以在不同的上下文中重用
- 灵活性:系统更容易适应变化
记忆技巧:
- S - 单一职责原则:只做一件事
- O - 开闭原则:外面很冷
- L - 里氏替换原则:代课老师
- I - 接口隔离原则:隔离燃料类型!
- D - 依赖倒置原则:可靠的插头适配器
实践建议:
- 从小开始:先应用单一职责原则
- 逐步改进:不要试图一次性重构所有代码
- 持续学习:在实践中不断学习和改进
- 团队协作:与团队成员分享这些原则
记住:SOLID原则不是绝对的规则,而是指导原则。在特定情况下,可能需要权衡和妥协。关键是要理解这些原则背后的思想,并在适当的时候应用它们。