浅谈设计模式
1.设计模式是什么
先看看必应搜索出来的词条:
我个人认为设计模式就是前人在编程中经过不断的试错和踩坑所总结出来的方法和套路
2.为什么要学设计模式
先看看必应搜索出来的词条:
我的理由:
- 为了写出健壮性代码
- 面试会考,而且常考
一个软件工程师驾驭技术的能力总共分为三个层次:
- 能用健壮的代码去解决具体的问题
- 能用抽象的思维去应对复杂的系统
- 能用工程化的思想去规划更大规模的业务
第一点是面对所有程序员,后两点更多的是面向团队中的负责架构的人员
3.SOLID设计原则
单一功能原则(Single Responsibility Principle)
开放封闭原则(Opened Closed Principle)
- 里式替换原则(Liskov Substitution Principle)
- 接口隔离原则(Interface Segregation Principle)
- 依赖反转原则(Dependency Inversion Principle)
4.设计模式的核心思想—封装变化
如果一个软件不需要进行迭代和版本更替,那我们再开发过程中只需要完成其功能就行,完全不用去考虑其可维护性和可拓展性。
但在通常开发中我们开发了一个软件,随着时间线的拉长,它会变得越来越复杂,体量也会越来越大。而我们所需要做的就是将他的变化造成的影响最小化——将变和不变分离,确保在维持软件原有功能情况下,完成新的功能增加和产品迭代
,这个过程就叫封装变化
5.23种常见设计模式
这里我们并不需要把所有的设计模式都学透,从强语言迁移过来的设计模式并不是全部都是用于现在的前端,暂时只需要把几个重点的弄明白就行
创建型
一、工厂模式(Factory Pattern)
工厂模式其实就是将创建对象的过程单独封装
简单工厂
有构造函数的地方,我们就应该想到简单工厂
1.解决的问题:
将实例化的操作
和使用对象的操作
分开,让使用者不用知道具体的参数就可以实例化出所需要的产品
类,避免了在客户端代码中显式指定,实现了解耦(使用者可以直接消费产品而不需要知道其生产细节)
2.构造器
通常创建对象:
const obj = {name:'发财',age:20}
如果要创建500个对象我们再这么做就显得很笨了
这时候可以通过定义构造函数给其传入某些属性让它帮我们进行创建
function Constructor(name,age){
this.name = name
this.age = age
}
let man = new Constructor('发财', 20)
let man2 = new Constructor('旺财', 22)
console.log(man)// Constructor{ name: '发财', age: 20 }
console.log(man2)// Constructor{ name: '旺财', age: 22 }
3.使用步骤
/**
* 1.创建工厂类
* 2.创建静态方法根据传入参数不同从而创建不同具体产品实例
* 3.外界通过调用工厂类的静态方法,传入不同参数从而创建不同具体产品类的实例
*/
//创建工厂类
class User{
// 创建构造方法
constructor(props) {
this.name = props.name
this.age = props.age
}
//创建静态方法
static getUser(name,age){
return new User({name:name,age:age})
}
}
let pre = User.getUser('发财',20)
let pre2 = User.getUser('旺财',22)
console.log(pre)// User { name: '发财', age: 20 }
console.log(pre2)// User { name: '旺财', age: 22 }
4.举例
需求:创建一个用户工厂,不同的用户他们的工作随他们的角色而不同
function User(name, age, career, work) {
this.name = name
this.age = age
this.career = career
this.work = work
}
function Factory(name, age, career) {
let work
switch (career) {
case 'coder':
work = ['写代码', '写系分', '修Bug']
break
case 'product manager':
work = ['订会议室', '写PRD', '催更']
break
case 'boss':
work = ['喝茶', '看报', '见客户']
}
return new User(name, age, career, work)
}
这个简单工厂的例子是有一点问题的:
Boss和员工在职能上是有一定差别的,所以我们在床技安不同对象的时候还需要考虑他的角色和相对应的权限。(并不是说谁都可以开除高管哈哈)
根据上诉问题,我们不能再说再在原来的函数体上做修改了,不然每次加了新的东西又要返回去直接修改函数体这样就会导致整个Factory变得很大直到难以维护——俗称:shi山
抽象工厂 (Abstract Factory Pattern)
抽象工厂模式它围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
在抽象工厂模式中,提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
举例
需求:建一个生产智能手机的工厂,这一次需要生产FakeStarFactory,用的安卓系统和高通硬件。但也要考虑下一个生产线生产的手机型号
// 定义抽象方法
class MobilePhoneFactory{
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,需要重写!")
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,需要重写!")
}
}
// 具体工厂继承抽象工厂
class FakeStarFactory extends MobilePhoneFactory{
// 创建安卓系统实例
createOS() {
return new AndroidOS()
}
// 提供高通硬件实例
createHardWare() {
return new QualcommHardWare()
}
}
// 定义操作系统这类产品的抽象产品类
class OS{
controlHardWare(){
throw new Error('抽象产品方法不难直接调用,需要重写')
}
}
// 定义具体操作系统类
class AndroidOS extends OS{
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
class AppleOS extends OS{
controlHardWare() {
console.log('我会用苹果的方式去操作硬件')
}
}
// 定义手机硬件这类产品的抽象产品类
class HardWare {
// 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder() {
console.log('我会用高通的方式去运转')
}
}
class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}
// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()
按以上的代码我下一次生产的手机只需要重新继承MobilePhoneFactory()去创建一个新的手机实例就行
二、单例模式(Singleton Pattern)
保证一个类仅有一个实例,并提供一个访问它的全局访问点
举例
静态方法实现:
class SingleDog {
show() {
console.log('我是一个单例对象')
}
static getInstance() {
// 判断是否已经new过1个实例
if (!SingleDog.instance) {
// 若这个唯一的实例不存在,那么先创建它
SingleDog.instance = new SingleDog()
}
// 如果这个唯一的实例已经存在,则直接返回
return SingleDog.instance
}
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
// true s1,s2都纸箱了getInstance方法
s1 === s2
闭包实现:
SingleDog.getInstance = (function() {
// 定义自由变量instance,模拟私有变量
let instance = null
return function() {
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new SingleDog()
}
return instance
}
})()
Vuex:单例模式的典型应用
1. 概念
在写vue的过程中当组件多了,嵌套关系多起来了的时候,在通过Props和Emit这种传统的方法传值就会使项目变得的难以维护,很复杂。
Vuex做的就是将共享的数据抽离了出来放在了全局,需要的时候去存取,具体可以参考我之前发布的这篇笔记:深入浅出Vuex。
需要注意的是:每个Vue实例里面只能对应一个Store
2. 确保Store的唯一性
通常我们会通过Vue.use()方法去安装Vuex插件。而为了确保Store的唯一性,避免多次调用,重复安装…它内部实现了一个install方法:
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)
}
举例2
需求:实现一个全局的Modal弹框
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单例模式弹框</title>
</head>
<style>
#modal {
height: 200px;
width: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
text-align: center;
}
</style>
<body>
<button id='open'>打开弹框</button>
<button id='close'>关闭弹框</button>
</body>
<script>
// 核心逻辑,这里采用了闭包思路来实现单例模式
const Modal = (function() {
let modal = null
return function() {
if(!modal) {
modal = document.createElement('div')
modal.innerHTML = '我是一个全局唯一的Modal'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
// 点击打开按钮展示模态框
document.getElementById('open').addEventListener('click', function() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
const modal = new Modal()
modal.style.display = 'block'
})
// 点击关闭按钮隐藏模态框
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal()
if(modal) {
modal.style.display = 'none'
}
})
</script>
</html>
三、原型模式(Prototype Pattern)
原型模式不仅是一种设计模式,它还是一种编程范式(programming paradigm),是 JavaScript 面向对象系统实现的根基。在用于创建重复的对象的同时又能保证性能。
在js中只要你还用prototype来创建对象和继承原型,那就算再应用原型模式
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
在 Java 等强类型语言中,原型模式的出现是为了实现类型之间的解耦。而 JavaScript 本身类型就比较模糊,不存在类型耦合的问题,所以说咱们平时根本不会刻意地去使用原型模式
原型范式
核心思想:利用实例对象来描述对象,用实例作为定义对象和继承的基础。
原型
在 JavaScript 中,每个构造函数都拥有一个prototype
属性,它指向构造函数的原型对象,这个原型对象中有一个 constructor
属性指回构造函数;
每个实例都有一个__proto__
属性,当我们使用构造函数去创建实例时,实例的__proto__
属性就会指向构造函数的原型对象。
深拷贝
参考:2022前端笔试、面试题
结构型
一、装饰器模式(Decorator Pattern)
装饰器模式:在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求。
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
举例
需求:点击打开按钮弹出弹窗,在弹框被打开后把按钮的文案改为“快去登录”,同时把按钮置灰。点击关闭按钮关闭弹窗。需要考虑后续需求变更
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
#modal {
height: 200px;
width: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
text-align: center;
}
</style>
<body>
<button id='open'>点击打开</button>
<button id='close'>关闭弹窗</button>
</body>
<script>
// 弹窗创建逻辑,这里我们复用了单例模式面试题的例子
const Modal = (function() {
let modal = null
return function() {
if (!modal) {
modal = document.createElement('div')
modal.innerHTML = '您还未登录哦~'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
// 定义打开按钮
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()
})
document.getElementById('close').addEventListener('click', function() {
const modal = document.getElementById('modal')
if (modal) {
modal.style.display = 'none'
}
})
</script>
</html>
常用装饰器库:core-decorators
二、适配器模式(Adapter Pattern)
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
三、代理模式(Proxy Pattern)
在代理模式中:在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的。(梯子/VPN/科学上网)
意图:其他对象提供一种代理以控制对这个对象的访问。
应用:事件代理、虚拟代理(懒加载等等)、缓存代理
应用
1. 事件代理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="a_Box">
<a href="#">我是A标签1</a>
<a href="#">我是A标签2</a>
<a href="#">我是A标签3</a>
<a href="#">我是A标签4</a>
<a href="#">我是A标签5</a>
<a href="#">我是A标签6</a>
</div>
</body>
<script>
// 获取父元素
const aBox = document.getElementById('a_Box')
aBox.addEventListener('click', function(e) {
// 识别目标是否是A标签
if(e.target.tagName === 'A') {
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
})
</script>
</html>
2. 图片预加载
原理:先用给一个占位图片,当真实图片加载完了之后再去取真实的地址替换。可以预防页面白屏、卡顿
class PreLoadImage {
constructor(imgNode) {
// 获取真实的DOM节点
this.imgNode = imgNode
}
// 操作img节点的src属性
setSrc(imgUrl) {
this.imgNode.src = imgUrl
}
}
class ProxyImage {
// 占位图的url地址
static LOADING_URL = 'xxxxxx'
constructor(targetImage) {
// 目标Image,即PreLoadImage实例
this.targetImage = targetImage
}
// 该方法主要操作虚拟Image,完成加载
setSrc(targetUrl) {
// 真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(ProxyImage.LOADING_URL)
// 创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image()
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
this.targetImage.setSrc(targetUrl)
}
// 设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl
}
}
ProxyImage 帮我们调度了预加载相关的工作,我们可以通过 ProxyImage 这个代理,实现对真实 img 节点的间接访问
3. 缓存代理
应用于一些计算量较大的场景里,当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。
// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
console.log('进行了一次新计算')
let result = 0
const len = arguments.length
for(let i = 0; i < len; i++) {
result += arguments[i]
}
return result
}
// 为求和方法创建代理
const proxyAddAll = (function(){
// 求和结果的缓存池
const resultCache = {}
return function() {
// 将入参转化为一个唯一的入参字符串
const args = Array.prototype.join.call(arguments, ',')
// 检查本次入参是否有对应的计算结果
if(args in resultCache) {
// 如果有,则返回缓存池里现成的结果
return resultCache[args]
}
return resultCache[args] = addAll(...arguments)
}
})()
举例
需求:模拟游戏礼包——>普通登录过的用户能领普通的礼包,充值超过100的Vip才能领取Vip礼包
// 创建礼包清单
const giftsList = {
generalGifts: ['黄色药丸', '10金币', '红色药丸'],
vipGifts: ['屠龙刀', '圣龙宠物', '+999强化石'],
// 充值最小金额
minPayValue: 10,
payValue: 60,
}
// 允许普通用户访问的信息
const baseInfo = ['generalGifts']
// 允许VIP用户访问的信息
const vipInfo = ['vipGifts']
// 创建玩家对象
const gamer = {
// 用户是否登录标识别
isValidated: true,
// 本次充值金额
currentPayValue:6,
isVip: false,
}
// proxy(target,handler) target:需要拦截的目标对象 handler:定制拦截行为
const honorGame = new Proxy(giftsList, {
get: function(giftsList, key) {
if (baseInfo.indexOf(key) !== -1 && !gamer.isValidated) {
console.log('请先登录,如未注册请先注册哦~')
return
}
if (gamer.isVip && vipInfo.indexOf(key) !== -1) {
console.log('只有vip才能领取喔~')
}
},
set: function(giftsList,key,val){
if (gamer.currentPayValue <giftsList.minPayValue){
console.log('单次充值金额不能少于'+giftsList.minPayValue)
return
}
// 如果充值金额满足最小充值金额
giftsList.payValue +=gamer.currentPayValue
// 判断充值金额能否满足成为vip
if (giftsList.payValue >=100){
gamer.isVip = true
}
}
})
行为型
一、策略模式(Strategy Pattern)
在策略模式中,一个类的行为或其算法可以在运行时更改。
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
举例
需求场景:
- 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
- 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
- 当价格类型为“返场价”时,满 200 - 50,不叠加
- 当价格类型为“尝鲜价”时,直接打 5 折
二、状态模式(State Pattern)
在状态模式中,不同状态触发不同行为函数,用对象映射封装映射关系
。
类的行为是基于它的状态改变的。通常在该模式中我们会创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
举例
需求场景:
- 美式咖啡态(american):只吐黑咖啡
- 普通拿铁态(latte):黑咖啡加点奶
- 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力
class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
// 初始化牛奶的存储量
this.leftMilk = '500ml';
}
stateToProcessor = {
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;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}
const mk = new CoffeeMaker();
mk.changeState('latte');
三、观察者模式(Observer Pattern)
当对象间存在一对多关系时,则使用观察者模式。比如,当一个对象被修改时,则会自动通知依赖它的对象。
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
也可以参考Vue双向绑定:前端笔试、面试题
举例
class Publisher{
constructor() {
this.observers = []
console.log('发布者被创建了')
}
add(observer){
console.log('发布者添加方法被调用了')
this.observers.push(observer)
}
remove(observer){
this.observers.forEach((item,i)=>{
if(item===observer){
this.observers.splice(i,1)
}
})
}
notify(){
console.log('通知订阅者')
this.observers.forEach((observe)=>{
observe.update(this)
})
}
}
class Observer {
constructor() {
console.log('订阅者创建了')
}
update(){
console.log('订阅者被唤醒')
}
}
const fortune = new Publisher()
const tang = new Observer()
fortune.add(tang)
fortune.notify()
console.log(fortune.observers)
fortune.remove('tang')
四、迭代器模式(Iterator Pattern)
迭代器模式是 Java 和 .Net 编程环境中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
JS中内置的比较简陋的数组迭代器:Array.prototype.forEach
es6有专门对迭代器进行实现:Js基础_ES6
PS:迭代协议
该文章是我在掘金小册中学习修言大佬的《JavaScript 设计模式核⼼原理与应⽤实践》所做的笔记以及心得,侵权即删