设计模式为什么重要? 因为可读性? 因为少 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的的所有理解和阐述, 努力想把这个概念讲解清楚, 有任何问题请给我留言, 我会尽力解答.
参考链接: