游离在设计模式中的开放封闭原则
:
软件实体应该是可扩展
,而不可修改
的。也就是说,对扩展是开放的,而对修改是封闭的。
因此,开放封闭原则主要体现在两个方面:
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
# 1. 单例模式
一个类仅有一个实例
// 静态属性形式
class Person {
show() {
console.log('我是单例模式')
}
static setInstance() {
if (!Person.instance) {
Person.instance = new Person()
}
return Person.instance
}
}
// 闭包模式
Person.setInstance = (function() {
let instance = null
return function() {
if (!instance) {
instance = new Person()
}
return instance
}
})()
# Vuex中的单例模式
与上面的setInstance
相似
let Vue // 这个Vue的作用和楼上的instance作用一样
...
export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}
单例模式保证每一个Vue实例只会被install一次Vuex插件,所以每个 Vue 实例只会拥有一个全局的 Store。如果没有单例模式会造成数据丢失,每次都会在当前vue实例注入新的store
# 原型模式
原型模式不仅是一种设计模式,它还是一种编程范式
,是js面向对象系统实现的根基。
# 装饰器模式
在不改变原对象
的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求。
es5实现
const Modal = (function() {
let modal = null
return function() {
if (!modal) {
modal = document.createElment('div')
modal.innerHTML = '您还未登录哦'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
document.getElementById('open').addEventListener('click', function() {
openModal()
changeButtonStatus()
})
document.getElementById('close').addEventListener('click', function() {
let modal = new Modal()
modal.style.display = 'none'
})
// 将展示Modal的逻辑单独封装
function openModal() {
const modal = new Modal()
modal.style.display = 'block'
}
// 新功能
function newFunction1 () {
const btn = document.getElementById('open')
btn.innerText = '快去登录'
}
function newFunction2 () {
const btn = document.getElementById('open')
btn.setAttribute("disabled", true)
}
// 新版本功能逻辑整合
function changeButtonStatus() {
changeButtonText()
disableButton()
}
es6实现
// 定义打开按钮
class OpenButton {
onClick() {
const modal = new Modal()
modal.style.display = 'block'
}
}
// 定义按钮对应的装饰器
class Decorator {
constructor(open_button) {
this.open_button = open_button
}
}
onClick() {
this.open_button.onClick()
// "包装"了一层新逻辑
this.changeButtonStatus()
}
changeButtonStatus() {
this.changeButtonText()
this.disableButton()
}
disableButton() {
const btn = document.getElementById('open')
btn.setAttribute("disabled", true)
}
changeButtonText() {
const btn = document.getElementById('open')
btn.innerText = '快去登录'
}
}
const openButton = new OpenButton()
const decorator = new Decorator(openButton)
document.getElementById('open').addEventListener('click', function() {
// openButton.onClick()
// 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效
decorator.onClick()
})
es7实现
# 策略模式
将臃肿的if-else模式进行业务拆分,提取逻辑,分发优化 比如将下面逻辑
逻辑分析:现有sm,mid,max三种获取n值方式,通过不同的tag获取不同的值
- 0 - 5 之间返回原值n
- 5 - 10 之间返回原值n - 1
- 10 - 100 之间返回原值 n - 2
function transfer(tag, n) {
if (tag === 'min') {
if (n > 0 & n <= 5) {
return n
}
}
if (tag === 'mid') {
if (n > 5 & n <= 10) {
return n - 1
}
}
if (tag === 'max') {
if (n > 10 & n <= 100) {
return n - 2
}
}
}
将内部各个函数体进行提取,用map管理逻辑体
const numberProcessor = {
sm(n) {
if (n > 0 & n <= 5) {
return n
}
},
mid(n) {
if (n > 5 & n <= 10) {
return n - 1
}
}
max(n) {
if (n > 10 & n <= 100) {
return n - 1
}
}
}
const numberDone = (tag, n) => {
return numberProcessor[tag](n)
}
# 状态模式
状态模式和策略模式很像,解决的问题没啥本质的差别,它们都封装行为、都通过委托来实现行为分发
但策略模式
中的行为函数是”潇洒“的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。
而状态模式
中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。即**允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
**
逻辑分析:四种咖啡的咖啡机体内,蕴含着四种状态:
- 美式咖啡态(american):只吐黑咖啡
- 普通拿铁态(latte):黑咖啡加点奶
- 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力
class CoffeeMaker {
constructor() {
this.state = 'init' // 初始化咖啡机状态
}
changeState(state) {
this.state = state
if (state === 'american') {
console.log('制作黑咖啡')
} else if (state === 'latte') {
console.log('黑咖啡加点奶')
} else if (state === 'vanillaLatte') {
console.log('黑咖啡加点奶再加点香草糖浆')
} else if (state === 'mocha') {
console.log('黑咖啡加点奶再加点巧克力')
}
}
}
const mk = new CoffeeMaker();
mk.changeState('latte'); // 输出 '黑咖啡加点奶'
通过状态模式实现,状态模式需要对函数主体状态有感知(需要随时都可以拿到函数主体中的各个属性)
class CoffeeMaker {
constructor() {
this.state = 'init' // 初始化咖啡模式
this.leftMilk = '500ml' // 初始化牛奶存储量
}
stateProcceror = {
that: this, // 拿到主体,后续可以任何时候获取主体的属性
american() {
// 尝试在行为函数里拿到咖啡机实例的信息并输出
console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
console.log('制作黑咖啡')
},
latte() {
this.american()
console.log('加点奶')
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆')
},
mocha() {
this.latte();
console.log('再加巧克力')
}
}
changeState(state) {
this.state = state
const fn = this.stateProcceror[state]
if (!fn) return
fn()
}
}
const mk = new CoffeeMaker()
mk.changeState('latte')
# 观察者模式 - 发布订阅模式
观察者模式
定义了一种一对多
的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。生活场景中比如公司群里领导发消息,领导(发布者)一发消息,所有人需要关注领导发言的(订阅者)都能收到推送信息,这就是观察者模式。
逻辑分析:
- 发布者:铲平经理拉群(增加订阅者),然后是@所有人(通知订阅者)。此外作为群主&产品经理,还具有踢走项目组成员(移除订阅者)的能力
- 订阅者:监听发布者的消息更新通知
// 定义发布者
class Publisher {
constructor() {
this.observers = []
console.log('publisher created')
}
add(observer) {
console.log('publisher add invoked')
this.observers.push(observer)
}
remove(observer) {
console.log('publisher remove invoked')
this.observers = this.observers.filter(item => item !== observer)
}
notify() {
this.observers.forEach(observer => {
observer.update(this)
})
}
}
// 定义订阅者 - 被通知、 去执行
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer update invoked')
}
}
以上,我们就完成了最基本的发布者和订阅者类的设计和编写。在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。比如我们可以通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化
// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
constructor() {
super()
// 初始化需求文档
this.prdState = null
// 产品经理还没有拉群,开发群目前为空
this.observers = []
console.log('PrdPublisher created')
}
// 获取当前的prdState
getState() {
console.log('PrdPublisher getState invoked')
return this.prdState
}
// 改变prdState
setState(state) {
console.log('PrdPublisher setState invoked')
this.prdState = state
// 需求变更立即通知所有订阅者
this.notify()
}
}
// 作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活
class DeveloperObserver extends Observer {
constructor() {
super()
// 需求文档一开始还不在,prd为空
this.prdState = {}
console.log('DeveloperObserver created')
}
update(publisher) {
console.log('DeveloperObserver update invoked')
// 更新需求文档
this.prdState = publisher.getState()
// 调用工作函数
this.word()
}
// 工作函数
work() {
// 获取需求文档
const prd = this.prdState
console.log('996 begins...o(╥﹏╥)o')
}
}
下面我们来看看韩梅梅和她的小伙伴们是如何搞事情的吧:
// 创建订阅者:前端开发李雷
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 韩梅梅出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
// 具体的需求内容
...
}
// 韩梅梅开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 韩梅梅发送了需求文档,并@了所有人
hanMeiMei.setState(prd)
# 命令模式
接收者、命令者、发布者
class Receiver { // 接收者类
execute() {
console.log('接收者执行请求)
// ... 一些请求
}
}
class Command { // 命令对象
constructor(receiver) {
this.receiver = receiver
}
execute() { // 调用接收者对应接口执行
console.log('命令对象-》接收者-》对应接口执行')
this.receiver.execute()
}
}
class Invoker { // 发布者
constructor(command) {
this.command = command
}
invoke() { // 发布请求,调用命令对象
console.log('发布者发布请求')
this.command.execute()
}
}
const warehouse = new Receiver() // 厨师
const order = new Command(warehouse) // 订单
const client = new Invoker(order) // 请求者
client.invoke()
# 例子 - 菜单
一个界面,包含很多个按钮,每个按钮有不同的功能,我们利用命令模式来完成
<button id="button1"></button>
<button id="button2"></button>
<button id="button3"></button>
<script>
var button1 = document.getElementById("button1");
var button2 = document.getElementById("button2");
var button3 = document.getElementById("button3");
</script>
然后定义一个setCommand函数,负责将按钮安装命令,可以确定的是,点击按钮会执行某个 command 命令,执行命令的动作被约定为调用 command 对象的 execute() 方法。如下:
var button1 = document.getElementById('button1')
var setCommand = function(button, conmmand) {
button.onclick = function() {
conmmand.execute()
}
}
点击按钮之后具体行为包括刷新菜单界面、增加子菜单和删除子菜单等,这几个功能被分布在 MenuBar 和 SubMenu 这两个对象中:
var MenuBar = {
refresh: function() {
console.log('刷新菜单目录')
}
}
var SubMenu = {
add: function() {
console.log('增加子菜单')
},
del: function(){
console.log('删除子菜单');
}
}
这些功能需要封装在对应的命令类中:
// 刷新菜单目录命令类
class RefreshMenuBarCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
// 增加子菜单命令类
class AddSubMenuCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
// '删除子菜单命令类
class DelSubMenuCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面:
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);