设计模式第二弹: 不知道怎么提升代码复用性?看看这几种设计模式吧!

本文是设计模式的第二篇文章,第一篇文章是不知道怎么封装代码?看看这几种设计模式吧!,后面还会有提升扩展性提升代码质量的设计模式,点个关注不迷路,哈哈~javascript

想必你们都据说过DRY原则,其实就是Don't repeat yourself(不要重复你本身),意思就是不要重复写同样的代码,换句话说就是要提升代码的复用性。那什么样的代码才算有好的复用性呢?前端

  1. 对象能够重复利用。这个其实有点像咱们关系型数据库的设计原则,数据表和关系表是分开的,数据表就是单纯的数据,没有跟其余表的关系,也没有业务逻辑,关系表才是存储具体的对应关系。当咱们须要某个数据时,直接读这个表就行,而不用担忧这个表会有其余的业务在里面。相似设计的还有redux,redux的store里面就是单纯的数据,并不对应具体的业务逻辑,业务若是须要改变数据须要发action才行。正是由于这种数据很单纯,因此咱们须要的地方均可以拿来用,复用性很是高。因此咱们设计数据或对象时,也要尽可能让他能够复用。
  2. 重复代码少。若是你写的代码重复度很高的话,说明你代码的抽象度不够。不少时候咱们重复代码的产生都是由于咱们可能须要写一个跟已经存在的功能相似的功能,因而咱们就把以前的代码拷贝过来,把其中两行代码改了完事。这样作虽然功能实现了,可是却制造了大量重复代码,本文要讲的几种设计模式就是用来解决这个问题的,提升代码的抽象度,减小重复代码。
  3. 模块功能单一。这意味着一个模块就专一于一个功能,咱们须要作一个大功能时,就将多个模块组合起来就行。这就像乐高积木,功能单一的模块就像乐高积木的一小块,咱们能够用10个小块拼成一个小汽车,也能够用20个小块拼成一个大卡车。可是若是咱们模块自己作复杂了,作成了小汽车,咱们是不能用两个小汽车拼成一个大卡车的,这复用性就下降了。

提升复用性的设计模式主要有桥接模式享元模式模板方法模式,下面咱们分别来看下。java

桥接模式

桥接模式人如其名,其实就至关于一个桥梁,把不一样维度的变量桥接在一块儿来实现功能。假设咱们须要实现三种形状(长方形,圆形,三角形),每种形状有三种颜色(红色,绿色,蓝色),这个需求有两个方案,一个方案写九个方法,每一个方法实现一个图形:jquery

function redRectangle() {}
function greenRectangle() {}
function blueRectangle() {}
function redCircle() {}
function greenCircle() {}
function blueCircle() {}
function redTriangle() {}
function greenTriangle() {}
function blueTriangle() {}

上述代码虽然功能实现了,可是若是咱们需求变了,咱们要求再加一个颜色,那咱们就得再加三个方法,每一个形状加一个。这么多方法看着就很重复,意味着他有优化的空间。咱们仔细看下这个需求,咱们最终要画的图形有颜色和形状两个变量,这两个变量实际上是没有强的逻辑关系的,彻底是两个维度的变量。那咱们能够将这两个变量拆开,最终要画图形的时候再桥接起来,就是这样:git

function rectangle(color) {     // 长方形
  showColor(color);
}

function circle(color) {     // 圆形
  showColor(color);
}

function triangle(color) {   // 三角形
  showColor(color);
}

function showColor(color) {   // 显示颜色的方法
  
}

// 使用时,须要一个红色的圆形
let obj = new circle('red');

使用桥接模式后咱们的方法从3 * 3变成了3 + 1,并且若是后续颜色增长了,咱们只须要稍微修改showColor方法,让他支持新颜色就好了。若是咱们变量的维度不是2,而是3,这种优点会更加明显,前一种须要的方法是x * y * z个,桥接模式优化后是x + y + z个,这直接就是指数级的优化。因此这里桥接模式优化的核心思想是观察重复代码能不能拆成多个维度,若是能够的话就把不一样维度拆出来,使用时再将这些维度桥接起来。github

实例:毛笔和蜡笔

桥接模式其实我最喜欢的例子就是毛笔和蜡笔,由于这个例子很是直观,好理解。这个例子的需求是要画,三种型号的线,每种型号的线须要5种颜色,若是咱们用蜡笔来画就须要15支蜡笔,若是咱们换毛笔来画,只须要3支毛笔就好了,每次用不一样颜色的墨水,用完换墨水就行。写成代码就是这样,跟上面那个有点像:ajax

// 先来三个笔的类
function smallPen(color) {
  this.color = color;
}
smallPen.prototype.draw = function() {
  drawWithColor(this.color);    // 用color颜色来画画
}

function middlePen(color) {
  this.color = color;
}
middlePen.prototype.draw = function() {
  drawWithColor(this.color);    // 用color颜色来画画
}

function bigPen(color) {
  this.color = color;
}
bigPen.prototype.draw = function() {
  drawWithColor(this.color);    // 用color颜色来画画
}

// 再来一个颜色类
function color(color) {
  this.color = color;
}

// 使用时
new middlePen(new color('red')).draw();    // 画一个中号的红线
new bigPen(new color('green')).draw();     // 画一个大号的绿线

上述例子中蜡笔由于大小和颜色都是他自己的属性,无法分开,须要的蜡笔数量是两个维度的乘积,也就是15支,若是再多一个维度,那复杂度是指数级增加的。可是毛笔的大小和颜色这两个维度是分开的,使用时将他们桥接在一块儿就行,只须要三只毛笔,5瓶墨水,复杂度大大下降了。上面代码的颜色我新建了一个类,而上个例子画图形那里的颜色是直接做为参数传递的,这样作的目的是为了演示即便同一个设计模式也能够有不一样的实现方案。具体采用哪一种方案要根据咱们实际的需求来,若是要桥接的只是颜色这么一个简单变量,彻底能够做为参数传递,若是要桥接一个复杂对象,可能就须要一个类了。另外上述代码的三个笔的类看着就很重复,其实进一步优化还能够提取一个模板,也就是笔的基类,具体能够看看后面的模板方法模式。算法

实例:菜单项

这个例子的需求是:有多个菜单项,每一个菜单项文字不同,鼠标滑入滑出时文字的颜色也不同。咱们通常实现时可能这么写代码:数据库

function menuItem(word) {
  this.dom = document.createElement('div');
  this.dom.innerHTML = word;
}

var menu1 = new menuItem('menu1');
var menu2 = new menuItem('menu2');
var menu3 = new menuItem('menu3');

// 给每一个menu设置鼠标滑入滑出事件
menu1.dom.onmouseover = function(){
  menu1.dom.style.color = 'red';
}
menu2.dom.onmouseover = function(){
  menu1.dom.style1.color = 'green';
}
menu3.dom.onmouseover = function(){
  menu1.dom.style1.color = 'blue';
}
menu1.dom.onmouseout = function(){
  menu1.dom.style1.color = 'green';
}
menu2.dom.onmouseout = function(){
  menu1.dom.style1.color = 'blue';
}
menu3.dom.onmouseout = function(){
  menu1.dom.style1.color = 'red';
}

上述代码看起来都好多重复的,为了消除这些重复代码,咱们将事件绑定和颜色设置这两个维度分离开:redux

// 菜单项类多接收一个参数color
function menuItem(word, color) {
  this.dom = document.createElement('div');
  this.dom.innerHTML = word;
  this.color = color;        // 将接收的颜色参数做为实例属性
}

// 菜单项类添加一个实例方法,用于绑定事件
menuItem.prototype.bind = function() {
  var that = this;      // 这里的this指向menuItem实例对象
  this.dom.onmouseover = function() {
    this.style.color = that.color.colorOver;    // 注意这里的this是事件回调里面的this,指向DOM节点
  }
  this.dom.onmouseout = function() {
    this.style.color = that.color.colorOut;
  }
}

// 再建一个类存放颜色,目前这个类的比较简单,后面能够根据须要扩展
function menuColor(colorOver, colorOut) {
  this.colorOver = colorOver;
  this.colorOut = colorOut;
}

// 如今新建菜单项能够直接用一个数组来循环了
var menus = [
  {word: 'menu1', colorOver: 'red', colorOut: 'green'},
  {word: 'menu2', colorOver: 'green', colorOut: 'blue'},
  {word: 'menu3', colorOver: 'blue', colorOut: 'red'},
]

for(var i = 0; i < menus.length; i++) {
  // 将参数传进去进行实例化,最后调一下bind方法,这样就会自动绑定事件了
  new menuItem(menus[i].word, new menuColor(menus[i].colorOver, menus[i].colorOut)).bind();
}

上述代码也是同样的思路,咱们将事件绑定和颜色两个维度分别抽取出来,使用的时候再桥接,从而减小了大量类似的代码。

享元模式

当咱们观察到代码中有大量类似的代码块,他们作的事情可能都是同样的,只是每次应用的对象不同,咱们就能够考虑用享元模式。如今假设咱们有一个需求是显示多个弹窗,每一个弹窗的文字和大小不一样:

// 已经有一个弹窗类了
function Popup() {}

// 弹窗类有一个显示的方法
Popup.prototype.show = function() {}

若是咱们不用享元模式,一个一个弹就是这样:

var popup1 = new Popup();
popup1.show();

var popup2 = new Popup();
popup2.show();

咱们仔细观察上面的代码,发现这两个实例作的事情都是同样的,都是显示弹窗,可是每一个弹窗的大小文字不同,那show方法是否是就能够提出来公用,把不同的部分做为参数传进去就行。这种思路其实就是享元模式,咱们改造以下:

var popupArr = [
  {text: 'popup 1', width: 200, height: 400},
  {text: 'popup 2', width: 300, height: 300},
]

var popup = new Popup();
for(var i = 0; i < popupArr.length; i++) {
  popup.show(popupArr[i]);    // 注意show方法须要接收参数
}

实例:文件上传

咱们再来看一个例子,假如咱们如今有个需求是上传文件,可能须要上传多个文件,咱们通常写代码可能就是这样:

// 一个上传的类
function Uploader(fileType, file) {
  this.fileType = fileType;
  this.file = file;
}

Uploader.prototype.init = function() {}  // 初始化方法
Uploader.prototype.upload = function() {}  // 具体上传的方法

var file1, file2, file3;    // 多个须要上传的文件
// 每一个文件都实例化一个Uploader
new Uploader('img', file1).upload();
new Uploader('txt', file2).upload();     
new Uploader('mp3', file3).upload();

上述代码咱们须要上传三个文件因而实例化了三个Uploader,但其实这三个实例只有文件类型和文件数据不同,其余的都是同样的,咱们能够重用同样的部分,不同的部分做为参数传进去就好了,用享元模式优化以下:

// 文件数据扔到一个数组里面
var data = [
  {filetype: 'img', file: file1},
  {filetype: 'txt', file: file2},
  {filetype: 'mp3', file: file3},
];

// Uploader类改造一下, 构造函数再也不接收参数
function Uploader() {}

// 原型上的其余方法保持不变
Uploader.prototype.init = function() {}

// 文件类型和文件数据实际上是上传的时候才用,做为upload的参数
Uploader.prototype.upload = function(fileType, file) {}

// 调用时只须要一个实例,循环调用upload就行
var uploader = new Uploader();
for(var i = 0; i < data.length; i++) {
  uploader.upload(data[i].filetype, data[i].file)
}

上述代码咱们经过参数的抽取将3个实例简化为1个,提升了Uploader类的复用性。上述两个例子实际上是相似的,但他们只是享元模式的一种形式,只要是符合这种思想的均可以叫享元模式,好比jQuery里面的extend方法也用到了享元模式。

实例:jQuery的extend方法

jQuery的extend方法是你们常常用的一个方法了,他接收一个或者多个参数:

  1. 只有一个参数时,extend会将传入的参数合并到jQuery本身身上。
  2. 传入两个参数obj1和obj2时,extend会将obj2合并到obj1上。

根据上述需求,咱们很容易本身实现:

$.extend = function() {
  if(arguments.length === 1) {
    for(var item in arguments[0]) {
      this[item] = arguments[0][item]
    }
  } else if(arguments.length === 2) {
    for(var item in arguments[1]) {
      arguments[0][item] = arguments[1][item];
    }
  }
}

上述代码的this[item] = arguments[0][item]arguments[0][item] = arguments[1][item]看着就很像,咱们想一想能不能优化下他,仔细看着两行代码,他们不一样的地方是拷贝的目标和来源不同,可是拷贝的操做倒是同样的。因此咱们用享元模式优化下,将不一样的地方抽出来,保持共用的拷贝不变:

$.extend = function() {
  // 不一样的部分抽取出两个变量
  var target  = this;                  // 默认为this,即$自己
  var source = arguments[0];           // 默认为第一个变量
  
  // 若是有两个参数, 改变target和source
  if(arguments.length === 2) {       
     target = arguments[0];
  	 source = arguments[1];
  }

  // 共同的拷贝操做保持不变
  for(var item in source) {
    target[item] = source[item];
  }
}

模板方法模式

模板方法模式其实相似于继承,就是咱们先定义一个通用的模板骨架,而后后面在这个基础上继续扩展。咱们经过一个需求来看下他的基本结构,假设咱们如今须要实现一个导航组件,可是这个导航类型还比较多,有的带消息提示,有的是横着的,有的是竖着的,并且后面还可能会新增类型:

// 先建一个基础的类
function baseNav() {
}

baseNav.prototype.action = function(callback){}  //接收一个回调进行特异性处理

上述代码咱们先建了一个基础的类,里面只有最基本的属性和方法,其实就至关于一个模板,并且在具体的方法里面还能够接收回调,这样后面派生出来的类能够根据本身的需求传入回调。模板方法模式其实就是相似于面向对象的基类和派生类的关系,下面咱们再来看一个例子。

实例:弹窗

仍是以前用过的弹窗例子,咱们要作一个大小文字可能不一样的弹窗组件,只是此次咱们的弹窗还有取消和肯定两个按钮,这两个按钮在不一样场景下可能有不一样的行为,好比发起请求什么的。可是他们也有一个共同的操做,就是点击这两个按钮后弹窗都会消失,这样咱们就能够把共同的部分先写出来,做为一个模板:

function basePopup(word, size) {
  this.word = word;
  this.size = size;
  this.dom = null;
}

basePopup.prototype.init = function() {
  // 初始化DOM元素
  var div = document.createElement('div');
  div.innerHTML = this.word;
  div.style.width = this.size.width;
  div.style.height = this.size.height;
  
  this.dom = div;
}

// 取消的方法
basePopup.prototype.cancel = function() {
  this.dom.style.display = 'none';
}

// 确认的方法
basePopup.prototype.confirm = function() {
  this.dom.style.display = 'none';
}

如今咱们有了一个基础的模板,那假如咱们还须要在点击取消或者确认后再进行其余操做,好比发起请求,咱们能够以这个模板为基础再加上后面须要的操做就行:

// 先继承basePopup
function ajaxPopup(word, size) {
  basePopup.call(this, word, size);
}
ajaxPopup.prototype = new basePopup();
ajaxPopup.prototype.constructor = ajaxPopup;       
// 上面是一个继承的标准写法,其实就至关于套用了模板

// 下面来加上须要的发起网络请求的操做
var cancel = ajaxPopup.prototype.cancel;    // 先缓存模板上的cancel方法
ajaxPopup.prototype.cancel = function() {
  // 先调模板的cancel
  cancel.call(this);     
  // 再加上特殊的处理,好比发起请求
  $.ajax();
}

// confirm方法是同样的处理
var confirm = ajaxPopup.prototype.confirm;
ajaxPopup.prototype.confirm = function() {
  confirm.call(this);
  $.ajax();
}

上面这个例子是经过继承实现了模板方法模式,可是这个模式并非必定要用继承的,他强调的是将一些基础部分提取出来做为模板,后面更多的操做能够在这个基础上进行扩展。

实例:算法计算器

这个例子咱们就不用继承了,他的需求是咱们如今有一系列的算法,可是这些算法在具体用的时候可能还会添加一些不一样的计算操做,须要添加的操做可能在这个算法前执行,也可能在这个算法后执行。

// 先定义一个基本的类
function counter() {
  
}

// 类上有一个计算方法
counter.prototype.count = function(num) {
  // 里面有一个算法自己的基本计算方法
  function baseCount(num) {
    // 这里的算法是什么不重要,咱们这里就加1吧
    num += 1;
    return num;
  }
}

根据需求咱们要解决的问题是在基本算法计算时可能还有其余计算操做,这些操做可能在基本计算前,也可能在基本计算以后,因此咱们要在这个计算类上留出可扩展的接口:

function counter() {
  // 添加两个队列,用于基本算法前或者后执行
  this.beforeCounting = [];
  this.afterCounting = [];
}

// 添加一个接口,接收基本算法计算前应该进行的计算
counter.prototype.before = function(fn) {
  this.beforeCounting.push(fn);       // 直接将方法放进数组里面
}

// 再添加一个接口,接收基本算法计算后应该进行的计算
counter.prototype.after = function(fn) {
  this.afterCounting.push(fn);       
}

// 改造计算方法,让他按照计算前-基本计算-计算后执行
counter.prototype.count = function(num) {
  function baseCount(num) {
    num += 1;
    return num;
  }
  
  var result = num;
  var arr = [baseCount];     // 将须要进行的计算都放到这个数组里面
  
  arr = this.beforeCounting.concat(arr);     // 计算前操做放到数组前面
  arr = arr.concat(this.afterCounting);      // 计算后操做放到数组后面
  
  // 将数组所有按顺序拿出来执行
  while(arr.length > 0) {
    result = arr.shift()(result);
  }
  
  return result;
}

// 如今counter就能够直接使用了
var counterIntance = new counter();
counterIntance.before(num => num + 10);      // 计算前先加10
counterIntance.after(num => num - 5);        // 计算后再减5

counterIntance.count(2);     // 2 + 10 + 1 - 5  = 8

此次咱们没有用继承了,可是咱们仍然是先定义了一个基本的操做骨架,而后在这个骨架上去扩展不一样地方须要的特殊操做。

总结

  1. 若是咱们的代码中出现了大量类似的代码块,每每意味着有进一步的优化空间。
  2. 若是这些重复代码块能够拆分红不一样的维度,那能够试试桥接模式,先将维度拆开,再桥接这些维度来使用。
  3. 若是这些重复代码有一部分操做是同样的,可是每次操做的对象不同,咱们能够考虑用享元模式将公有操做提取成方法,将私有部分做为参数传进去。
  4. 若是这些重复代码有一些基本操做是同样的,可是具体应用时须要的功能更多,咱们能够考虑将这些基本操做提取成模板,而后在模板上留出扩展接口,须要的地方能够经过这些接口来扩展功能,有点相似于继承,但实现方式并不只限于继承。
  5. 咱们将重复部分提取出来,其余地方也能够用,其实就是提升了代码的复用性。
  6. 仍是那句话,设计模式没有固定的范式,主要仍是要理解他的思想,代码在不一样地方能够有不一样的实现方式。

文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。

本文素材来自于网易高级前端开发工程师微专业唐磊老师的设计模式课程。

做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

做者掘金文章汇总:https://juejin.im/post/5e3ffc85518825494e2772fd

相关文章
相关标签/搜索