阅读 238

JavaScript 实现四种重要的设计模式

什么是设计模式?

设计模式就是代码问题的一套解决方案,或者说,解决特定问题的“最佳实践”。

“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样。你就能一次又一次地使用该方案而不必做重复劳动” - Christopher Alexander

常见的设计模式大概有23种,这里以一张图片说明。

image.png

本文先聊聊以下四种设计模式。

  • 对象 ✅
  • 组合模式(Composite Pattern) ✅
  • 适配器模式( Adapter Pattern) ✅
  • 装饰者模式( Decorator Pattern) ✅

对象

JS 面向对象的改造

特别说明:实际开发过程中尽量不要修改对象的.prototype属性,下面的写法仅仅是为了展示。

// 原始代码
function startAnimation() {
    console.log('start animation')
}
function stopAnimation() {
    console.log('stop animation')
}

// 改为面向对象的方式
var Anim = function() {
}
Anim.prototype.start = function() {
    console.log('start animation')
}
Anim.prototype.stop = function() {
    console.log('stop animation')
}
var myAnim = new Anim()
myAnim.start()
myAnim.stop()

// 变形简化
var Anim = function() {}
Anim.prototype = {
    start: function() {
        console.log('start')
    },
    stop: function() {
        console.llog('end')
    }
}

// 持续改进
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn
}
var Anim = function() {
}
Anim.method('start', function() {
    console.log('start animation')
})
Anim.method('stop', function() {
    console.log('stop animation')
})

// 继续改进,链式调用
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn
    return this
}
var Anim = function() {}
Anim.method('start', function() {
    console.log('start animation')
}).method('stop', function() {
    console.log(stop animation')
})
复制代码

JS 匿名函数

// 以下是立即执行的函数表达式
(function(){
    var foo = 1
    var bar = 2
    console.log(foo * bar)
})()

// 携带参数
(function(foo, bar){
   console.log(foo * bar)
})(1, 2)

// 闭包,访问函数内部的局部变量
var baz
(function(){
    var foo = 1
    var bar = 2
    baz = function() {
        return foo * bar
    }
})()
baz()
复制代码

组合模式(Composite Pattern)

image.png

特点:

  • 层层嵌套,父节点 - 叶子节点
  • 父节点和叶子节点有相同的接口以及不同的实现
class Container {
  constructor(id) {
    this.children = []
    this.element = document.createElement('div')
    this.element.id = id
    this.element.style.border = '1px solid black'
    this.element.style.margin = '10px'
    this.element.classList.add('container')    
  }

  add(child) {
    this.children.push(child)
    this.element.appendChild(child.getElement())
  }


  hide() {
    this.children.forEach(node => node.hide())
    this.element.style.display = 'none'
  }

  show() {
    this.children.forEach(node => node.show())
    this.element.style.display = ''
  }

  getElement() {
    return this.element
  }

}

class Text {
  constructor(text) {
    this.element = document.createElement('p')
    this.element.innerText = text
  }

  add() {}

  hide() {
    this.element.style.display = 'none'
  }

  show() {
    this.element.style.display = ''
  }

  getElement() {
    return this.element
  }
}

let header = new Container('header')
header.add(new Text('标题'))
header.add(new Text('logo'))

let main = new Container('main')
main.add(new Text('这是内容1'))
main.add(new Text('这是内容2'))

let page = new Container('page')
page.add(header)
page.add(main)
page.show()

document.body.appendChild(page.getElement())
复制代码

适配器模式(Adapter Pattern)

In software engineering, the adapter pattern is a software design pattern (also known as wrapper, an alternative naming shared with the decorator pattern) that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code. --from wiki

一句话,适配器模式可以类比于家里的插座,上面有不同类型的插孔,用来适配不同的插头。

实际的开发场景

  • 场景1

    使用nodejs做一个ORM框架,给用户暴露一套统一的数据库操作接口,底层根据数据库类型适配不同数据库。

  • 场景2

    做一个日志模块,给用户暴露一套统一的记录日志接口,底层根据类型适配是文件存储日志还是数据库存储日志。

  • 场景3

    前端开发过程中需用到获取数据和保存数据。在开发阶段,可以把数据存储和查询用 localStorage 来做;接口就绪后可以发送请求从服务器存取数据。使用适配器模式,为使用者提供统一接口。

以下为适配器模式的代码示例。

const  localStorageAdapter = {
  findAll: function(callback) {
    let cartList = JSON.parse(localStorage['cart'])
    callback(cartList)
  },
  save: function(item) {
    let cartList = JSON.parse(localStorage['cart'])
    cartList.push(item)
    localStorage['cart'] = JSON.stringify(cartList)
  }
}

const  serverAdapter = {
  findAll: function(callback) {
      fetch('https://someAPI.com/getCartList')
        .then(res => res.json())
        .then(data => callback(data))
  },
  save: function(item) {
    fetch('https://someAPI.com/addToCart', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(item) })
      .then(res => res.json())
      .then(data => callback(data))
  }
}

class ShoppingCart {
    constructor(adapter) {
        this.adapter = adapter
    }
    add(item) {
        this.adapter.save(item)
    }
    show() {
        this.adapter.findAll(list => {
            console.log(list)
        } )
    }
}

let cart = new ShoppingCart(localStorageAdapter) // 使用了 localStorage 这个插孔
// let cart = new ShoppingCart(serverAdapter)
cart.add({title: '手机'})
cart.add({title: '电脑'})
cart.show()
复制代码

装饰者模式(Decorator Pattern)

先问一个问题:如何给一个对象增加额外的功能?

方法1:直接修改对象

方法2:创建子类继承自父类,子类实例化新对象(如果太细,会导致子类对象的泛滥)

方法3:不改变原对象,在原对象基础上进行“装饰”,新增一些和核心功能无关的功能

装饰器模式即为上述方法3

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. --wiki

套用 Wiki 的描述,装饰器模式的最大特点是保留主干,增加装饰,但不影响原功能。假如用一个成语来形容,我认为最好的莫过于“锦上添花”了。

使用场景

  • 场景1:Form表单组件,用户点击提交时把用户内容提交。增加装饰:提交之前做个校验

  • 场景2:一个功能正常执行。增加装饰:在执行前记录下起始时间,在执行后记录下结束时间并计算消耗时间

  • 场景3: 用户点击按钮,执行某个功能。增加装饰:在执行前发请求到统计平台,统计用户的点击次数

  • 场景4:给一个编辑器组件增加一个输入改变时,保存数据到服务器节流的装饰

AOP 面向切面编程

Java 大名鼎鼎的 Spring 框架的核心编程思想 - AOP 面向切面编程,装饰器模式就是它比较常见的实现。

下面用 JS 来实现一个简单的 AOP 对象。

const AOP = {}
AOP.before = function (fn, before) {
    return function() {
        before.apply(this,arguments)
        fn.apply(this, arguments)
    }
}
AOP.after = function(fn, after) {
    return function () {
        fn.apply(this, arguments)
        after.apply(this, arguments)
    }
}

// 点击按钮提交数据
function submit() {
    console.log('提交数据')
}

document.querySelector('.btn').onclick = submit

// 在原有功能基础上做点装饰:点击按钮,提交数据前做个校验
function submit() {
    console.log(this)
    console.log('提交数据')
}
function check() {
    console.log(this)
    console.log('先进行校验')
}
submit = AOP.before(submit, check)
document.querySelector('.btn').onclick = submit
复制代码

ES7 Decorator 修饰符

借鉴其他语言的优势,JS 在 ES7 推出了 Decorator 修饰符,不好理解也不太实用,但如果熟悉装饰器模式的话,理解起来就容易多了:这不就是装饰器模式的语法糖嘛,示例如下。

const logWrapper = targetClass => {
    let orignRender = targetClass.prototype.render
    targetClass.prototype.render = function(){
        console.log("before render")
        orignRender.apply(this) 
        console.log("after render")
    }
    return targetClass
}


class App {
    constructor() {
      this.title = '首页'
    }
    render(){
        console.log('渲染页面:' + this.title);
    }
}

App = logWrapper(App)

new App().render()

// 使用 decorator 修饰符,修改class
const logWrapper = targetClass => {
  let orignRender = targetClass.prototype.render
  targetClass.prototype.render = function(){
    console.log("before render")
    orignRender.apply(this) 
    console.log("after render")
  }
  return targetClass
}

@logWrapper
class App {
  constructor() {
    this.title = '首页'
  }
  render(){
    console.log('渲染页面:' + this.title);
  }
}
复制代码

使用 Decorator 修饰符,修改原型属性

Tips: 运行代码需babel支持,把JavaScript模式调整到ES6/babel

function logWrapper(target, name, descriptor) {
    console.log(arguments)
    let originRender = descriptor.value
    descriptor.value = function() {
      console.log('before render')
      originRender.bind(this)()
      console.log('after render')
    }
    console.log(target)
}

class App {
  constructor() {
    this.title = '首页'
  }
  @logWrapper
  render(){
    console.log('渲染页面:' + this.title);
  }
}

new App().render()
复制代码

总结

设计模式看起来似乎有些枯燥乏味,学习起来短期可能也并不能见到效果,但当业务变得复杂、代码变得越来越冗余时,设计模式就能给你正确的指引。毕竟代码相关的“最佳实践”流传至今,必定有它存在的道理。

本文内容大多来自经典书籍 《Pro JavaScript Design Patterns》,这本书讲解了十几种 JavaScript 经典的设计模式。学习设计模式不仅能大幅提高 JavaScript 的内功修为,更是程序员进阶之路上必不可少的一座山。

文章分类
前端