Skip to content

面向对象设计原则与设计模式的关系

引言

在上一章中,我们了解了设计模式的基本概念和分类。设计模式不是凭空产生的,它们是面向对象设计原则的具体应用和体现。要真正掌握设计模式,我们必须首先理解这些设计原则。

正如 Robert C. Martin 在《敏捷软件开发:原则、模式与实践》中所说:"设计模式是原则的具体应用。"设计原则是指导思想,而设计模式是实践方法。

SOLID 原则详解

SOLID 是五个面向对象设计原则的首字母缩写,由 Robert C. Martin 提出。这些原则旨在使软件设计更易于理解、灵活和可维护。

1. 单一职责原则(Single Responsibility Principle, SRP)

定义:一个类应该只有一个引起它变化的原因。

换句话说,一个类应该只有一个职责,只负责一类功能。

违反 SRP 的示例

javascript
// 违反单一职责原则的类
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  
  // 用户数据管理职责
  saveToDatabase() {
    // 保存用户到数据库的逻辑
    console.log(`保存用户 ${this.name} 到数据库`);
  }
  
  // 邮件发送职责
  sendEmail(message) {
    // 发送邮件的逻辑
    console.log(`发送邮件给 ${this.email}: ${message}`);
  }
  
  // 数据验证职责
  validateEmail() {
    // 验证邮箱格式的逻辑
    return /\S+@\S+\.\S+/.test(this.email);
  }
  
  // 日志记录职责
  logActivity(activity) {
    // 记录用户活动的逻辑
    console.log(`用户 ${this.name} 执行了: ${activity}`);
  }
}

遵循 SRP 的改进

javascript
// 遵循单一职责原则的类
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  save(user) {
    console.log(`保存用户 ${user.name} 到数据库`);
  }
}

class EmailService {
  send(email, message) {
    console.log(`发送邮件给 ${email}: ${message}`);
  }
}

class EmailValidator {
  static validate(email) {
    return /\S+@\S+\.\S+/.test(email);
  }
}

class ActivityLogger {
  log(user, activity) {
    console.log(`用户 ${user.name} 执行了: ${activity}`);
  }
}

// 使用示例
const user = new User('Alice', 'alice@example.com');
const validator = new EmailValidator();
const repository = new UserRepository();
const emailService = new EmailService();
const logger = new ActivityLogger();

if (validator.validate(user.email)) {
  repository.save(user);
  emailService.send(user.email, '欢迎注册!');
  logger.log(user, '注册账户');
}

SRP 与设计模式的关系

单一职责原则是许多设计模式的基础,例如:

  • 策略模式:将不同的算法封装在独立的类中,每个类只有一个职责
  • 命令模式:将每个请求封装为一个对象,每个命令类只负责一个操作
  • 观察者模式:将观察者和被观察者的职责分离

2. 开闭原则(Open/Closed Principle, OCP)

定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

这意味着我们应该能够不修改现有代码的情况下扩展系统的行为。

违反 OCP 的示例

javascript
// 违反开闭原则的代码
class PaymentProcessor {
  processPayment(amount, type) {
    if (type === 'credit') {
      // 处理信用卡支付
      console.log(`处理信用卡支付: $${amount}`);
    } else if (type === 'debit') {
      // 处理借记卡支付
      console.log(`处理借记卡支付: $${amount}`);
    } else if (type === 'paypal') {
      // 处理 PayPal 支付
      console.log(`处理 PayPal 支付: $${amount}`);
    }
    // 每次添加新的支付方式都需要修改这个方法
  }
}

遵循 OCP 的改进

javascript
// 遵循开闭原则的代码
class PaymentProcessor {
  processPayment(amount, paymentMethod) {
    paymentMethod.process(amount);
  }
}

class PaymentMethod {
  process(amount) {
    throw new Error('必须实现 process 方法');
  }
}

class CreditCardPayment extends PaymentMethod {
  process(amount) {
    console.log(`处理信用卡支付: $${amount}`);
  }
}

class DebitCardPayment extends PaymentMethod {
  process(amount) {
    console.log(`处理借记卡支付: $${amount}`);
  }
}

class PayPalPayment extends PaymentMethod {
  process(amount) {
    console.log(`处理 PayPal 支付: $${amount}`);
  }
}

// 添加新的支付方式时,不需要修改现有代码
class WeChatPayment extends PaymentMethod {
  process(amount) {
    console.log(`处理微信支付: $${amount}`);
  }
}

// 使用示例
const processor = new PaymentProcessor();
processor.processPayment(100, new CreditCardPayment());
processor.processPayment(200, new PayPalPayment());
processor.processPayment(150, new WeChatPayment()); // 新增支付方式

OCP 与设计模式的关系

开闭原则是许多设计模式的核心思想,例如:

  • 策略模式:通过定义一系列算法,将它们封装起来,并且使它们可以互相替换
  • 工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪个类
  • 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新

3. 里氏替换原则(Liskov Substitution Principle, LSP)

定义:子类型必须能够替换它们的基类型。

这意味着在程序中,任何基类出现的地方,子类都应该能够无缝替换,而不会影响程序的正确性。

违反 LSP 的示例

javascript
// 违反里氏替换原则的示例
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  
  setWidth(width) {
    this.width = width;
  }
  
  setHeight(height) {
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(side) {
    super(side, side);
  }
  
  setWidth(width) {
    this.width = width;
    this.height = width; // 强制保持正方形
  }
  
  setHeight(height) {
    this.width = height; // 强制保持正方形
    this.height = height;
  }
}

// 这个函数期望使用矩形,但使用正方形时行为不一致
function increaseRectangleWidth(rectangle) {
  rectangle.setWidth(rectangle.width + 1);
}

const rectangle = new Rectangle(2, 3);
const square = new Square(2);

console.log(rectangle.getArea()); // 6
increaseRectangleWidth(rectangle);
console.log(rectangle.getArea()); // 9 (3*3)

console.log(square.getArea()); // 4
increaseRectangleWidth(square);
console.log(square.getArea()); // 9 (3*3) 但正方形的高也被改变了

遵循 LSP 的改进

javascript
// 遵循里氏替换原则的改进
class Shape {
  getArea() {
    throw new Error('必须实现 getArea 方法');
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }
  
  getArea() {
    return this.side * this.side;
  }
}

// 现在每个形状都有明确的职责,不会出现替换问题
function printArea(shape) {
  console.log(`面积: ${shape.getArea()}`);
}

const rectangle = new Rectangle(2, 3);
const square = new Square(2);

printArea(rectangle); // 面积: 6
printArea(square);    // 面积: 4

4. 接口隔离原则(Interface Segregation Principle, ISP)

定义:客户端不应该被迫依赖它们不使用的接口。

这个原则强调应该创建更小、更具体的接口,而不是一个庞大的通用接口。

违反 ISP 的示例

javascript
// 违反接口隔离原则的示例
class MultiFunctionPrinter {
  print(document) {
    // 打印文档
  }
  
  fax(document) {
    // 发送传真
  }
  
  scan(document) {
    // 扫描文档
  }
}

class OldFashionedPrinter extends MultiFunctionPrinter {
  print(document) {
    // 只能打印
  }
  
  // 传真和扫描功能不支持,但接口强制实现
  fax(document) {
    throw new Error('不支持传真功能');
  }
  
  scan(document) {
    throw new Error('不支持扫描功能');
  }
}

遵循 ISP 的改进

javascript
// 遵循接口隔离原则的改进
class Printer {
  print(document) {
    throw new Error('必须实现 print 方法');
  }
}

class Scanner {
  scan(document) {
    throw new Error('必须实现 scan 方法');
  }
}

class FaxMachine {
  fax(document) {
    throw new Error('必须实现 fax 方法');
  }
}

// 只需要打印功能的打印机
class SimplePrinter extends Printer {
  print(document) {
    console.log(`打印文档: ${document}`);
  }
}

// 多功能打印机
class MultiFunctionMachine extends Printer {
  constructor(printer, scanner, faxMachine) {
    super();
    this.printer = printer;
    this.scanner = scanner;
    this.faxMachine = faxMachine;
  }
  
  print(document) {
    this.printer.print(document);
  }
  
  scan(document) {
    this.scanner.scan(document);
  }
  
  fax(document) {
    this.faxMachine.fax(document);
  }
}

5. 依赖倒置原则(Dependency Inversion Principle, DIP)

定义

  1. 高层模块不应该依赖低层模块,二者都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

违反 DIP 的示例

javascript
// 违反依赖倒置原则的示例
class LightBulb {
  turnOn() {
    console.log('灯泡打开了');
  }
  
  turnOff() {
    console.log('灯泡关闭了');
  }
}

class ElectricPowerSwitch {
  constructor() {
    this.lightBulb = new LightBulb(); // 直接依赖具体实现
    this.on = false;
  }
  
  press() {
    if (this.on) {
      this.lightBulb.turnOff();
      this.on = false;
    } else {
      this.lightBulb.turnOn();
      this.on = true;
    }
  }
}

遵循 DIP 的改进

javascript
// 遵循依赖倒置原则的改进
class Switchable {
  turnOn() {
    throw new Error('必须实现 turnOn 方法');
  }
  
  turnOff() {
    throw new Error('必须实现 turnOff 方法');
  }
}

class LightBulb extends Switchable {
  turnOn() {
    console.log('灯泡打开了');
  }
  
  turnOff() {
    console.log('灯泡关闭了');
  }
}

class Fan extends Switchable {
  turnOn() {
    console.log('风扇开始转动');
  }
  
  turnOff() {
    console.log('风扇停止转动');
  }
}

class ElectricPowerSwitch {
  constructor(device) {
    this.device = device; // 依赖抽象而不是具体实现
    this.on = false;
  }
  
  press() {
    if (this.on) {
      this.device.turnOff();
      this.on = false;
    } else {
      this.device.turnOn();
      this.on = true;
    }
  }
}

// 使用示例
const bulb = new LightBulb();
const fan = new Fan();

const switch1 = new ElectricPowerSwitch(bulb);
const switch2 = new ElectricPowerSwitch(fan);

switch1.press(); // 灯泡打开了
switch2.press(); // 风扇开始转动

其他重要设计原则

除了 SOLID 原则外,还有一些其他重要的设计原则:

1. DRY 原则(Don't Repeat Yourself)

定义:避免重复代码,将共同的逻辑封装起来。

javascript
// 违反 DRY 原则
function calculateCircleArea(radius) {
  return 3.14159 * radius * radius;
}

function calculateSphereVolume(radius) {
  return (4/3) * 3.14159 * radius * radius * radius;
}

// 遵循 DRY 原则
const PI = 3.14159;

function calculateCircleArea(radius) {
  return PI * radius * radius;
}

function calculateSphereVolume(radius) {
  return (4/3) * PI * radius * radius * radius;
}

2. KISS 原则(Keep It Simple, Stupid)

定义:保持简单,避免过度设计。

javascript
// 过度复杂的解决方案
function isEven(number) {
  return number % 2 === 0 ? true : false;
}

// 简单直接的解决方案
function isEven(number) {
  return number % 2 === 0;
}

3. YAGNI 原则(You Aren't Gonna Need It)

定义:不要添加当前不需要的功能。

这个原则提醒我们不要过度设计,只实现当前需要的功能。

总结

面向对象设计原则是设计模式的基础,它们为我们提供了设计高质量软件的指导方针。理解这些原则有助于我们:

  1. 编写更易维护的代码 - 通过遵循 SRP 和 ISP 等原则
  2. 创建更灵活的系统 - 通过遵循 OCP 和 DIP 等原则
  3. 提高代码的可复用性 - 通过遵循 LSP 等原则
  4. 减少代码的复杂性 - 通过遵循 KISS 和 YAGNI 等原则

在下一章中,我们将探讨设计模式的历史发展,了解它们是如何从 GoF 的经典著作发展到现代 JavaScript 和前端开发中的应用。