简单易懂的设计模式(上)

1、单例模式

1. 什么是单例模式

单例模式的定义是,保证一个类仅有一个实例,并提供一个访问它的全局访问点。javascript

有一些对象,好比线程池/全局缓存/浏览器中的 window 对象等等,咱们就只须要一个实例。html

下面将根据实际场景进行介绍。java

2. 实际场景

1. 登陆浮窗

当咱们单击登陆按钮时,页面中会出现一个登陆的浮窗,而这个登陆浮窗是惟一的,不管单击多少次登陆按钮,这个浮窗都只会被建立一次,那么这个登陆浮窗就适合用单例模式来建立。git

1.1 传统作法

传统作法在页面加载完成时,就建立好登陆浮窗,当用户点击登陆按钮时,显示登陆浮窗,实现代码以下:github

<button id="loginBtn">登陆</button>
var loginLayer = (() => {
    let div = document.createElement('div')
    div.innerHTML = '我是登陆弹窗'
    div.style.display = 'none'

    document.body.appendChild(div)

    return div
})()

document.getElementById('loginBtn').onclick = () => {
    loginLayer.style.display = 'block'
}

上述代码有如下缺点:web

  1. 在无需登陆的状况下,也会新增登陆浮窗的 DOM 节点,浪费性能。

如今优化一下,将代码改成,在用户点击登陆按钮后,才新增登陆浮窗的 DOM 节点。ajax

代码以下:算法

var createLoginLayer = () => {
    let div = document.createElement('div')
    div.innerHTML = '我是登陆弹窗'
    div.style.display = 'none'

    document.body.appendChild(div)

    return div
}

document.getElementById('loginBtn').onclick = () => {
    const loginLayer = createLoginLayer()
    loginLayer.style.display = 'block'
}

上述代码也存在缺陷,具体以下:设计模式

  1. 每次点击登陆按钮,都会建立一个登陆浮窗,频繁的建立 DOM 节点更加浪费性能。

实际上,咱们只须要建立一次登陆浮窗。浏览器

1.2 单例模式

经过单例模式,重构上述代码。

const createLoginLayer = () => {
    const div = document.createElement('div')
    div.innerHTML = '我是登陆弹窗'
    div.style.display = 'none'
    console.log(123)

    document.body.appendChild(div)
    return div
}

const createSingle = (function () {
    var instance = {}
    return function (fn) {
        if (!instance[fn.name]) {
            instance[fn.name] = fn.apply(this, arguments)
        }
        return instance[fn.name]
    }
})()

const createIframe = function () {
    const iframe = document.createElement('iframe')
    document.body.appendChild(iframe)
    iframe.style.display = 'none'
    return iframe
}

const createSingleLoginLayer = createSingle(createLoginLayer)
const createSingleIframe = createSingle(createIframe)

document.getElementById('loginBtn').onclick = () => {
    const loginLayer = createSingleLoginLayer
    const iframe = createSingleIframe
    loginLayer.style.display = 'block'
    iframe.style.display = 'block'
}

通过重构,代码作了如下优化:

  1. 将建立实例对象 createLoginLayer / createIframe 的职责和管理单例对象 createSingle 的职责分离,符合单一职责原则;
  2. 经过闭包存储实例,并进行判断,无论点击登陆按钮多少次,只建立一个登陆浮窗实例
  3. 易于扩展,当下次须要建立页面中惟一的 iframe / script 等其余标签时,能够直接复用该逻辑。

3. 总结

单例模式是一种简单但很是实用的模式,特别是惰性单例技术,在合适的时候才建立对象,而且只建立惟一的一个。更奇妙的是,建立对象和管理单例的职责被分布在两个不一样的方法中,这两个方法组合起来才具备单例模式的威力。

2、策略模式

1. 什么是策略模式

当咱们计划国庆出去游玩时,在交通方式上,咱们能够选择贵而快的飞机、价格中等但稍慢的动车、便宜但超级慢的火车,根据不一样的人,选择对应的交通方式,且能够随意更换交通方式,这就是策略模式

策略模式的定义是,定义一系列算法,把它们一个个封装起来,而且使它们能够相互替换。

2. 实际场景

1. 计算年终奖

1.1 传统作法

有一个计算员工年终奖的需求,假设,绩效为 S 的员工年终奖是 4 倍工资,绩效为 A 的员工年终奖是 3 倍工资,绩效为 B 的员工年终奖是 2 倍工资,下面咱们来计算员工的年终奖。

var calculateBonus = function(performanceLevel, salary) {
    if (performanceLevel === 'S') {
        return salary * 4;
    }
    if (performanceLevel === 'A') {
        return salary * 3;
    }
    if (performanceLevel === 'B') {
        return salary * 2;
    }
};

calculateBonus('B', 20000); // 输出:40000 
calculateBonus( 'S', 6000 ); // 输出:24000

上述代码有如下缺点:

  1. 使用 if-else 语句描述逻辑,代码庞大;
  2. 缺少弹性,若是须要修改绩效 S 的奖金系数,必须修改 calculateBonus 函数,违反了开放-封闭原则;
  3. 没法再次复用,当其余地方须要用到这套逻辑,只能再复制一份。

1.2 策略模式作法

使用策略模式改良后

const strategies = {
    S: salary => {
        return salary * 4
    },
    A: salary => {
        return salary * 3
    },
    B: salary => {
        return salary * 2
    }
}

const calculateBonus = (level, salary) => {
    return strtegies[level](salary)
}

console.log(calculateBonus('s', 20000))
console.log(calculateBonus('a', 10000))

能够看到上述代码作了如下改动:

  1. 策略类 strategies 封装了具体的算法和计算过程(每种绩效的计算规则);
  2. 环境类 calculateBonus 接受请求,把请求委托给策略类 strategies(员工的绩效和工资;
  3. 将算法的使用和算法的实现分离,代码清晰,职责分明;
  4. 消除大量的 if-else 语句。

1.3 小结

策略模式使代码可读性更高,易于拓展更多的策略算法。当绩效系数改变,或者绩效等级增长,咱们只须要为 strategies 调整或新增算法,符合开放-封闭原则。

2. 表单校验

当网页上的表单须要校验输入框/复选框等等规则时,如何去实现呢?

如今有一个注册用户的表单需求,在提交表单以前,须要验证如下规则:

  1. 用户名不能为空
  2. 密码长度不能少于 6 位
  3. 手机号码必须符合格式

2.1 传统作法

使用 if-else 语句判断表单输入是否符合对应规则,如不符合,提示错误缘由。

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
    <form id='registerForm' action="xxx" method="post">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号:<input type="text" name="phone">
        <button>提交</button>
    </form>
    <script type="text/javascript">
        let registerForm = document.getElementById('registerForm')

        registerForm.onsubmit = () => {
                if (registerForm.userName.value) {
                        alert('用户名不能为空')
                        return false
                }

                if (registerForm.password.value.length < 6) {
                        alert('密码长度不能少于6')
                        return false
                }

                if (!/(^1[3|5|8][0-9]$)/.test(registerForm.phone.value)) {
                        alert('手机号码格式不正确')
                        return false
                }
        }
        </script>
</body>
</html>

image.png

上述代码有如下缺点:

  • onsubmit 函数庞大,包含大量 if-else 语句;
  • onsubmit 缺少弹性,当有规则须要调整,或者须要新增规则时,须要改动 onsubmit 函数内部,违反开放-封闭原则;
  • 算法复用性差,只能经过复制,复用到其余表单。

2.2 策略模式作法

使用策略模式重构上述代码。

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
    
    <form action="http://xxx.com/register" id="registerForm" method="post">
         请输入用户名:
        <input type="text" name="userName" />
         请输入密码:
        <input type="text" name="password" />
         请输入手机号码:
        <input type="text" name="phoneNumber" />
        <button>
            提交
        </button>
    </form>
    <script type="text/javascript" src="index.js">
        
    </script>            
</body>  
</html>
// 表单dom
const registerForm = document.getElementById('registerForm')

// 表单规则
const rules = {
    userName: [
        {
            strategy: 'isNonEmpty',
            errorMsg: '用户名不能为空'
        },
        {
            strategy: 'minLength:10',
            errorMsg: '用户名长度不能小于10位'
        }    
    ],
    password: [
        {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
        }
    ],
    phoneNumber: [
        {
            strategy: 'isMobile',
            errorMsg: '手机号码格式不正确'
        }
    ]
}

// 策略类
var strategies = {
    isNonEmpty: function(value, errorMsg) {
        if (value === '') {
            return errorMsg;
        }
    },
     minLength: function(value, errorMsg, length) {
        console.log(length)
        if (value.length < length) {
            return errorMsg;
        }
    },
     isMobile: function(value, errorMsg) {
        if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    }
};

// 验证类
const Validator = function () {
    this.cache = []
}

// 添加验证方法
Validator.prototype.add = function ({ dom, rules}) {
    rules.forEach(rule => {
        const { strategy, errorMsg } = rule
        console.log(rule)
        const [ strategyName, strategyCondition ] = strategy.split(':')
        console.log(strategyName)
        const { value } = dom
        this.cache.push(strategies[strategyName].bind(dom, value, errorMsg, strategyCondition))
    })
}

// 开始验证
Validator.prototype.start = function () {
    let errorMsg
    this.cache.some(cacheItem => {
            const _errorMsg = cacheItem()
            if (_errorMsg) {
                    errorMsg = _errorMsg
                    return true
            } else {
                    return false
            }
    })

    return errorMsg
}

// 验证函数
const validatorFn = () => {
    const validator = new Validator()
    console.log(validator.add)

    Object.keys(rules).forEach(key => {
        console.log(2222222, rules[key])
        validator.add({
            dom: registerForm[key],
            rules: rules[key]
        })
    })

    const errorMsg = validator.start()
    return errorMsg
}


// 表单提交
registerForm.onsubmit = () => {
    const errorMsg = validatorFn()
    if (errorMsg) {
        alert(errorMsg)
        return false
    }
    return false
}

上述代码经过 strategies 定义规则算法,经过 Validator 定义验证算法,将规则和算法分离,咱们仅仅经过配置的方式就能够完成表单的校验,这些校验规则也能够复用在程序的任何地方,还能做为插件的形式,方便的被移植到其余项目中。

3. 总结

策略模式是一种经常使用且有效的设计模式,经过上述例子,能够总结出策略模式的一些优势:

  • 策略模式利用组合/委托和多态等技术和思想,能够有效的避免多重条件选择语句;
  • 策略模式提供了对开放-封闭原则的完美支持,将算法封装中独立的策略类中,使得它们易于切换/理解/扩展;
  • 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的代替方案。

3、代理模式

1. 什么是代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式的关键是,当客户不方便直接访问一个对象或者不知足须要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。

2. 模拟场景

1. 小明送花给小白

1.1 传统作法

传统作法是小明直接把花送给小白,小白接收到花,代码以下:

const Flower = function () {
    return '玫瑰🌹'
}

const xiaoming = {
    sendFlower: target => {
        const flower = new Flower()
        target.receiveFlower(flower)
    }
}

const xiaobai = {
    receiveFlower: flower => {
        console.log('收到花', flower)
    }
}

xiaoming.sendFlower(xiaobai)

1.2 代理模式

可是,小明并不认识小白,他想要经过小代,帮他打探小白的状况,在小白心情好的时候送花,这样成功率更高。代码以下:

const Flower = function () {
    return '玫瑰🌹'
}

const xiaoming = {
    sendFlower: target => {
        const flower = new Flower()
        target.receiveFlower(flower)
    }
}

const xiaodai = {
    receiveFlower: flower => {
        xiaobai.listenGoodMood().then(() => {
            xiaobai.receiveFlower(flower)
        })
    }
}

const xiaobai = {
    receiveFlower: flower => {
        console.log('收到花', flower)
    },
    listenGoodMood: fn => {
        return new Promise((reslove, reject) => {
            // 10秒后,心情变好
            reslove()
        })
    }
}

xiaoming.sendFlower(xiaodai)

以上,小明经过小代,监听到小白心情的心情变化,选择在小白心情好时送花给小白。不只如此,小代还能够作如下事情:

  1. 帮助小白过滤掉一些送花的请求,这就叫作保护代理;
  2. 帮助小明,在小白心情好时,再执行买花操做,这就叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正须要它的时候才去建立。

3. 实际场景

1. 图片预加载

图片预加载时一种常见的技术,若是直接给 img 标签节点设置 src 属性,因为图片过大或网络不佳,图片的位置每每有一段时间时空白。

1.1 传统作法

const myImage = (() => {
    const imgNode = document.createElement('img')
    document.body.appendChild(imgNode)

    return {
        setSrc: src => {
            imgNode.src = src
        }
    }
})()

myImage.setSrc('https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png')

经过开发者工具把网速设置为 5kb/s 时,会发如今很长一段时间内,图片位置是空白的。

image.png

1.2 虚拟代理

下面用虚拟代理优化该功能,把加载图片的操做交给代理函数完成,在图片加载时,先用一张loading 图占位,当图片加载成功后,再把它填充进 img 节点。

代码以下:

const myImage = (() => {
    const imgNode = document.createElement('img')
    document.body.appendChild(imgNode)

    return {
        setSrc: src => {
            imgNode.src = src
        }
    }
})()

const loadingSrc = '../../../../img/loading.gif'
const imgSrc = 'https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png'

const proxyImage = (function () {
    const img = new Image()
    img.onload = () => {
        myImage.setSrc(img.src)
    }

    return {
        setSrc: src => {
            myImage.setSrc(loadingSrc)
            img.src = src
        }
    }
})()

proxyImage.setSrc(imgSrc)

上述代码有如下优势:

  1. 经过 proxyImage 控制了对 MyImage 的访问,在 MyImage 未加载成功以前,使用 loading 图占位;
  2. 践行单一职责原则,给 img 节点设置 src 的函数 MyImage,预加载图片的函数 proxyImage,都只有一个职责;
  3. 践行开放-封闭原则,给 img 节点设置 src 和预加载图片的功能,被隔离在两个对象里,它们能够各自变化不影响对方。

2. 合并HTTP请求

假设咱们要实现一个同步文件的功能,经过复选框,当复选框选中的时候,将该复选框对应的 id 传给服务器,告诉服务器须要同步 id 对应的文件。

思考一下,会发现,若是每选中一个复选框,就请求一次接口,假设 1s 内选中了 10 个复选框,那么就要发送 10 次请求。

2.1 虚拟代理

能够经过虚拟代理来优化上述作法,新增一个代理,帮助复选框发起同步文件的请求,收集在这 1s 内的请求,1s 后再一块儿把这些文件 id 发送到服务器。

代码以下:

<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<head>
    <title></title>
</head>
<body>
  a <input type="checkbox" value="a" />
  b <input type="checkbox" value="b" />
  c <input type="checkbox" value="c" />
  d <input type="checkbox" value="d" />
    <script type="text/javascript" src="index.js">
    </script>
</body> 
</html>
const synchronousFile = cache => {
  console.log('开始同步文件,id为:'+ cache.join('/'))
}

const proxySynchronousFile = (() => {
  const cache = []

  let timer

  return id => {
    console.log(id)
    cache.push(id)

    if (timer) {
      return
    }

    timer = setTimeout(() => {
      synchronousFile(cache)
      clearTimeout(timer)
      timer = null
      cache.length = 0
    }, 2000)
  }
})()

const checkbox = document.getElementsByTagName('input')

Array.from(checkbox).forEach(i => {
  console.log(i)
  i.onclick = () => {
    if (i.checked) {
      proxySynchronousFile(i.value)
    }
  }
})

3. ajax异步请求数据

在列表须要分页时,同一页的数据理论上只须要去后台拉取一次,能够把这些拉取过的数据缓存下来,下次请求时直接使用缓存数据。

3.1 缓存代理

使用缓存代理实现上述功能,代码以下:

(async function () {
  function getArticle (currentPage, pageSize) {
    console.log('getArticle', currentPage, pageSize)
    // 模拟一个ajax请求
    return new Promise((resolve, reject) => {
      resolve({
        ok: true,
        data: {
          list: [],
          total: 10,
          params: {
            currentPage, 
            pageSize
          }
        }
      })
    })
  }
  
  const proxyGetArticle = (() => {
    const caches = []
  
    return async (currentPage, pageSize) => {
  
      const cache = Array.prototype.join.call([currentPage, pageSize],',')
  
      if (cache in caches) {
        return caches[cache]
      }
      const { data, ok } = await getArticle(currentPage, pageSize)
  
      if (ok) {
        caches[cache] = data
      }
  
      return caches[cache]
    }
  })()

  // 搜索第一页
  await proxyGetArticle(1, 10)
  
  // 搜索第二页
  await proxyGetArticle(2, 10)

  // 再次搜索第一页
  await proxyGetArticle(1, 10)
  
})()

经过缓存代理,在第二次请求第一页的数据时,直接在缓存数据中拉取,无须再次从服务器请求数据。

4. 总结

上面根据实际场景介绍了虚拟代理和缓存代理的作法。

当咱们不方便直接访问某个对象时,找一个代理方法帮咱们去访问该对象,这就是代理模式。

可经过 github源码 进行实操练习。

但愿本文能对你有所帮助,感谢阅读❤️~


欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。