前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript 中的 SOLID 原则

JavaScript 中的 SOLID 原则

作者头像
一颗小行星
发布2022-04-12 16:29:56
3740
发布2022-04-12 16:29:56
举报
文章被收录于专栏:混沌前端混沌前端

你可能已经了解过一些设计原则或者设计模式,本文主要渐进的讲解了SOLID原则:

- 不使用SOLID是怎么编写代码的,存在什么问题?

- 应该使用SOLID中的哪个原则?

- 使用SOLID我们应该如何对代码进行修改?

相信对比和沉浸式的示例会让你更容易理解SOLID原则,以及如何应用到代码实践中。

> 这是SOLID的一篇翻译文章,作者是[serhiirubets](https://hackernoon.com/u/serhiirubets)。

这是一篇汇总,你也可以分章阅读:

[JavaScript 中的 SOLID 原则(一):“S”代表什么](https://mp.weixin.qq.com/s/biFxh81cO2YLaIPZeTVC9A)

[JavaScript 中的 SOLID 原则(二):“O”代表什么](https://mp.weixin.qq.com/s/2PmY9YTgvEKR_-nxosW7dA)

[JavaScript 中的 SOLID 原则(三):“L”代表什么](https://mp.weixin.qq.com/s/D3Eq2dX0DWHwW3rFrLCYcg)

[JavaScript 中的 SOLID 原则(四):“I”代表什么](https://mp.weixin.qq.com/s/4FTAtP4rNjfetkcWU60Lxw)

[JavaScript 中的 SOLID 原则(五):“D”代表什么](https://mp.weixin.qq.com/s/ie7DZRUFkwQpxK4Ofi7mzQ)

在本文中,我们将讨论什么是 SOLID 原则,为什么我们应该使用他们和如何在JavaScript中使用他们。

#### 什么是SOLID

SOLID 是 Robert C. Martin 的前五个面向对象设计原则的首字母缩写词。 这些原则的目的是:让你的代码、架构更具可读性、可维护性、灵活性。

##### 单一职责原则(Single Responsibility Principle)

**S - 单一职责原则** 一个实体应该解决一项特定任务。

当我们的类(函数、组件、服务)做很多东西,那就会得到一堆关联的代码,如果改动一处可能会影响到其他地方,这些地方其实没有相关性。而且这个类很难维护,新增的代码改动可能会影响到其他地方,造成不可预知的问题。可读性也会很差,如果这个文件代码量很大,理解起来会异常痛苦。

我们先来看一端没有使用单一原则的示例:

```javascript

class Movie {

constructor(options){

this.name = options.name;

this.description = options.description;

this.rating = options.rating;

}

changeDescription (newDescription) {

this.description = newDescription;

}

changeRat ing (newRating) {

this.rating = newRating;

}

saveUserToFile() []

saveUserToDB() []

}

```

我们写了一个简单的类Movie,并提供了一个方法来修改描述、评级、保存电影到数据库或文件系统。看上去没有什么问题,但是考虑到未来可能新增的扩展:

- 我们可能会添加一些新的方法,比如:从数据库中获取一部电影的数据,在保存电影的时候进行验证,从数据库中删除电影等,我们的类将会是“God Object”反模式(“上帝模式”:一个类做了太多事情,或者把很多不相关的逻辑放到了一个类中来完成)。

- 我们可能会修改一个方法,很大概率上会影响其他地方。

- 重复的代码。我们可能还有其他的类,比如Audio或Picture,这些类可能也会使用类似的数据库、文件系统、和验证方法,我们应该怎么做呢?第一个想法可能是在每个类(Audio、Picture、Movie)中去写同样的方法,这刚好就是第二个反模式“DRY”(Don't repeat yourself.)。而且如果系统中包含很多类,每个类都有自己的方法,当做调整的时候大概率会忘记修改某个类的逻辑,这就会造成问题。

- 更难理解和维护。

那么如何重写代码逻辑来解决这些问题?我们应该先想起使用“单一职责原则”,“单一职责”实际上就是“一个实体解决一个特定的任务”。那再“Movie”类中有什么任务呢?

- 处理电影数据

- 操作数据库

- 操作文件系统

那我们就可以创建3个类:Movie、DB、FileSystem。

```javascript

class Movie {

   constructor(options) {

       this.name = options.name ;

       this.description = options.description;

       this.rating = options.rating;

  }

   changeDescription(newDescription) {

       this.description = newDescription;

  }

   changeRat ing (newRating) {

       this.rating = newRating;

  }

}

class DB {

   constructor(options) {

       this.url = options.url;

       this.loginname = options.loginname;

       this.password = options.password;

  }

   save(data) {}

   delete(id) {}

}

class FileSystem {

   constructor(options) {

       this.name = options.name;

  }

   save(data) {}

   delete(data) {}

}

```

现在我们有了3个独立的类,每个类只用来完成一个特定的任务。这样分离有以下好处:

- **DRY原则**。我们不需要再重复DB(文件)的逻辑,可以把任何实体(音乐、图片)传递给DB类,类会将他们保存到DB。

- 代码可读性更好,逻辑更简单。

- 没有了“God Object”

##### 开闭原则(Open-Closed Principle)

**O - 开闭原则**。实体(类、模块、方法、文件等)应该对扩展开放,对修改关闭。从定义上很难理解,来看几个例子:

假设:我们有几个不同的形状,圆形、方向、三角形,需要计算他们的面积总和。如何解决呢?

没什么难的,让我们为每个形状创建一个类,每个类有不同的字段:大小、高度、宽度、半径和类型字段。当计算每个形状的面积时,我们使用类型字段来区分。

```javascript

class Square{

constructor(size){

this.size = size;

this.type ='square' ;

}

}

class Circle{

constructor(radius) {

this.radius = radius;

this.type = 'circle' ;

}

}

class Rect{

constructor(width, height) {

this.width = width

this.height = height;

this.type = 'rect' ;

}

}

```

我们再创建一个函数,来计算面积。

```javascript

function getTotalAreas (shapes){

return shapes.reduce((total, shape) =>{

if (shape.type =='square') {

total += shape.size * shape.size;

}else if (shape.type = 'circle') {

total += Math.PI * shape.radius;

}else if (shape. type == ' rect') {

total += shape.width * shape.height;

}

return total;

}, 0);

}

getTotalAreas([

new Square(5),

new Circle(4),

new Rect(7,14)

]);

```

似乎看起来并没有什么问题,但是想象一下,如果我们想添加另一个形状(原型、椭圆、菱形),我们应该怎么做?我们需要为他们中的每一个创建一个新的类,定义类型并在getTotalAreas中添加新的if/else。

**注意:** **O - 开闭原则**。让我们再重复一遍:这个原则是指:实体(类、模块、方法等)应该对扩展开放,对修改关闭。

在getTotalAreas中,每次添加新的形状都需要进行修改。这不符合*开闭原则*,我们需要做什么调整?

我们需要在每个类中创建getArea方法(类型字段已经不再需要,已被删除)。

```javascript

class Square {

constructor(size) {

this.size = size;

}

getArea() {

return this.size * this.size;

}

}

class Circle {

constructor(radius) {

this.radius = radius;

}

getArea() {

return Math.PI * (this.radius * this.radius);

}

}

class Rect {

constructor(width, height) {

this.width = width;

this.height = height;

}

getArea() {

return this.width * this.height;

}

}

function getTotalAreas (shapes) {

return shapes. reduce((total, shape) => {

return total + shape. getArea();

},0)

}

getTotalAreas([

new Square(5),

new Circle(4),

new Rect(7,14)

]);

```

现在我们已经遵循了开闭原则,当我们要添加另一个形状,比如三角形,我们会创建一个Triangle类(对扩展开放),定义一个getArea方法,仅此而已。我们不需要修改getTotalAreas方法(对修改关闭),只需要在调用getTotalAreas时向其数组增加一个参数。

我们再来看一个更实际的例子, 假设客户端接收一个指定格式的错误验证消息:

```javascript

const response = {

errors: {

name: ['The name field should be more than 2 letters', 'The name field should not contains numbers'] ,

email: ['The email field is required'],

phone: ['User with provided phone exist']

}

}

```

想象一下,服务端可能会使用不同的服务来验证,可能是我们自己的服务,也可能是返回不同格式错误信息的外部服务。

让我们使用尽可能用简单的示例来模拟错误信息:

```javascript

const errorFromFacebook ='Bad credentials' ;

const errorFromTwitter = ['Bad credentials'];

const errorFromGoogle = { error: 'Bad credentials' }

function requestToFacebook() {

return {

type: 'facebook',

error: errorFromFacebook

}

}

function requestToTwitter() {

return {

type: 'twitter',

error: errorFromTwitte

}

}

function requestToGoogle() {

return {

type: 'google',

error: errorFromGoogle

}

}

```

我们来把错误信息转换成客户端所需要的格式:

```javascript

function getErrors() {

const errorsList = [requestToFacebook(), requestToTwitter(), requestToGoogle()];

const errors = errorsList.reduce((res, error) => {

if (error.type == ' facebook') {

res.facebookUser = [error.error]

}

if (error.type == 'twitter') {

res.twitterUser = error.error;

}

if (error.type == 'google') {

res.googleUser = [error.error];

}

return res;

},[]);

return { errors };

}

console.log(getErrors());

```

我们就得到了客户端所期望的结果:

```javascript

{

errors: {

facebookUser:['Bad credentials'],

twitterUser:['Bad credentials'],

googleUser:['Bad credentials']

}

}

```

但是,还是同样的问题,我们没有遵循**开闭原则**,当我们需要从外部服务添加一个新的验证时,我们就需要修改getErrors方法,添加新的if/else逻辑。

怎么解决这个问题呢?一个可行的解决方案是:我们可以创建一些通用的错误验证类,并在其中定义一些通用的逻辑。我们就可以为每个错误创建一个我们自己的类(FaceBookValidationError,GoogleValidationError)。

在每个类中,我们可以指定方法,像getErrors或TransformErrors,每个validationError类都应该遵循这个规则。

```javascript

const errorFromFacebook =' Bad credentials ' ;

const errorFromTwitter = ['Bad credentials'];

const errorFromGoogle = {error: ' Bad credentials'}

class ValidationError {

constructor(error) {

this.error = error;

}

getErrors() {}

}

class FacebookValidationError extends ValidationError {

getErrors() {

return { key: ' facebookUser', text:[this.error] };

}

}

class TwitterValidationError extends ValidationError {

getErrors() {

return {

key: ' twitterUser',

text: this.erro

}

}

}

class GoogleValidationError extends ValidationError {

getErrors() {

return { key: ' googleUser', text: [this.error.error] }

}

}

```

我们来在Mock的函数中使用这个错误验证类,修改getErrors函数:

```javascript

function requestToFacebook() {

return new FacebookValidationError(errorFromFacebook)

}

function requestToTwitter() {

return new TwitterValidationError(errorFromTwitter)

}

function requestToGoogle() {

return new GoogleValidationError(errorFromGoogle)

}

function getErrors (errorsList) {

const errors = errorsList.reduce((res, item) => {

const error = item.getErrors();

res[error.key] = error.text

return res ;

}, {});

return {errors}

}

console.log(getErrors([requestToFacebook(), requestToTwitter(), requestToGoogle()]));

```

可以看到,在getErrors函数接收errorList作为参数,而不是在函数中进行硬编码。运行结果是一样的,但是我们遵循了开闭原则,当新增一个错误时:我们可以为这个错误创建一个新的验证类并且指定getErrors方法(对扩展开放),getErrors可以帮我们把外部服务返回的信息转换成我们需要的格式。我们在通用的getErrors方法中来调用错误类的getErrors,无需进行其他修改(对修改关闭)。

#### 里氏替换原则(Liskov Substitution Principle)

**L - 里氏替换原则**。这个原则是指:如果S是T的子类型,那么程序中的T对象可以被S对象替换,不需要改变程序中任何所需属性。从定义上可能没有办法清晰的理解其含义,我们稍微换一个说法:函数中使用的指针或引用基类必须可以替换为其派生类。

让我们用更简单的方式来描述它,例如:你有一个“Car”类,并且在不同地方进行了使用。这个原则的意思是:每一个使用Car类的地方,都应该可以被Car类的子类替换。如果我们有一个继承自“Car“的“Passenger Car”, 或者有一个“SUV”类也继承自“Car“,如果我们把“Car”类替换成“SUV”类或者“Passenger Car”类,即把父类Car替换成任何一个子类后,我们的系统应该像以前一样正常工作。

举个简单的例子,我们有一个“Rectangle”(矩形)类,因为”正方形“也是“矩形”,我们可以创建一个基本的“Rectangle”类和“Square”类,“Square”继承自“Rectangle”。

```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

}

}

// Square计算面积的方式有点不同,它的高度和宽度一样的,重写setWidth和setHeight方法。

class Square extends Rectangle {

setWidth(width) {

this.width = width;

this.height = width;

}

setHeight(height) {

this.width = height;

this.height = height;

}

}

```

```javascript

const rectangleFirst = new Rectangle(10, 15)

const rectangleSecond = new Rectangle(5, 10)

console.log(rectangleFirst.getArea()); // 150

console.log(rectangleSecond.getArea()); // 50

rectangleFirst.setWidth(20)

rectangleSecond.setWidth(15)

console.log(rectangleFirst.getArea()); // 300

console.log(rectangleSecond.getArea()); // 150

```

我们创建了两个实例,查看了矩形面积,更改宽高并再次检查了面积,我们看到一切正常,代码按预期工作,但是,让我们再看一下**里氏替换原则**:如果我们更改任何子类的基类,我们的系统应该像以前一样工作。

```javascript

const rectangleFirst = new Square(10, 15)

const rectangleSecond = new Square(5, 10)

console.log(rectangleFirst.getArea()); // 150

console.log(rectangleSecond.getArea()); // 50

rectangleFirst.setWidth(20)

rectangleSecond.setWidth(15)

console.log(rectangleFirst.getArea()); // 400

console.log(rectangleSecond.getArea()); // 225

```

我们把`new Rectangle()` 替换为`new Square()`后发现,在`setWidth`之后, `getArea`返回了和替换之前不同的值,很明显我们没有遵循里氏替换原则。

那么我们应该怎么解决呢?解决方案是使用继承,但不是从”Rectangle“类,而是准备一个更“正确”的类。比如,我们创建一个“Sharp”类,它只负责计算面积:

```javascript

class Shape {

getArea() {

return this.width * this.height;

}

}

class Rectangle {

constructor(width,height) {

super();

this.width = width

this.height = height

}

setWidth(width) {

this.width = width

}

setHeight(height) {

this.height = height

}

}

class Square extends Shape {

setWidth(width) {

this.width = width;

this.height = width;

}

setHeight(height) {

this.width = height;

this.height = height;

}

}

```

我们创建了一个更通用的基类`Shape`,在使用`new Shape()`的地方我们都可以把`Shape`修改为任何它的子类,而不会破坏原有逻辑。

在我们的示例中,Rectangle和Square是不同的对象,它们包含了一些相似的逻辑,但也有不同的逻辑,所以把他们分开而不是用作“父子”类会更正确。

我们再来看一个对理解这个原则有帮助的例子:

我们要创建一个`Bird`类,我们正在考虑应该添加什么方法,从第一个角度来看,我们可以考虑添加`fly`方法,因为所有的鸟都会飞。

```javascript

class Bird{

fly(){}

}

function allFly(birds) {

birds.forEach(bird => bird.fly())

}

allFly([new Bird(), new Bird(), new Bird()])

```

之后,我们意识到存在不同的鸟类:鸭子、鹦鹉、天鹅。

```javascript

class Duck extends Bird {

quack(){}

}

class Parrot extends Bird {

repeat(){}

}

class Swan extends Bird{

beBeautiful(){}

}

```

现在,里氏替换原则说,如果我们把基类更改为子类,系统应该像以前一样工作:

```javascript

class Duck extends Bird {

quack(){}

}

class Parrot extends Bird {

repeat(){}

}

class Swan extends Bird{

beBeautiful(){}

}

function allFly(birds){

birds.forEach(bird=> bird.fly())

}

allFly([new Duck(), new Parrot(), new Swan()])

```

我们在调用`allFly`函数时,改变了参数,我们调用了`new Duck()`,`new Parrot()`,`new Swan()`, 而不是调用`new Bird()`。一切正常,我们正确的遵循了里氏替换原则。

现在我们想再添加一只企鹅,但是企鹅并不会飞,如果想调用`fly`方法,我们就抛出一个错误。

```javascript

class Penguin extends Bird {

fly(){

throw new Error('Sorry, but I cannot fly')

}

swim(){}

}

allFly([new Duck(), new Parrot(), new Swan(), new Penguin()])

```

但是我们遇到一个问题:fly方法并不期望出现内部错误,allFly方法也只是为会飞的鸟创建的,企鹅不会飞,所以我们违背了里氏替换原则。

怎么解决这个问题?与其创建一个基本的`Bird`类,不如创建一个`FlyingBird`类,所有会飞的鸟都只继承自`FlyingBird`类,allFly方法也只接受`Flying Bird`。

```javascript

class Bird{

}

class FlyingBird{

fly(){}

}

class Duck extends FlyingBird {

quack(){}

}

class Parrot extends FlyingBird {

repeat(){}

}

class Swan extends FlyingBird{

beBeautiful(){}

}

class Penguin extends Bird {

swim(){}

}

```

`Penguin`继承自Bird类,而不是FlyingBird类,我们也不需要调用会引发错误的fly方法。在任何调用FlyingBird的地方,可以直接换成更具体的鸟类,比如Duck、Parrot、Swan,代码也会正常工作。

希望你可以通过本文能够更好的理解*里氏替换原则*,了解在JavaScript是如何工作的和如何在项目中使用。

#### 接口隔离原则(Interface Segregation Principle)

**I - 接口隔离原则**。这个原则是指:客户端不应该依赖他们不使用的接口(接口应该是精简的,拥有尽可能少的行为)。

这是什么意思? 这个原则是关于接口的,但是在JavaScript中没有接口,不过有类似的东西,那就是类。虽然两者不一样,但是这个原则可以应用到JS类上。

对于JS类来说,这个原则是指当我们创建一个基础类,需要在其中定义所有子类都会用到的方法,并且避免只有部分子类会用到的方法。

举个简单的例子,当我们创建一个`Transport`的基础类并添加以下方法:move、stop、fly和sail。示例中的方法只添加了`console.log`,实际应用中对应的应该是真正的业务逻辑。

```javascript

class Transport {

move() {

console.log('move');

}

stop() {

console.log('stop');

}

fly() {

console.log('fly');

}

sail() {

console.log('sail');

}

}

```

我们再创建三个子类:Plane, Car 和 Ship。

```javascript

class Plane extends Transport {

sail() {

return null;

}

}

class Car extends Transport {

fly() {

return null ;

}

sail() {

return null;

}

}

class Ship extends Transport {

fly() {

return null ;

}

}

```

你可能注意到了,每个子类中重写了继承的方法,并返回了`null`。为什么这么做呢,拿Plane举例,飞机可以fly和move,但是不能sail(船类航行)。

我们的基类包含了sail逻辑,但是飞机不能sail。我们应该做一些事情,因为有人可能会调用plane实例上的sail方法,我们可以抛出错误或者像现在一样重写sail方法。其他两个类也是使用同样的处理方式,Car重写了fly和sail,ship重写了fly。

所以问题在于:我们创建的基类包含的方法,有的子类可以使用,但其他的子类不能。这就是**接口隔离原则**所指的:我们不应该在基类中创建子类不会使用到的逻辑。

当然,这个和多态没有关系,如果我们创建了一个通用的方法,但是每个子类都会重写这个方法逻辑,是可以的。

举个例子:我们有一个Animal基类,包含一个breathe方法,它的子类也可以breathe但是使用了不同的方式,我们可以使用多态:

```javascript

class Animal {

breath() {

console.log('common breath')

}

}

class Human extends Animal {

breath() {

console.log('lung breath')

}

}

class Fish extends Animal {

breath() {

console.log('gills breath')

}

}

```

再重温一下**接口隔离原则**:正确的在基类中创建方法,这些方法应该被继承的子类所使用。

那么我们怎么解决Transport类中的问题呢?我们可以创建更具体的子类,子类中包含只有自身会使用到的方法:

```javascript

class Transport {

move() {

console.log('move')

}

stop() {

console.log('stop')

}

}

class FlyingTransport extends Transport {

fly() {

console.log('fly')

}

}

class SailingTransport extends Transport {

sail() {

console.log('sail')

}

}

class Car extends Transport {}

class Plane extends FlyingTransport {}

class Ship extends SailingTransport {}

```

现在我们的Transport基类包含了move和stop两个方法,这两个方法可以用在所有的子类。我们还创建了两个具体的子类,plane可以继承FlyingTransport,轮船可以继承SailingTransport。

这就是“SOLID”原则中“I”的含义:这个原则主要的目的是让代码拥有良好的层次结构,尽量不要在基类中创建子类不需要的方法。

#### 依赖倒置原则

**D - 依赖倒置原则** 这个原则是指:高级模块不应该依赖低级模块;两者都应该依赖于抽象,抽象应该不依赖于细节,细节应该取决于抽象。

举个例子,假设我们想处理电影数据,我们创建了一个简单的Movie类:

```javascript

class Movie {

 constructor(title,description) {}

}

```

我们还需要保存视频信息到localStorage,为了遵循**单一职责原则**,我们单独创建一个类:

```javascript

class MovieStorage {

 setItem(data) {}

 getItemById(id) {}

 getAll() {}

}

```

一切都很好,而且我们的逻辑会在其他地方使用。

```javascript

const movieStorage = new MovieStorage()

const ironMan = new MovieStorage('Iron man', 'Movie about Iron man')

const spiderMan = new MovieStorage('Spider man', 'Movie about Spider man')

movieStorage.setItem(ironMan)

movieStorage.setItem(spiderMan)

// here could be different other logic

movieStorage.getItemById(1);

```

如果我们想把数据修改为存储到本地文件系统,没问题,再创建一个类:

```javascript

class MovieFileStorage {

 save(data){}

 editFile(data){}

 readMovieById(id){}

 readAllMovies(){}

}

```

现在我们需要把之前使用localStorage的地方,替换成fileStorage

![Snipaste_2022-04-11_17-45-14.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c867d505b38412b8637483361a83118~tplv-k3u1fbpfcp-watermark.image?)

看起来没有什么问题,我们只是删除并替换了4行代码,但就像我们之前讨论的,如果你在很多文件中多次使用了local Storage,很难找到所有使用的地方并正确的修改它们。而且如果你已经为此写了测试,所有测试也需要进行修改。

修改后的代码可以正常工作,但是随着时间的推移,对本地文件系统占用越来越大,我们打算切换到数据库进行存储,MongoDB或SQL,我们应该怎么做?遵循“单一职责原则”,我们创建了一个DB存储类:

```javascript

class MovieDBStorage{

 insert(data){}

 update(data){}

 selectAll(){}

 selectById(id){}

}

```

现在我们遇到了同样的问题,我们需要查找所有的文件,把文件系统的逻辑修改为数据库操作,需要查找到所有相关文件中的调用并修改方法名,签名,为此编写的测试也需要进行调整。

![image-20220411152435036](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2960f53de6a45dabfd163f64b60447f~tplv-k3u1fbpfcp-zoom-1.image)

使用的地方越多,修改起来越困难,这也是导致代码出现bug的原因之一。

希望你可以理解这个问题,我们来看看怎么才能避免它,还记得依赖倒置原则吗:**高级模块不应该依赖低级模块;两者都应该依赖于抽象,抽象不应该依赖细节,细节应该取决于抽象。**

我们从抽象开始重构吧:我们创建一个MoveStorage类,这个类将会是我们的“抽象”。抽象不应该依赖细节,我们应该怎么实现它呢?很简单,我们为MoveStorage类创建方法,这些方法用来代替MoveDBStorage,MovieFileStorage。

```javascript

class MovieStorage {

 save(data) {}

 edit(data) {}

 getById(id) {}

 getAll() {}

}

```

这个类就像一个接口,我们所有的代码都会使用这些方法,它们的名称不会被改变,也就是说我们的高级模块(我们使用“抽象”的地方)将不依赖于我们的内部逻辑。

接下来,我们为每个存储方式创建特定的类,而且每个类使用的方法名、参数都和我们的“抽象”类保持一致:

```javascript

class MovieFileStorage {

 save(data) {}

 edit(data) {}

 getById(id) {}

 getAll() {}

}

class MovieDBStorage {

 save(data) {}

 edit(data) {}

 getById(id) {}

 getAll() {}

}

```

最后我们来调整我们的“抽象”:

```javascript

class MovieStorage {

constructor (storage) {

this.storage = storage;

}

save(data) {

this.storage.save(data)

}

edit(data) {

this.storage.edit(data)

}

getById(id) {

this.storage.getById(id)

}

getAll() {

this.storage.getAll()

}

}

```

现在我们的“抽象”已经不依赖细节了,MovieStorage接收任何存储的实例,并且实例遵循我们的接口:

```javascript

const movieStorage = new MovieStorage(new MovieFileStorage())

movieStorage.save(ironMan)

movieStorage.save(spiderMan)

moveStorage.getById(1)

```

如果我们想把文件存储修改为缓存存储、本地/会话存储、MongoDB、SQL等,我们只需要准备对应的存储类(用于mongo、redis、sql),它应该实现和我们的“抽象”同名的方法,并把新的类实例传递到构造器中:

```javascript

const movieStorage = new MovieStorage(new MovieDBStorage())

movieStorage.save(ironMan)

movieStorage.save(spiderMan)

movieStorage.getById(1)

```

我们只是改变了传递的参数:MovieStorage接收的实例从`new MovieFileStorage()`修改成了`new MovieDBStorage()`。我们不需要查找并修改所有的文件,也不需要修改已有的测试。我们所有的文件都使用了相同的抽象,而且我们的抽象不依赖于逻辑,抽象即逻辑。

这就是JS中“SOLID”的收尾,希望你可以在时间中至少使用到他们中的一个。你可以全部使用,也可以只选择一个,比如:*单一职责原则*,查看你的代码是否都遵循了这个原则,如果没有,那就重构你的代码吧。

你也可以使用“依赖倒置原则”,并检查你的代码是否符合这个原则,幸运的是,像“Angular”或“NestJS”这些框架遵循了这个原则,你可以在使用他们的项目中看到具体的实践。

我们来做个回顾吧:

1、单一职责原则(SRP):一个类应该有且只有一个职责,解决一项特定任务。

2、开放封闭原则(OCP):一个类应该对扩展开放,对修改关闭。一个类在应用的其他地方已经开始使用,就不应该再修改它。

3、里氏替换原则(LSP):派生的子类应该是可替换基类的,也就是说任何基类可以出现的地方,都可以被子类替换。值得注意的是,当通过继承实现多态行为时,如果派生类没有遵守LSP,可能会让系统引发异常。

4、接口隔离原则(ISP):基类不应该包含他们子类不使用的方法,也就是说一个接口应该拥有尽可能少的行为。应该把那些大而全的接口拆分成一些小的、具体的接口,这样客户端就只需关心他们要用到的接口。

5、依赖倒置原则(DIP):高级模块不应该依赖低级模块,相反,他们应该依赖抽象类或者接口。也就是不应该在高级模块中使用具体的低级模块,应该遵从依赖于抽象(接口)而不是一个实例(类)。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MongoDB
腾讯云数据库 MongoDB(TencentDB for MongoDB)是腾讯云基于全球广受欢迎的 MongoDB 打造的高性能 NoSQL 数据库,100%完全兼容 MongoDB 协议,支持跨文档事务,提供稳定丰富的监控管理,弹性可扩展、自动容灾,适用于文档型数据库场景,您无需自建灾备体系及控制管理系统。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档