蓝布编程网

分享编程技术文章,编程语言教程与实战经验

掌握SOLID:面向对象原则(sv面向对象)


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原则是面向对象编程的基石,它们帮助我们创建:

关键收益:

  1. 可维护性:代码更容易理解和修改
  2. 可扩展性:新功能可以通过扩展而不是修改来添加
  3. 可测试性:代码更容易进行单元测试
  4. 可重用性:组件可以在不同的上下文中重用
  5. 灵活性:系统更容易适应变化

记忆技巧:

  • S - 单一职责原则:只做一件事
  • O - 开闭原则:外面很冷
  • L - 里氏替换原则:代课老师
  • I - 接口隔离原则:隔离燃料类型!
  • D - 依赖倒置原则:可靠的插头适配器

实践建议:

  1. 从小开始:先应用单一职责原则
  2. 逐步改进:不要试图一次性重构所有代码
  3. 持续学习:在实践中不断学习和改进
  4. 团队协作:与团队成员分享这些原则

记住:SOLID原则不是绝对的规则,而是指导原则。在特定情况下,可能需要权衡和妥协。关键是要理解这些原则背后的思想,并在适当的时候应用它们。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言