设计模式 & 使用场景

155 阅读7分钟

总结一下在日常开发中比较常用到的设计模式,从使用场景的来体验其作用

意义

软件越来越复制,实现方式千万种。业务可以实现但是如果完全不考虑可维护性、可扩展性。那么以后的版本就真的要多痛苦就有多痛苦。但我们可以尽量不变与变的功能代码分离,确保灵活性的同时不变的部分又可以稳定,于是我们部分不变的代码进行封装,而设计模式就是为了帮助我们写出这样的封装。

策略模式

  • 策略模式可看作为if/else判断的另一种表现形式;
  • 将一个个算法|逻辑封装起来,提高代码复用率,减少代码冗余;
  • 减少了代码量以及代码维护成本。

使用场景

假设有一活动需求

  1. 当类型为“预售”时,满 100 - 10,不满 100 打 8 折
  2. 当类型为“大促”时,满 100 - 20,不满 100 打 7 折
  3. 当类型为“返场”时,满 200 - 40,不叠加
  4. 当类型为“尝鲜”时,直接打 4 折

常规写法

function answerPrice(tag, originPrice) {
  // 处理预热
  if (tag === 'pre') {
    if (originPrice >= 100) {
      return originPrice - 10;
    }
    return originPrice * 0.8;
  }

  // 处理大促
  if (tag === 'onSale') {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.7;
  }

  // 处理返场
  if (tag === 'back') {
    if (originPrice >= 200) {
      return originPrice - 40;
    }
    return originPrice;
  }

  // 处理尝鲜
  if (tag === 'fresh') {
    return originPrice * 0.4;
  }
}

违背了“单一功能”原则,万一其中一行代码出了 Bug,那么整个询价逻辑都会崩坏;与此同时出了 Bug 你很难定位到底是哪个代码块坏了事;再比如说单个能力很难被抽离复用

违背了“开放封闭”原则,假设产品要要他加一个满 100 - 50 的“新人价”怎么办?他只能继续 if-else。

策略模式写法

function prePrice(originPrice) {
  if (originPrice >= 100) {
    return originPrice - 10;
  }
  return originPrice * 0.8;
}

// 处理大促价
function onSalePrice(originPrice) {
  if (originPrice >= 100) {
    return originPrice - 20;
  }
  return originPrice * 0.7;
}

// 处理返场价
function backPrice(originPrice) {
  if (originPrice >= 200) {
    return originPrice - 40;
  }
  return originPrice;
}

// 处理尝鲜价
function freshPrice(originPrice) {
  return originPrice * 0.4;
}

function answerPrice(tag, originPrice) {
  // 处理预热价
  if (tag === 'pre') {
    return prePrice(originPrice);
  }
  // 处理大促价
  if (tag === 'onSale') {
    return onSalePrice(originPrice);
  }

  // 处理返场价
  if (tag === 'back') {
    return backPrice(originPrice);
  }

  // 处理尝鲜价
  if (tag === 'fresh') {
    return freshPrice(originPrice);
  }

}
// 定义一个询价处理器对象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 10;
    }
    return originPrice * 0.8;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.7;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 40;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.4;
  },
};



// 询价函数
function askPrice1(tag, originPrice) {
  return priceProcessor[tag](originPrice);
}

// 加入要添加一个新人价
priceProcessor.newUser = function(originPrice) {
  if (originPrice >= 100) {
    return originPrice - 50;
  }
  return originPrice;
};
  • 获得更好的扩展性
  • 减少if/else 或 Switch的使用

单例模式

使用场景

实现一个全局的模态框

常规写法

一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象

class Bigfish {
  say() {
    console.log('hi! Bigfish');
  }
}

const c1 = new Bigfish();
const c2 = new Bigfish();

这里先 new 了一个 c1,又 new 了一个 c2,很明显 c1 和 c2 之间没有任何瓜葛,两者是相互独立的对象,各占一块内存空间。而模态框全局只能出现一个,常规写法无论是性能还是对于两个实例的维护都不好。那么我们可以使用单例模式只维护一个实例,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。

单例模式写法

class Bigfish {
    say() {
        console.log('hi! Bigfish')
    }
    static getInstance() {
        // 判断是否已经new过1个实例
        if (!Bigfish.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            Bigfish.instance = new Bigfish()
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return Bigfish.instance
    }
}

const c1 = Bigfish.getInstance() // init
const c2 = Bigfish.getInstance()

这里 c1 === c2,于是模态框的完整实现是

// 核心逻辑,这里采用了闭包思路来实现单例模式
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';
  }
});

装饰器模式

在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求。

(高阶函数)

应用:react中=>高阶组件HOC,redux connect、类组件装饰器

使用场景

临时需求,修改样式,产品要求打开模态框的时候上报埋点

// 将展示Modal的逻辑单独封装
function openModal() {
  const modal = new Modal();
  modal.style.display = 'block';
}

// 埋点逻辑
function fetchPoint() {
  // xxxxx

}

// 新版本功能逻辑整合
function changeButtonStatus() {
  openModal();
  fetchPoint();
}

如此一来,我们就实现了“只添加,不修改”的装饰器模式。相当于给手机套上一个手机壳。

适配器模式

适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。生活中比如耳机接口,充电宝的兼容

使用场景

ajax请求

常规写法

axios.get(url地址, 入参).then((data) => {...})
// 发送get请求
Ajax('get', url地址, post入参, function (data) {
    // 成功的回调逻辑
}, function (error) {
    // 失败的回调逻辑
})

适配器写法

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase()
    let result
    try {
        // 实际的请求全部由新接口发起
        if (type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if (type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch (error) {
        // 捕捉网络错误
        if (failed) {
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed)
}

代理模式

在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式

使用场景

事件委托/代理

<body>
  <div id="father">
    <a href="#">1</a>
    <a href="#">2</a>
    <a href="#">3</a>
    <a href="#">4</a>
  </div>
  <script>
    // 获取父元素

    // 给父元素安装一次监听函数
    father.addEventListener('click', function (e) {
      // 识别是否是目标子元素
      if (e.target.tagName === 'A') {
        // 以下是监听函数的函数体
        e.preventDefault()
        alert(`我是${e.target.innerText}`)
      }
    })
  </script>
</body>

虚拟代理——图片预加载

为了避免网络不好、或者图片太大时,页面长时间给用户留白的尴尬。常见的操作是先让这个 img 标签展示一个占位图,然后创建一个 Image 实例,让这个 Image 实例的 src 指向真实的目标图片地址、观察该 Image 实例的加载情况 —— 当其对应的真实图片加载完毕后,即已经有了该图片的缓存内容,再将 DOM 上的 img 元素的 src 指向真实的目标图片地址。此时我们直接去取了目标图片的缓存,所以展示速度会非常快

class PreLoadImage {
  // 占位图的url地址
  static LOADING_URL = 'xxxxxx'
  constructor(imgNode) {
    // 获取该实例对应的DOM节点
    this.imgNode = imgNode;
  }

  // 该方法用于设置真实的图片地址
  setSrc(targetUrl) {
    // img节点初始化时展示的是一个占位图
    this.imgNode.src = PreLoadImage.LOADING_URL;
    // 创建一个帮我们加载图片的Image实例
    const image = new Image();
    // 监听目标图片加载的情况,完成时再将DOM上的img节点的src属性设置为目标图片的url
    image.onload = () => {
      this.imgNode.src = targetUrl;
    };
    // 设置src属性,Image实例开始加载图片
    image.src = srcUrl;
  }
}

缓存代理——斐波那契数列

一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望 能从内存里去取出现成的计算结果。

// 为求和方法创建代理
const proxyFn = function(fn) {
  // 求和结果的缓存池
  const resultCache = {};
  return function() {
    // 将入参转化为一个唯一的入参字符串
    const args = Array.prototype.join.call(arguments, ',');
    // 检查本次入参是否有对应的计算结果
    if (args in resultCache) {
      // 如果有,则返回缓存池里现成的结果
      return resultCache[args];
    }
    return resultCache[args] = fn(...arguments);
  };
};

保护代理

所谓“保护代理”,就是在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。

const protector = new Proxy(obj, {
  get: function(obj, key) {
    if (baseInfo.indexOf(key) !== -1 && !user.isValidated) {
      alert('您还没有完成验证哦');
      return;
    }
    // ...(此处省略其它有的没的各种校验逻辑)
    // 此处我们认为只有验证过的用户才可以购买VIP
    if (user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
      alert('只有VIP才可以查看该信息哦');
      return;
    }
  },
  set: function(obj, key, val) {
    // 赋值拦截
    if (key === 'msg') {
      if (val.value === '') {
        alert('sorry,信息为空');
        return;
      }
      // 如果没有拒收,则赋值成功
      obj[msg] = val;
    }
  },
});