SOLID的 JavaScript 实现

648 阅读6分钟

设计模式为什么重要? 因为可读性? 因为少 bug? 因为好测试? 我觉得这些都不是最主要的, 就像狗刨和有泳姿的游泳, 只有掌握了泳姿, 游泳才会有乐趣, (去游泳馆洗眼睛的除外). 设计模式可以让我们写代码有章可循, 写出艺术性.

JavaScript 里万物皆对象, 虽然 ES6 之前一直是用比较让人头晕的原型链, 加上__proto__在各个浏览器中的实现都不一致, 引起了许多人的困惑, 导致就一个简简单单的继承都有好几种写法. 好在 ES6 之后有了 class, 虽然本质仍是原型链的语法糖, 但至少规范了继承的写法, 官方认定的写法.

所以先说面向对象必须的 SOLID 法则

S-Single Responsibility 单一职责

一个类只应该负责一个职责就是这条原则的全部, 但是如何让一个类只负责一个职责? 怎么样才算是一个职责? 好, 先上一个 violation.

class HttpClient {
    getPosts(url) {
        fetch(url,{
            headers: {
                'Accept': 'application/json'
            }
        })
        .then(response => {
            if(response.ok) return response.json()
            else if (response.statue === 401) {
               // 401的处理逻辑 
            } else if (response.statue === 404) {
                // 404的处理逻辑
            }
        })
    }
}

想法是很好的, 因为在使用 fetch 的时候难免遇到错误, 所以按照正向思维思考下来那么肯定是要把错误进行处理的, 需要分好几种情况, 进行弹窗或者是 toast 都是对用户比较友好的交互.

相信你也看到了, 这里其实职责不够明确的, 因为 HttpClient 只应该负责发出请求并返回数据, 错误的处理应该单独拎出来一个函数来解决.

import ErrorHandler from 'xxx'
class HttpClient {
    getPosts(url) {
        fetch(url,{
            headers: {
                'Accept': 'application/json'
            }
        })
        .then(response => {
            if(response.ok) return response.json()
            else {
                return ErrorHandler.handleResponseStatue(response.statue)
            }
        })
    }
}

ErrorHandler类专门处理错误, HttpClient专门处理请求, 两者职责分明清晰.这就是 S 的意义. 至于划分职责的粒度, 没有很明确的标准, 需要找到一个平衡点,如果一个类只有一个功能,那么会有很多的类, 降低可读性 如果一个类糅杂太多, 又会提高耦合度.

O-Open/Close 开闭原则

对扩展开放, 对更改关闭(Open to extensions, close to modifications). 什么是扩展? 什么是更改? 不能以其昏昏使人昭昭, 接下来还是一个 violation的例子

class Person {
    static studentID = 'xxx'
    static employID = 'xxx'
    static professorID = 'xxx'
    constructor(name, age, id) {
        this.name = name
        this.age = age
        this.id = id
    }
    // 返回人员类型
    get type () {
        return this._type
    }
    // 设置人员类型
    set type(type) {
        this._type = type
    }
    authorize() {
        if (!this.type) throw Error('No type signature')
        if (this.type === 'student') return this.id === Person.studentID
        elif(this.type === 'employ') return this.id === Person.employID
        elif(this.type === 'professor') return this.id === Person.professorID
    }
}
const studentA = new Person('lorry', 26, 123)
const employA = new Person('Lebron', 29, 321)
studentA.type = 'student'
employA.type = 'employ'
studentA.authorize()
employA.autorize()

看起来还行是不?回到上面那个问题, 好扩展吗? 不好, 因为别的类可能不需要 authorize 这个方法, 也不需要 type, 比如我要扩展一个有地域的类 ChinaPerson. 对修改关闭吗? 也没有, 可以任意的更改 type 的值. 下面是优化版本

class Person {
  constructor(name, age) {
      this.name = name
      this.age = age
  }
}
class Student extends Person {
  static AuthorizedID = 'xxx'
  constructor(id, name, age) {
    super(name, age)
    this.id = id
  }
  authorize() {
    return this.id === Student.AuthorizedID
  }
}

class Employ extends Person {
  static AuthorizedID = 'xxx'
  constructor(id, name, age) {
    super(name, age)
    this.id = id
  }
  authorize() {
    return this.id === Employ.AuthorizedID
  }
}

const student = new Student(123, 'lorry', 26)
const employ = new Employ(321, 'Lebron', 30)
student.authorize()
employ.authorize()

这下要扩展就好多了, 而且 Person, Student, Employ 这三个类在 new 之后就无法进行修改了, 实现对扩展开放, 对修改封闭

L-Liskov Substitution Principle LSP里氏替换原则

这是由Liskov在1897年提出来的. 该原则表示任何基类(父类)可以出现的地方, 子类都可以出现, 子类的使用不会破环软件的功能性. 里氏替换原则是为了表述继承关系的, 即子类应该继承父类的所有方法, 并且不破环父类的接口定义.来看看下面的violation

class ProductStorage {
    products = []
    get length = () => this.products.length
    save(product) {
        this.products.push(product)
    }
}
class Product {
    constructor(name, price) {
        this.name = name
        this.price = price
    }
    save(storage) {
        storage.save({name: this.name, price: this.price})
        return storage.length
    }
}
class DiscountProduct extends Product {
    constructor(name, price, discount){
        super(name, price)
        this.discont = discont
    }
    save(storage) {
        const discounted = {name: this.name, price: this.price * (1-this.discont)})
        storage.save(discounted)
        return disconted
    }
}
const products = [
    {
        name: 'cat',
        price: 1000
    },
    {
        name: 'airplane',
        price: 500000
    },
    {
        name: 'mobilePhont',
        price: 300,
        discount: 0.2
    }
]
function insertAll(products) {
    let storage = new ProductStorage()
    for p of products {
        let product
        if (p.discount) {
            produce = new DiscountProduct(...p)
        } else {
            product = new Product(...p)
        }
        product.save(storage)
        console.log(`product saved, the things count is ${storage.length}`)
    }
}
insertAll(products)

代码很少相信大家已经看出来问题所在了, save的方法中子类实现与基类的实现接口时不一样的, 基类返回的是一个数字, 而子类返回的是一个对象, 这就违背了里氏替换原则, 修改也很简单

class DiscountProduct extends Product {
    // ...
    save(storage) {
        const discounted = {name: this.name, price: this.price * (1-this.discont)})
        storage.save(discounted)
        return storage.length
    }
}

I-Interface Segregation 接口隔离原则

JavaScript中没有接口的概念, 但是可以用类来模拟接口.强烈推荐ts, 以后我会再写下ts相关内容 先来解释下接口隔离的概念:不要包含任何没有实现的接口.比如

这个图表示Shape实现了IDrawable, IDrawable由两个属性, draw和calculateArea计算面积.然后Rectangle和Line继承了Shape, 所以分别都会由IDrawable的这两个方法实现.

class Shape {
    draw() {
        throw Error(`haven't implement this method yet`)
    }
    calculateArea() {
        throw Error(`haven't implement this method yet`)
    }
}
class Rectangle extends Shape {
    constructor(x1,y1,x2,y2) {
        this.height = y2 - y1
        this.width = x2 - x1
        this.startX = x1
        this.startY = y1
    }
    draw() {
        drawRectangle(this.startX, this.startY, this.width, this.height) // 绘制矩形函数
    }
    calculateArae() {
        return this.height * this.width
    }
}
class Line extends Shape

那么问题来了, Line是没有面积可以计算的, 这就违反了接口隔离. 可以将其更改为

class Shape {
    draw() {
        throw Error(`haven't implement this method yet`)
    }
}

这样就符合接口隔离的概念了.只包含充分且必要的接口. 不管是基类还是实现类

D-Dependencies Inversion 依赖倒置

这是Solid里最后一个设计模式, 也是我认为最不好理解的一个设计模式. 概念是指程序应该依赖于抽象的接口, 而不是具体的实现, 这样可以降低跟依赖的耦合度(不用去管依赖的实现了). 来看下一个violation

class BMWCar {
    
}
class BENSCar {
    
}
class AutoSystem {
    constructor(type) {
        this.bmw = new BMWCar()
        this.benz = new BENZCar()
        this.type = type
    }
    runCar () {
        if(this.type === 'bmw'){
            this.bmw.run()
        } else {
            this.benz.run()
        }
    }
    stop() {
        if(this.type === 'bmw') {
            this.bmw.stop()
        } else {
            this.benz.stop()
        }
    }
}

如果现在要新增一辆AUDI, 那么需要改动的地方就会很多, 每一个方法都会新增一个if判断, 当车辆类型越来越多的时候, 这个AutoSystem类就很不清真了.这个就是依赖于实现而不是接口, 可进行以下的改造

class AutoSystem {
    constructor(car) {
        // 传入实例
        this.car = car
    }
    run() {
        thia.car.run()
    }
    stop() {
        this.car.stop()
    }
}

现在AutoSystem仅仅依赖于CarBase这个抽象. 而不是具体的car的实现. 从而实现了依赖的倒置

当然如果是ts的话会更加的直观

interface Icar {
    run: () => void;
    stop: () => void;
}

class BMWCar implements Icar {
    run() {
        
    }
    stop() {
        
    }
}
class BenzCar implement Icar {
    run() {
        
    }
    stop() {
        
    }
}
class AutoSystem{
    constructor(car: Icar) {
        this.car = car
    }
    runCar () {
        this.car.run()
    }
    stopCar() {
        this.car.stop()
    }
}

以上就是对SOLID的的所有理解和阐述, 努力想把这个概念讲解清楚, 有任何问题请给我留言, 我会尽力解答.

参考链接:

  1. www.oreilly.com/library/vie…
  2. baike.baidu.com/item/依赖倒置原则…