設計模式第二彈: 不知道怎麼提升代碼複用性?看看這幾種設計模式吧!

本文是設計模式的第二篇文章,第一篇文章是不知道怎麼封裝代碼?看看這幾種設計模式吧!,後面還會有提升擴展性提升代碼質量的設計模式,點個關注不迷路,哈哈~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

相關文章
相關標籤/搜索