一文搞懂SOLID原则(javascript)

2023-03-06 15:57:13 浏览数 (3)

SOLID 是面向对象编程重要的原则,javascript 作为面向对象开发的语言之一,掌握这些原则,可以写出更优雅的代码。

SOLID 由 Robert C. Martin(其被誉为世界编程大师,设计模式、敏捷开发先驱)在21世纪初定义。

采用 SOLID 编程,可以让代码的 ① 可持续性 ② 扩展性 ③ 鲁棒性(健壮性)得到有效的提高

SOLID

  • SRP(Single Responsibility Principle)-单一职责原则
  • OCP(Open Closed Principle)-开闭原则
  • LSP(Liskov Substitution Principle)-里氏替换原则
  • ISP(Interface Segregation Principle)-接口隔离原则
  • DIP(Dependency Inversion Principle)-依赖反转原则

SRP(Single Responsibility Principle)-单一职责原则

SRP(Single Responsibility Principle)单一职责原则。一个类都应该只对某一类职责负责。 需求变更/升级,往往需要通过更改职责相关的类来体现。如果一个类拥有多个职责,对于某一职责的更改可能会破坏其他耦合的职责,产生无法预期的破坏。

不推荐:UserSettings 中拥有多个职责 UserSettings 除了身份设置功能,还包含了验证身份verifyCredentials 的功能(职责),使其耦合度较高

代码语言:javascript复制
class UserSettings {
  constructor(user) {
    this.user = user;
  }

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

  verifyCredentials() {
    // ...
  }
}

推荐:为每一个职责创建不同的类。 针对身份验证部分,单独放到 UserAuth class 中。

代码语言: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()) {
      // ...
    }
  }
}

好处:① 避免 class 或 function 臃肿;② 修改单一内容不易引起其他串联问题;③ 多人协同开发过程中,减少合并代码冲突;④ 扩展性、易读性等更加容易。

OCP(Open Closed Principle)-开闭原则

OCP (Open Closed Principle)开闭原则。软件实体(类,模块,方法等)应该对扩展开放,对修改封闭。 当需求变化时,可以通过添加新的代码来扩展这个模块的行为,而不去更改那些已经存在的可以工作的代码。

不推荐:扩展和修改全部放到了 HttpRequester 中 对于新增加一种请求类型(如:axiosAdapter),需要修改 HttpRequester 来做处理,随之类型的增多,会变得异常庞大,难以维护。

代码语言:javascript复制
class xmlHttpReqAdapter {
  constructor() {
    this.name = "XMLHttpRequest";
  }
}

class fetchAdapter {
  constructor() {  
    this.name = "fetch";
  }
}

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

  fetch(url) {
    if (this.adapter instanceof xmlHttpReqAdapter) {
    	// makeXmlHttpRequestCall:transform response and return   
    } else if (this.adapter instanceof fetchAdapter) {
      // makeFetchCall:transform response and return
    }
  }
}

推荐:request 方法封装的各自类中,保持完成整性的同时,更易扩展新类型 对于扩展新的类型,提供了开放性,同时针对某类型实现方式全部由自己内部控制

代码语言: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(Liskov Substitution Principle)-里氏替换原则

LSP(Liskov Substitution Principle)里氏替换原则。继承必须确保超类所拥有的性质在子类中仍然成立。 举个经典示例来解释:从数学概念上,正方形属于长方形的一种;代码实现上,我们可以 正方形 extends 长方形,在这种场景下,我们要确保对于正方形的实现不能破坏原有长方形逻辑。 子类可以扩展父类的功能,但不能改变父类原有的功能。 也就是说,当子类继承父类时,除了添加新的方法完成新增功能外,尽量不要重写父类的方法。

不推荐:子类改写了父类行为 正方形继承了长方形,改写了 setWidth/setHeight 方法,但结果出现了问题

代码语言:javascript复制
class Rectangle {
  constructor () {
    this.width = 0;
    this.height = 0;
  }
  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;
  }
}

let s = new Square()
s.setHeight(4)
s.setWidth(5)
s.getArea()	// 25

推荐:扩展新的函数

代码语言:javascript复制
class Square extends Rectangle {
  setSize(size) {
    this.size = size;
  }
  getArea () {
    return this.size * this.size
  }
}

继承的开发方式给代码带来了侵入性,可移植能力降低, 类之间的耦合度较高。 当对父类修改时,就要考虑一整套子类的实现是否有风险,测试成本较高。

里氏替换原则的目的是使用约定的方式,让使用继承后的代码具备更良好的扩展性和兼容性。

ISP(Interface Segregation Principle)-接口隔离原则

ISP(Interface Segregation Principle)接口隔离原则。多个专用的接口比一个通用接口好。一个类决不要实现不会用到的接口。所以,实现多个特定的接口比实现一个通用接口要好。 JavaScript 中没有接口,下述以 typescript 为例。

不推荐:Animal并不具备work属性

代码语言:javascript复制
interface AnimalInterface {
  name: string;
  eat(something: string): void;
  // 新扩展
  work(): void;
}

class Person implements AnimalInterface {}

推荐:单独抽离接口

代码语言:javascript复制
interface AnimalInterface {
  name: string;
  eat(something: string): void;
}
interface PersonInterface {
  work(): void;
}

class Person implements AnimalInterface,PersonInterface {}

好处:① 解耦 ② 易于扩展 ③ 修改不会互相影响

DIP(Dependency Inversion Principle)-依赖反转原则

DIP(Dependency Inversion Principle)依赖反转原则。 在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,以此构建一个复杂系统。在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务,但对于低层次组件的依赖限制了高层次组件被重用的可行性。 依赖反转原则的目的是把高层次组件从对低层次组件的依赖中解耦出来,这样使得重用不同层级的组件实现变得可能。

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。 抽象不应该依赖于具体实现细节,而具体实现细节应该依赖于抽象。

不推荐:高层模块直接依赖低层模块 高层模块包含着应用程序中重要的业务决策信息,是这些业务模型包含了应用程序的功能特征。当这些模块依赖于低层模块时,对低层模块的修改将直接影响高层模块

代码语言:javascript复制
class Lower {}
class Upper {
  // 调用低层模块
  new Lower()
}

现需要增加新扩展的相似低层模块

代码语言:javascript复制
/**方式一:改动上次模块,增加判断分支*/
class Lower2 {}
class Upper {
  if (isLower) { new Lower() }
  if (isLower2) { new Lower2() }
}
/**方式二:改动低层模块,已兼容新扩展功能*/
class Lower {
  if (...)
}

随着功能的扩展,方式一会导致高层模块非常庞大、难以维护;方式二会导致高层模块可能会受到低层模块修改带来的副作用。

推荐: 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

代码语言:javascript复制
interface LowerInterface {}
class Lower1 implements LowerInterface {}
class Lower2 implements LowerInterface {}
class Upper {
  lower: LowerInterface;
  setLower(l: LowerInterface) {
    this.lower = l
  }
	// 统一调用
	this.lower.xxx
}

该原则不同于依赖注入(其为设计模式的一种,目的为了各个模块之间不做相互依赖)

0 人点赞