Skip to content

SOLID

单一职责原则(SRP)

正如《代码整洁之道》所述:"类的修改原因不应超过一个"。开发人员常常试图在类中集成过多功能,就像搭乘航班时只能携带一件行李那样受限。但这样做会导致类缺乏内聚性,并且会有多个修改的原因。尽量减少修改类的次数非常重要,因为如果一个类包含过多功能,修改其中一部分可能会难以理解这将如何影响代码库中的其他依赖模块。

不良实践:

javascript
class UserSettings {
  constructor(user) {
    this.user = user;
  }

  changeSettings(settings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() {
    // ...
  }
}

优化方案:

javascript
class UserAuth {
  constructor(user) {
    this.user = user;
  }

  verifyCredentials() {
    // ...
  }
}

class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
  }

  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}

开闭原则(OCP)

Bertrand Meyer提出的核心理念是:"软件实体(类、模块、函数等)应对扩展开放,对修改封闭"。这一原则强调当需要添加新功能时(例如通过继承或组合),应通过新增代码而非修改已有代码来实现。

不良实践:

javascript
class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = "ajaxAdapter";
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = "nodeAdapter";
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    if (this.adapter.name === "ajaxAdapter") {
      return makeAjaxCall(url).then(response => {
        // transform response and return
      });
    } else if (this.adapter.name === "nodeAdapter") {
      return makeHttpCall(url).then(response => {
        // transform response and return
      });
    }
  }
}

function makeAjaxCall(url) {
  // request and return promise
}

function makeHttpCall(url) {
  // request and return promise
}

优化方案:

javascript
class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = "ajaxAdapter";
  }

  request(url) {
    // request and return promise
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = "nodeAdapter";
  }

  request(url) {
    // request and return promise
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    return this.adapter.request(url).then(response => {
      // transform response and return
    });
  }
}

里氏替换原则(LSP)

这个术语描述了一个简单的子类型关系:若S是T的子类型,则程序中T类型的对象可被S类型对象替换,而不影响程序的正确性。该原则确保继承关系的正确应用(例如在矩形-正方形案例中,数学上的包含关系若直接映射为继承关系,将导致违反行为一致性)。

当存在父子类关系时,应当确保基类与子类可互换使用而不产生错误结果。以经典的矩形-正方形为例,若在代码中允许设置不同长宽值,将破坏正方形长宽必须相等的本质约束。

不良实践:

javascript
class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width;
  }

  setHeight(height) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles) {
  rectangles.forEach(rectangle => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
    rectangle.render(area);
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

优化方案:

javascript
class Shape {
  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }
}

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(length) {
    super();
    this.length = length;
  }

  getArea() {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach(shape => {
    const area = shape.getArea();
    shape.render(area);
  });
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

接口隔离原则(ISP)

由于JavaScript没有接口类型,本原则不像其他语言那样严格适用,但其核心思想仍具指导意义。

ISP强调"客户端不应被迫依赖其不使用的方法"。在JavaScript中,这表现为通过鸭子类型实现的隐式契约管理。

处理需要大型配置对象的类时,最佳实践是将参数设为可选并提供默认值。这避免了客户端为不使用的参数承担依赖,有效预防"臃肿接口"问题。

不良实践:

javascript
class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.settings.animationModule.setup();
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName("body"),
  animationModule() {} // Most of the time, we won't need to animate when traversing.
  // ...
});

优化方案:

javascript
class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.options = settings.options;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.setupOptions();
  }

  setupOptions() {
    if (this.options.animationModule) {
      // ...
    }
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName("body"),
  options: {
    animationModule() {}
  }
});

依赖倒置原则(DIP)

本原则包含两个核心要点:

  1. 一、高层模块不应依赖底层模块,两者都应依赖抽象
  2. 二、抽象不应依赖实现细节,实现细节应依赖抽象

虽然依赖注入(DI)与DIP并非完全相同的概念,但DIP通过DI实现(例如AngularJS的依赖注入),使得高层模块无需了解底层模块的细节。这种做法显著降低了模块间的耦合度,而高耦合正是导致代码难以重构的主因。

在JavaScript中,抽象依赖表现为隐式契约。例如库存跟踪类InventoryTracker隐式要求请求模块必须具备requestItems方法,这种契约允许具体实现的灵活替换。

不良实践:

javascript
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ["HTTP"];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryTracker {
  constructor(items) {
    this.items = items;

    // BAD: We have created a dependency on a specific request implementation.
    // We should just have requestItems depend on a request method: `request`
    this.requester = new InventoryRequester();
  }

  requestItems() {
    this.items.forEach(item => {
      this.requester.requestItem(item);
    });
  }
}

const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();

优化方案:

javascript
class InventoryTracker {
  constructor(items, requester) {
    this.items = items;
    this.requester = requester;
  }

  requestItems() {
    this.items.forEach(item => {
      this.requester.requestItem(item);
    });
  }
}

class InventoryRequesterV1 {
  constructor() {
    this.REQ_METHODS = ["HTTP"];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryRequesterV2 {
  constructor() {
    this.REQ_METHODS = ["WS"];
  }

  requestItem(item) {
    // ...
  }
}

// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
  ["apples", "bananas"],
  new InventoryRequesterV2()
);
inventoryTracker.requestItems();