不知道怎麼提高代碼質量?來看看這幾種設計模式吧!

提高代碼質量的目的

程序猿的本職工作就是寫代碼,寫出高質量的代碼應該是我們的追求和對自己的要求,因為:

  1. 高質量的代碼往往意味着更少的BUG,更好的模塊化,是我們擴展性,復用性的基礎
  2. 高質量的代碼也意味着更好的書寫,更好的命名,有利於我們的維護

什麼代碼算好的質量

怎樣來定義代碼質量的”好”,業界有很多標準,本文認為好的代碼應該有以下特點:

  1. 代碼整潔,比如縮進之類的,現在有很多工具可以自動解決這個問題,比如eslint。
  2. 結構規整,沒有漫長的結構,函數拆分合理,不會來一個幾千行的函數,也不會有幾十個if...else。這要求寫代碼的人有一些優化的經驗,本文會介紹幾種模式來優化這些情況。
  3. 閱讀起來好理解,不會出現一堆a,b,c這種命名,而是應該盡量語義化,變量名和函數名都盡量有意義,最好是代碼即註釋,讓別人看你的代碼就知道你在幹嘛。

本文介紹的設計模式主要有策略/狀態模式外觀模式迭代器模式備忘錄模式

策略/狀態模式

策略模式基本結構

假如我們需要做一個計算器,需要支持加減乘除,為了判斷用戶具體需要進行哪個操作,我們需要4個if...else來進行判斷,如果支持更多操作,那if...else會更長,不利於閱讀,看着也不優雅。所以我們可以用策略模式優化如下:

function calculator(type, a, b) {
  const strategy = {
    add: function(a, b) {
      return a + b;
    },
    minus: function(a, b) {
      return a - b;
    },
    division: function(a, b) {
      return a / b;
    },
    times: function(a, b) {
      return a * b;
    }
  }
  
  return strategy[type](a, b);
}

// 使用時
calculator('add', 1, 1);

上述代碼我們用一個對象取代了多個if...else,我們需要的操作都對應這個對象裏面的一個屬性,這個屬性名字對應我們傳入的type,我們直接用這個屬性名字就可以獲取對應的操作。

狀態模式基本結構

狀態模式和策略模式很像,也是有一個對象存儲一些策略,但是還有一個變量來存儲當前的狀態,我們根據當前狀態來獲取具體的操作:

function stateFactor(state) {
  const stateObj = {
    status: '',
    state: {
      state1: function(){},
      state2: function(){},
    },
    run: function() {
      return this.state[this.status];
    }
  }
  
  stateObj.status = state;
  return stateObj;
}

// 使用時
stateFactor('state1').run();

if...else其實是根據不同的條件來改變代碼的行為,而策略模式和狀態模式都可以根據傳入的策略或者狀態的不同來改變行為,所有我們可以用這兩種模式來替代if...else

實例:訪問權限

這個例子的需求是我們的頁面需要根據不同的角色來渲染不同的內容,如果我們用if...else寫就是這樣:

// 有三個模塊需要显示,不同角色看到的模塊應該不同
function showPart1() {}
function showPart2() {}
function showPart3() {}

// 獲取當前用戶的角色,然後決定显示哪些部分
axios.get('xxx').then((role) => {
  if(role === 'boss'){
    showPart1();
    showPart2();
    showPart3();
  } else if(role === 'manager') {
    showPart1();
    showPart2();
  } else if(role === 'staff') {
    showPart3();
  }
});

上述代碼中我們通過API請求獲得了當前用戶的角色,然後一堆if...else去判斷應該显示哪些模塊,如果角色很多,這裏的if...else就可能很長,我們可以嘗試用狀態模式優化下:

// 先把各種角色都包裝到一個ShowController類裏面
function ShowController() {
  this.role = '';
  this.roleMap = {
    boss: function() {
      showPart1();
      showPart2();
      showPart3();
    },
    manager: function() {
      showPart1();
    	showPart2();
    },
    staff: function() {
      showPart3();
    }
  }
}

// ShowController上添加一個實例方法show,用來根據角色展示不同的內容
ShowController.prototype.show = function() {
  axios.get('xxx').then((role) => {
    this.role = role;
    this.roleMap[this.role]();
  });
}

// 使用時
new ShowController().show();

上述代碼我們通過一個狀態模式改寫了訪問權限模塊,去掉了if...else,而且不同角色的展示都封裝到了roleMap裏面,後面要增加或者減少都會方便很多。

實例:複合運動

這個例子的需求是我們現在有一個小球,我們需要控制他移動,他移動的方向可以是上下左右,還可以是左上,右下之類的複合運動。如果我們也用if...else來寫,這頭都會寫大:

// 先來四個方向的基本運動
function moveUp() {}
function moveDown() {}
function moveLeft() {}
function moveRight() {}

// 具體移動的方法,可以接收一個或兩個參數,一個就是基本操作,兩個參數就是左上,右下這類操作
function move(...args) {
  if(args.length === 1) {
    if(args[0] === 'up') {
      moveUp();
    } else if(args[0] === 'down') {
      moveDown();        
    } else if(args[0] === 'left') {
      moveLeft();        
    } else if(args[0] === 'right') {
      moveRight();        
    }
  } else {
    if(args[0] === 'left' && args[1] === 'up') {
      moveLeft();
      moveUp();
    } else if(args[0] === 'right' && args[1] === 'down') {
      moveRight();
      moveDown();
    }
    // 後面還有很多if...
  }
}

可以看到這裏if...else看得我們頭都大了,還是用策略模式來優化下吧:

// 建一個移動控制類
function MoveController() {
  this.status = [];
  this.moveHanders = {
    // 寫上每個指令對應的方法
    up: moveUp,
    dowm: moveDown,
    left: moveLeft,
    right: moveRight
  }
}

// MoveController添加一個實例方法來觸發運動
MoveController.prototype.run = function(...args) {
  this.status = args;
  this.status.forEach((move) => {
    this.moveHanders[move]();
  });
}

// 使用時
new MoveController().run('left', 'up')

上述代碼我們也是將所有的策略都封裝到了moveHanders裏面,然後通過實例方法run傳入的方法來執行具體的策略。

外觀模式

基本結構

當我們設計一個模塊時,裏面的方法可以會設計得比較細,但是暴露給外面使用的時候,不一定非得直接暴露這些細小的接口,外部使用者需要的可能是組合部分接口來實現某個功能,我們暴露的時候其實就可以將這個組織好。這就像餐廳裏面的菜單,有很多菜,用戶可以一個一個菜去點,也可以直接點一個套餐,外觀模式提供的就類似於這樣一個組織好的套餐:

function model1() {}

function model2() {}

// 可以提供一個更高階的接口,組合好了model1和model2給外部使用
function use() {
  model2(model1());
}

實例:常見的接口封裝

外觀模式說起來其實非常常見,很多模塊內部都很複雜,但是對外的接口可能都是一兩個,我們無需知道複雜的內部細節,只需要調用統一的高級接口就行,比如下面的選項卡模塊:

// 一個選項卡類,他內部可能有多個子模塊
function Tab() {}

Tab.prototype.renderHTML = function() {}    // 渲染頁面的子模塊
Tab.prototype.bindEvent = function() {}    // 綁定事件的子模塊
Tab.prototype.loadCss = function() {}    // 加載樣式的子模塊

// 對外不需要暴露上面那些具體的子模塊,只需要一個高級接口就行
Tab.prototype.init = function(config) {
  this.loadCss();
  this.renderHTML();
  this.bindEvent();
}

上述代碼這種封裝模式非常常見,其實也是用到了外觀模式,他當然也可以暴露具體的renderHTMLbindEventloadCss這些子模塊,但是外部使用者可能並不關心這些細節,只需要給一個統一的高級接口就行,就相當於改變了外觀暴露出來,所以叫外觀模式

實例:方法的封裝

這個例子也很常見,就是把一些類似的功能封裝成一個方法,而不是每個地方去寫一遍。在以前還是IE主導天下的時候,我們需要做很多兼容的工作,僅僅是一個綁定事件就有addEventListenerattachEvent,onclick等,為了避免每次都進行這些檢測,我們可以將他們封裝成一個方法:

function addEvent(dom, type, fn) {
  if(dom.addEventListener) {
    return dom.addEventListener(type, fn, false);
  } else if(dom.attachEvent) {
    return dom.attachEvent("on" + type, fn);
  } else {
    dom["on" + type] = fn;
  }
}

然後將addEvent暴露出去給外面使用,其實我們在實際編碼時經常這樣封裝方法,只是我們自己可能沒意識到這個是外觀模式。

迭代器模式

基本結構

迭代器模式模式在JS裏面很常見了,數組自帶的forEach就是迭代器模式的一個應用,我們也可以實現一個類似的功能:

function Iterator(items) {
  this.items = items;
}

Iterator.prototype.dealEach = function(fn) {
  for(let i = 0; i < this.items.length; i++) {
    fn(this.items[i], i);
  }
}

上述代碼我們新建了一個迭代器類,構造函數接收一個數組,實例方法dealEach可以接收一個回調,對實例上的items每一項都執行這個回調。

實例:數據迭代器

其實JS數組很多原生方法都用了迭代器模式,比如findfind接收一個測試函數,返回符合這個測試函數的第一個數據。這個例子要做的是擴展這個功能,返回所有符合這個測試函數的數據項,而且也可以接收兩個參數,第一個參數是屬性名,第二個參數是值,同樣返回所有該屬性與值匹配的項:

// 外層用一個工廠模式封裝下,調用時不用寫new
function iteratorFactory(data) {
  function Iterator(data) {
    this.data = data;
  }
  
  Iterator.prototype.findAll = function(handler, value) {
    const result = [];
    let handlerFn;
    // 處理參數,如果第一個參數是函數,直接拿來用
    // 如果不是函數,就是屬性名,給一個對比的默認函數
    if(typeof handler === 'function') {
      handlerFn = handler;
    } else {
      handlerFn = function(item) {
        if(item[handler] === value) {
          return true;
        }
        
        return false;
      }
    }
    
    // 循環數據裏面的每一項,將符合結果的塞入結果數組
    for(let i = 0; i < this.data.length; i++) {
      const item = this.data[i];
      const res = handlerFn(item);
      if(res) {
        result.push(item)
      }
    }
    
    return result;
  }
  
  return new Iterator(data);
}

// 寫個數據測試下
const data = [{num: 1}, {num: 2}, {num: 3}];
iteratorFactory(data).findAll('num', 2);    // [{num: 2}]
iteratorFactory(data).findAll(item => item.num >= 2); // [{num: 2}, {num: 3}]

上述代碼封裝了一個類似數組find的迭代器,擴展了他的功能,這種迭代器非常適合用來處理API返回的大量結構相似的數據。

備忘錄模式

基本結構

備忘錄模式類似於JS經常使用的緩存函數,內部記錄一個狀態,也就是緩存,當我們再次訪問的時候可以直接拿緩存數據:

function memo() {
  const cache = {};
  
  return function(arg) {
    if(cache[arg]) {
      return cache[arg];
    } else {
      // 沒緩存的時候先執行方法,得到結果res
      // 然後將res寫入緩存
      cache[arg] = res;
      return res;
    }
}

實例:文章緩存

這個例子在實際項目中也比較常見,用戶每次點進一個新文章都需要從API請求數據,如果他下次再點進同一篇文章,我們可能希望直接用上次請求的數據,而不再次請求,這時候就可以用到我們的備忘錄模式了,直接拿上面的結構來用就行了:

function pageCache(pageId) {
  const cache = {};
  
  return function(pageId) {
    // 為了保持返回類型一致,我們都返回一個Promise
    if(cache[pageId]) {
      return Promise.resolve(cache[pageId]);
    } else {
      return axios.get(pageId).then((data) => {
        cache[pageId] = data;
        return data;
      })
    }
  }
}

上述代碼用了備忘錄模式來解決這個問題,但是代碼比較簡單,實際項目中可能需求會更加複雜一些,但是這個思路還是可以參考的。

實例:前進後退功能

這個例子的需求是,我們需要做一個可以移動的DIV,用戶把這個DIV隨意移動,但是他有時候可能誤操作或者反悔了,想把這個DIV移動回去,也就是將狀態回退到上一次,有了回退狀態的需求,當然還有配對的前進狀態的需求。這種類似的需求我們就可以用備忘錄模式實現:

function moveDiv() {
  this.states = [];       // 一個數組記錄所有狀態
  this.currentState = 0;  // 一個變量記錄當前狀態位置
}

// 移動方法,每次移動記錄狀態
moveDiv.prototype.move = function(type, num) {
  changeDiv(type, num);       // 偽代碼,移動DIV的具體操作,這裏並未實現
  
  // 記錄本次操作到states裏面去
  this.states.push({type,num});
  this.currentState = this.states.length - 1;   // 改變當前狀態指針
}

// 前進方法,取出狀態執行
moveDiv.prototype.forward = function() {
  // 如果當前不是最後一個狀態
  if(this.currentState < this.states.length - 1) {
    // 取出前進的狀態
    this.currentState++;
    const state = this.states[this.currentState];
    
    // 執行該狀態位置
    changeDiv(state.type, state.num);
  }
}

// 後退方法是類似的
moveDiv.prototype.back = function() {
  // 如果當前不是第一個狀態
  if(this.currentState > 0) {
    // 取出後退的狀態
    this.currentState--;
    const state = this.states[this.currentState];
    
    // 執行該狀態位置
    changeDiv(state.type, state.num);
  }
}

上述代碼通過一個數組將用戶所有操作過的狀態都記錄下來了,用戶可以隨時在狀態間進行前進和後退。

總結

本文講的這幾種設計模式策略/狀態模式外觀模式迭代器模式備忘錄模式都很好理解,而且在實際工作中也非常常見,熟練使用他們可以有效減少冗餘代碼,提高我們的代碼質量。

  1. 策略模式通過將我們的if條件改寫為一條條的策略減少了if...else的數量,看起來更清爽,擴展起來也更方便。狀態模式策略模式很像,只是還多了一個狀態,可以根據這個狀態來選取具體的策略。
  2. 外觀模式可能我們已經在無意間使用了,就是將模塊一些內部邏輯封裝在一個更高級的接口內部,或者將一些類似操作封裝在一個方法內部,從而讓外部調用更加方便。
  3. 迭代器模式在JS數組上有很多實現,我們也可以模仿他們做一下數據處理的工作,特別適合處理從API拿來的大量結構相似的數據。
  4. 備忘錄模式就是加一個緩存對象,用來記錄之前獲取過的數據或者操作的狀態,後面可以用來加快訪問速度或者進行狀態回滾。
  5. 還是那句話,設計模式的重點在於理解思想,實現方式可以多種多樣。

本文是講設計模式的最後一篇文章,前面三篇是:

(500+贊!)不知道怎麼封裝代碼?看看這幾種設計模式吧!

(100+贊!)框架源碼中用來提高擴展性的設計模式

不知道怎麼提高代碼復用性?看看這幾種設計模式吧

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續創作的動力。

本文素材來自於網易高級前端開發工程師微專業唐磊老師的設計模式課程。

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

作者掘金文章匯總:https://juejin.im/post/5e3ffc85518825494e2772fd

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

您可能也會喜歡…