如何写出优雅的 JS 代码?使用 SOLID 原则

作者:ryanmcdermott
译者:前端小智
来源:github
点赞再看,养成习惯

本文 GitHub https://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

设计模式的六大原则有:

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则

把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。下面我们来分别看一下这六大设计原则。

单一职责原则(SRP)

单一功能原则 :单一功能原则 认为对象应该仅具有一种单一功能的概念。

换句话说就是让一个类只做一种类型责任,当这个类需要承担其他类型的责任的时候,就需要分解这个类。在所有的SOLID原则中,这是大多数开发人员感到最能完全理解的一条。严格来说,这也可能是违反最频繁的一条原则了。单一责任原则可以看作是低耦合、高内聚在面向对象原则上的引申,将责任定义为引起变化的原因,以提高内聚性来减少引起变化的原因。责任过多,可能引起它变化的原因就越多,这将导致责任依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合度。单一责任,通常意味着单一的功能,因此不要为一个模块实 现过多的功能点,以保证实体只有一个引起它变化的原因。

不好的写法

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

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

  verifyCredentials() {
    // ...
  }
}

好的写法

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)

软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。

  1. 通过增加代码来扩展功能,而不是修改已经存在的代码。
  2. 若客户模块和服务模块遵循同一个接口来设计,则客户模块可以不关心服务模块的类型,服务模块可以方便扩展服务(代码)。
  3. OCP支持替换的服务,而不用修改客户模块。

说大白话就是:你不是要变化吗?,那么我就让你继承实现一个对象,用一个接口来抽象你的职责,你变化越多,继承实现的子类就越多。

不好的写法

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
}

好的写法

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)

里氏替换原则 :里氏替换原则 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。

LSP则给了我们一个判断和设计类之间关系的基准:需不需 要继承,以及怎样设计继承关系。

当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。继承对于OCP,就相当于多态性对于里氏替换原则。子类可以代替基类,客户使用基类,他们不需要知道派生类所做的事情。这是一个针对行为职责可替代的原则,如果ST的子类型,那么S对象就应该在不改变任何抽象属性情况下替换所有T对象。

客户模块不应关心服务模块的是如何工作的;同样的接口模块之间,可以在不知道服务模块代码的情况下,进行替换。即接口或父类出现的地方,实现接口的类或子类可以代入。

不好的写法

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);

好的写法

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)

接口隔离原则 :接口隔离原则 认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。

不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

这个原则起源于施乐公司,他们需要建立了一个新的打印机系统,可以执行诸如装订的印刷品一套,传真多种任务。此系统软件创建从底层开始编制,并实现了这些 任务功能,但是不断增长的软件功能却使软件本身越来越难适应变化和维护。每一次改变,即使是最小的变化,有人可能需要近一个小时的重新编译和重新部署。这 是几乎不可能再继续发展,所以他们聘请罗伯特Robert帮助他们。他们首先设计了一个主要类Job,几乎能够用于实现所有任务功能。只要调用Job类的 一个方法就可以实现一个功能,Job类就变动非常大,是一个胖模型啊,对于客户端如果只需要一个打印功能,但是其他无关打印的方法功能也和其耦合,ISP 原则建议在客户端和Job类之间增加一个接口层,对于不同功能有不同接口,比如打印功能就是Print接口,然后将大的Job类切分为继承不同接口的子 类,这样有一个Print Job类,等等。

不好的写法

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

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

  traverse() {
    // ...
  }
}

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

好的写法

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)

依赖倒置原则: 依赖倒置原则 认为一个方法应该遵从“依赖于抽象而不是一个实例” 的概念。依赖注入是该原则的一种实现方式。

依赖倒置原则(Dependency Inversion Principle,DIP)规定:代码应当取决于抽象概念,而不是具体实现。

  • 高层模块不要依赖低层模块
  • 高层和低层模块都要依赖于抽象;
  • 抽象不要依赖于具体实现
  • 具体实现要依赖于抽象
  • 抽象和接口使模块之间的依赖分离

类可能依赖于其他类来执行其工作。但是,它们不应当依赖于该类的特定具体实现,而应当是它的抽象。这个原则实在是太重要了,社会的分工化,标准化都 是这个设计原则的体现。显然,这一概念会大大提高系统的灵活性。如果类只关心它们用于支持特定契约而不是特定类型的组件,就可以快速而轻松地修改这些低级 服务的功能,同时最大限度地降低对系统其余部分的影响。

不好的写法

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();

好的写法

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) {
    // ...
  }
}
const inventoryTracker = new InventoryTracker(
  ["apples", "bananas"],
  new InventoryRequesterV2()
);
inventoryTracker.requestItems();

原文:https://github.com/ryanmcderm...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

如何写出优雅的 JS 代码?使用 SOLID 原则_第1张图片

你可能感兴趣的:(javascript,前端,html,设计模式)