设计原则

软件设计中的 SOLID 原则及常用设计原则

问题

什么是 SOLID 原则?前端开发中有哪些常用的设计原则?

解答

SOLID 原则

1. 单一职责原则 (SRP)

一个类或模块只负责一件事。

// ❌ 违反 SRP:一个类做了太多事
class User {
  constructor(name) {
    this.name = name;
  }
  
  saveToDatabase() { /* 保存到数据库 */ }
  generateReport() { /* 生成报告 */ }
  sendEmail() { /* 发送邮件 */ }
}

// ✅ 遵循 SRP:职责分离
class User {
  constructor(name) {
    this.name = name;
  }
}

class UserRepository {
  save(user) { /* 保存到数据库 */ }
}

class UserReportGenerator {
  generate(user) { /* 生成报告 */ }
}

class EmailService {
  send(to, content) { /* 发送邮件 */ }
}

2. 开闭原则 (OCP)

对扩展开放,对修改关闭。

// ❌ 违反 OCP:添加新类型需要修改函数
function calculateArea(shape) {
  if (shape.type === 'circle') {
    return Math.PI * shape.radius ** 2;
  } else if (shape.type === 'rectangle') {
    return shape.width * shape.height;
  }
  // 添加新形状需要修改这个函数
}

// ✅ 遵循 OCP:通过扩展添加新功能
class Shape {
  area() {
    throw new Error('需要实现 area 方法');
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius ** 2;
  }
}

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

// 添加新形状只需新增类,无需修改现有代码
class Triangle extends Shape {
  constructor(base, height) {
    super();
    this.base = base;
    this.height = height;
  }
  area() {
    return (this.base * this.height) / 2;
  }
}

3. 里氏替换原则 (LSP)

子类必须能够替换父类。

// ❌ 违反 LSP:子类改变了父类的行为
class Bird {
  fly() {
    return '飞行中';
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error('企鹅不会飞'); // 破坏了父类契约
  }
}

// ✅ 遵循 LSP:正确的抽象
class Bird {
  move() {
    throw new Error('需要实现 move 方法');
  }
}

class FlyingBird extends Bird {
  move() {
    return '飞行中';
  }
}

class SwimmingBird extends Bird {
  move() {
    return '游泳中';
  }
}

class Sparrow extends FlyingBird {}
class Penguin extends SwimmingBird {}

4. 接口隔离原则 (ISP)

不应该强迫客户端依赖它不需要的接口。

// ❌ 违反 ISP:接口过于臃肿
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class Robot implements Worker {
  work() { /* 工作 */ }
  eat() { /* 机器人不需要吃饭 */ }
  sleep() { /* 机器人不需要睡觉 */ }
}

// ✅ 遵循 ISP:接口拆分
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

class Human implements Workable, Eatable, Sleepable {
  work() { /* 工作 */ }
  eat() { /* 吃饭 */ }
  sleep() { /* 睡觉 */ }
}

class Robot implements Workable {
  work() { /* 工作 */ }
}

5. 依赖倒置原则 (DIP)

高层模块不应该依赖低层模块,两者都应该依赖抽象。

// ❌ 违反 DIP:高层直接依赖低层实现
class MySQLDatabase {
  save(data) { /* 保存到 MySQL */ }
}

class UserService {
  constructor() {
    this.db = new MySQLDatabase(); // 直接依赖具体实现
  }
  
  createUser(user) {
    this.db.save(user);
  }
}

// ✅ 遵循 DIP:依赖抽象
class Database {
  save(data) {
    throw new Error('需要实现 save 方法');
  }
}

class MySQLDatabase extends Database {
  save(data) { /* 保存到 MySQL */ }
}

class MongoDB extends Database {
  save(data) { /* 保存到 MongoDB */ }
}

class UserService {
  constructor(database) {
    this.db = database; // 依赖注入,依赖抽象
  }
  
  createUser(user) {
    this.db.save(user);
  }
}

// 使用时注入具体实现
const userService = new UserService(new MySQLDatabase());

其他常用原则

DRY (Don’t Repeat Yourself)

// ❌ 重复代码
function validateEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

function validateUserEmail(user) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(user.email);
}

// ✅ 复用代码
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function validateEmail(email) {
  return EMAIL_REGEX.test(email);
}

function validateUserEmail(user) {
  return validateEmail(user.email);
}

KISS (Keep It Simple, Stupid)

// ❌ 过度复杂
function isEven(num) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(num % 2 === 0);
    }, 0);
  });
}

// ✅ 保持简单
function isEven(num) {
  return num % 2 === 0;
}

YAGNI (You Aren’t Gonna Need It)

// ❌ 过度设计:添加了当前不需要的功能
class User {
  constructor(name) {
    this.name = name;
    this.permissions = [];
    this.roles = [];
    this.preferences = {};
    this.activityLog = [];
    // ... 很多当前用不到的属性
  }
}

// ✅ 只实现当前需要的
class User {
  constructor(name) {
    this.name = name;
  }
}

关键点

  • SRP:一个模块只做一件事,降低耦合
  • OCP:通过扩展而非修改来添加功能
  • LSP:子类必须能完全替代父类使用
  • ISP:接口要小而专一,避免臃肿
  • DIP:依赖抽象而非具体实现,便于测试和替换
  • DRY:避免重复代码,提取公共逻辑
  • KISS:保持简单,不要过度设计