如何寫出好代碼之消除代碼中的重復(fù)

軟件開(kāi)發(fā)中,有個(gè)很重要的DRY原則,即Dont Repeat Yourself,也就是不要重復(fù)自己。

重復(fù)的代碼會(huì)帶來(lái)以下問(wèn)題:

  • 開(kāi)發(fā)效率低,重復(fù)造輪子
  • 不同人開(kāi)發(fā)的同一類功能,質(zhì)量層次不齊
  • 修改問(wèn)題時(shí)可能會(huì)遺漏,修了這個(gè)地方,忘了那個(gè)地方,導(dǎo)致一個(gè)bug反復(fù)修改多次。

重復(fù)產(chǎn)生的原因是多方面,有的是工作模式導(dǎo)致的,有的是編碼導(dǎo)致的,弄清楚重復(fù)的原因,也就很容易找出消除重復(fù)的方法。

工作模式導(dǎo)致的重復(fù)

團(tuán)隊(duì)缺少溝通

同一個(gè)團(tuán)隊(duì)可能會(huì)針對(duì)相同或相似的功能進(jìn)行編碼,有時(shí)你封裝了一個(gè)功能,卻沒(méi)有在團(tuán)隊(duì)內(nèi)部大量使用,
究其原因可能是大家根本不知道有這么個(gè)功能,或者不知道怎么使用,干脆自己寫,也或者雖然有點(diǎn)相似,但是并不完全適用自己。

這類問(wèn)題核心還是溝通問(wèn)題,所以要根本解決這類重復(fù),我們必須在工作流程上做一些改進(jìn)。

  1. 基礎(chǔ)功能開(kāi)發(fā)評(píng)審

當(dāng)你要開(kāi)發(fā)一個(gè)基礎(chǔ)功能時(shí),需要發(fā)起設(shè)計(jì)評(píng)審,這樣首先起到了周知的作用,讓大家知道有這么個(gè)東西, 其次大家會(huì)針對(duì)功能提出自己的建議,以方便他人后續(xù)使用。

  1. 基礎(chǔ)功能宣貫

在開(kāi)發(fā)完某個(gè)功能之后,需要及時(shí)的進(jìn)行宣貫,讓其他同事知道可以使用了,可以在每周的團(tuán)隊(duì)周會(huì)上安排一個(gè)環(huán)節(jié), 進(jìn)行公共功能的變更宣貫。

  1. CodeReview

CodeReview中發(fā)現(xiàn)存在重復(fù)問(wèn)題,及時(shí)提出修正。

缺少基礎(chǔ)設(shè)施-公共庫(kù)

在一個(gè)項(xiàng)目中,我們可能建一個(gè)公共的文件夾,比如common,用來(lái)存放我們的基礎(chǔ)組件和庫(kù),但是一旦跨項(xiàng)目,這種方式就無(wú)效了,
如果沒(méi)有公共庫(kù),就需要進(jìn)行復(fù)制粘貼了,把一段代碼從一個(gè)項(xiàng)目復(fù)制到另一個(gè)項(xiàng)目,這樣就造成一個(gè)問(wèn)題的修改需要同時(shí)修改多個(gè)項(xiàng)目,
如果趕上項(xiàng)目時(shí)間緊張,再加上可能有懶惰思想,慢慢的多個(gè)項(xiàng)目之間的基礎(chǔ)功能就不再一致了,后續(xù)的維護(hù)更加復(fù)雜。

解決這類問(wèn)題可以參考兩個(gè)方法:

1.使用monorepo

monorepo也就是單一倉(cāng)庫(kù)管理多個(gè)項(xiàng)目,有些公司將所有代碼存儲(chǔ)在一個(gè)代碼庫(kù)中,由所有人共享, 因此monorepo可以非常大。例如,理論上谷歌擁有有史以來(lái)最大的代碼庫(kù),每天有成百上千次提交,整個(gè)代碼庫(kù)超過(guò) 80 TB。
其他已知運(yùn)營(yíng)大型單一代碼庫(kù)的公司還有微軟、Facebook 和 Twitter。

我們可以在monorepo中添加一個(gè)公共項(xiàng)目,用來(lái)存放我們的基礎(chǔ)組件和utils工具庫(kù)。

2.通過(guò)發(fā)布公共包

可以建設(shè)一個(gè)基礎(chǔ)組件庫(kù),發(fā)包到npm或者公司內(nèi)部的包管理系統(tǒng)。

monorepo和npm發(fā)包各有優(yōu)缺點(diǎn),根據(jù)自身情況進(jìn)行選擇。

知識(shí)的重復(fù)

在團(tuán)隊(duì)中不只是代碼存在重復(fù),在知識(shí)層面也會(huì)存在重復(fù),比如大家重復(fù)的進(jìn)行某個(gè)知識(shí)的學(xué)習(xí),包括技術(shù)上的和業(yè)務(wù)上的, 整體上增加了團(tuán)隊(duì)工作的重復(fù),降低了效率。

針對(duì)知識(shí)的重復(fù)我們可以定期舉辦內(nèi)部分享。

  1. 新功能上線演示

針對(duì)一些大的功能上線,可以組織內(nèi)部的上線演示,一方面增加大家對(duì)業(yè)務(wù)的了解,另一方面,減少大家后續(xù)重復(fù)學(xué)習(xí)的問(wèn)題。

2.定期分享

團(tuán)隊(duì)內(nèi)部定期舉辦業(yè)務(wù)、技術(shù)、工作方法/效率等方面的分享,減少對(duì)知識(shí)學(xué)習(xí)的重復(fù)工作。

編碼層面的重復(fù)

沒(méi)有意識(shí)到重復(fù)

有時(shí)候重復(fù)的代碼并不是那么明顯,可能只是幾行代碼,由于重復(fù)的行數(shù)較少,所以就很自然的采用復(fù)制粘貼, 沒(méi)有意識(shí)到重復(fù)的發(fā)生。

比如針對(duì)一個(gè)應(yīng)用的狀態(tài)判斷,應(yīng)用存在多種狀態(tài),比如未安裝(Uninstalled)、運(yùn)行中(Running)、已銷毀(Destroyed)等,
只有當(dāng)應(yīng)用處于未安裝或者已銷毀狀態(tài)才允許安裝,而這個(gè)條件可能在多處都使用。

// 應(yīng)用列表頁(yè)在進(jìn)行安裝操作時(shí)進(jìn)行狀態(tài)的判斷
function install(app) {
    if (['Uninstalled', 'Destroyed'].includes(app.status)) {
        
    }
}
<!--應(yīng)用詳情頁(yè)在進(jìn)行安裝時(shí)進(jìn)行狀態(tài)的判斷-->
<template>
  <button v-if="['Uninstalled', 'Destroyed'].includes(app.status)">安裝</button>
</template>

每當(dāng)安裝條件發(fā)生變更時(shí),都要四處尋找然后一個(gè)一個(gè)地方修改,針對(duì)這種也應(yīng)該進(jìn)行封裝,雖然他很小。
我們可以封裝一個(gè)判斷是否能安裝的方法,其他地方進(jìn)行引用。

//抽象一個(gè)方法,判斷是否能按照
function canInstall(app) {
    return ['Uninstalled', 'Destroyed'].includes(app.status)
}


//在需要進(jìn)行狀態(tài)判斷的地方引用封裝的函數(shù)
function install(app) {
    if (canInstall(app)) {

    }
}

后續(xù)安裝條件發(fā)生變更,只需要修改canInstall方法即可

//需求發(fā)生變化,InstallError狀態(tài)和沒(méi)有runtimeId也支持安裝
function canInstall(app) {
    return ['Uninstalled', 'Destroyed', 'InstallError'].includes(app.status)
        || !app.runtimeId
}

我們一般對(duì)大塊的重復(fù)代碼比較敏感,而對(duì)于小塊的代碼重復(fù),則一般會(huì)忽略,但是重復(fù)是部分大小的,小段代碼很可能在更多的地方使用。

缺少抽象和封裝

比如要讓你炸毀地球,不應(yīng)該直接寫一個(gè)炸毀地球的方法,而是寫一個(gè)炸毀星球的方法,將地球作為參數(shù)傳進(jìn)去。

抽象的東西要比具體的東西復(fù)用性更強(qiáng),因此要想提高復(fù)用性,就要對(duì)所做的功能進(jìn)行抽象,而不是面向具體單一的業(yè)務(wù)需求開(kāi)發(fā)。

示例1:組件的重復(fù)

比如在刪除k8s資源時(shí)需要輸入k8s資源的名稱進(jìn)行確認(rèn),輸入正確后才能進(jìn)行刪除,于是針對(duì)這個(gè)業(yè)務(wù)場(chǎng)景封裝了一個(gè)彈窗確認(rèn)組件,
在調(diào)用顯示彈窗方法showDialog時(shí),傳入了一個(gè)名為k8s資源名稱(k8sResourceName)的參數(shù)。

<!--刪除確認(rèn)彈窗 DeleteResource.vue 的實(shí)現(xiàn)-->
<template>
  <el-dialog
      title="刪除確認(rèn)"
      :visible.sync="dialogVisible"
      @confirm="deleteResource"
  >
    <div>請(qǐng)輸入<span>{{ k8sResourceName }}</span>確定刪除</div>
    <el-input v-model.trim="inputName"/>
  </el-dialog>
</template>

<script>
export default {
  data() {
    return {
      dialogVisible: false,
      k8sResourceName: '',
      inputName: ''
    };
  },
  methods: {
    showDialog(params) {
      this.k8sResourceName = params.k8sResourceName;
      this.dialogVisible = true;
      this.inputName = '';
    },
    deleteResource() {
      if (this.inputName === this.k8sResourceName) {
        this.dialogVisible = false;
        this.$emit('delete');
      }
    }
  }
};
</script>

其實(shí)這是一個(gè)很常用的功能,可能很多地方都會(huì)使用,比如在刪除應(yīng)用頁(yè)面,也可能用到這個(gè)組件進(jìn)行確認(rèn),
那么我們傳遞一個(gè)名為k8sResourceName的參數(shù)顯然是很不合適的(這里只是簡(jiǎn)化了這塊的實(shí)現(xiàn),實(shí)際可能并不是簡(jiǎn)單改一個(gè)名稱這么簡(jiǎn)單)。
顯然,并不是寫這塊代碼的同事, 沒(méi)有能力封裝一個(gè)通用的組件,而是缺少抽象的思維,導(dǎo)致寫出了面向具體單一業(yè)務(wù)的組件。

試著利用抽象思維,寫出更具復(fù)用性的代碼。

示例2:樣式的重復(fù)

比如在CSS中,經(jīng)常會(huì)針對(duì)某段文字設(shè)置字體大小、顏色等,一般情況下,同一個(gè)網(wǎng)站的字體大小、顏色是存在共性的,
比如標(biāo)題的顏色每個(gè)頁(yè)面都是一樣的,提示類型的文字顏色也是一樣的,如果沒(méi)有進(jìn)行抽象,那么我們代碼可能是這樣的。

.news-title {
    color: #409EFF;
    font-size: 16px;
}

.app-title {
    color: #409EFF;
    font-size: 16px;
}

這樣的設(shè)置會(huì)在樣式中存在大量的重復(fù),大致有兩個(gè)解決方法。

第一種就是對(duì)整個(gè)網(wǎng)站的css進(jìn)行分層,比如樣式分為全局樣式、頁(yè)面樣式和組件樣式。我們可以在全局樣式中抽取共性的css樣式。

比如抽取common.css,定義網(wǎng)站通用標(biāo)題的樣式。

.common-title {
    color: #409EFF;
    font-size: 16px;
}

另外一種就是通過(guò)抽取一些變量,在頁(yè)面樣式和組件樣式中引用這些變量。

比如element中的var.scss

/* Color
-------------------------- */
/// color|1|Brand Color|0
$--color-primary: #409EFF !default;
/// color|1|Background Color|4
$--color-white: #FFFFFF !default;
/// color|1|Background Color|4
$--color-black: #000000 !default;

/* Size
-------------------------- */
$--size-base: 14px !default;

/* z-index
-------------------------- */
$--index-normal: 1 !default;
$--index-top: 1000 !default;
$--index-popper: 2000 !default;

后續(xù)如果想修改網(wǎng)站的配色,只需要修改模板文件即可。

示例3:網(wǎng)絡(luò)請(qǐng)求的重復(fù)

網(wǎng)絡(luò)請(qǐng)求也是重復(fù)容易滋生的地方,比如在一個(gè)組件內(nèi)部請(qǐng)求文章的詳情方法getNewsDetail,看看有那幾處重復(fù)呢?


<script>
import axios from 'axios'

export default {
  methods: {
    getNewsDetail(id) {
      axios.get('/api/v1/news/detail/' + id).then(res => {
        this.detail = res
      }).catch(e => {
        if (e.code === '401') {
          showToast('未登錄,請(qǐng)先登錄')
        } else {
          showToast('接口請(qǐng)求失敗')
        }
      })
    }
  }
}
</script>

上述代碼可能存在如下重復(fù)問(wèn)題:

  • 接口地址可能存在重復(fù),別的頁(yè)面也能會(huì)使用文章詳情接口,如果接口地址發(fā)生變更需要修改多處
  • 請(qǐng)求方法可能存在重復(fù),別的頁(yè)面如果也要請(qǐng)求文章詳情,也需要寫一個(gè)getNewsDetail方法
  • 錯(cuò)誤處理存在重復(fù),多個(gè)網(wǎng)絡(luò)請(qǐng)求可能都要進(jìn)行相同的錯(cuò)誤處理
  • 請(qǐng)求依賴具體的第三方庫(kù)axios,如果有一天要更換網(wǎng)絡(luò)請(qǐng)求庫(kù),則需要進(jìn)行大量的修改

接下來(lái)我們來(lái)逐一解決這些重復(fù)。

首先是依賴axios的問(wèn)題,我們盡量不要直接依賴某個(gè)第三方插件,解決辦法也很簡(jiǎn)單,就是增加一層封裝, 這樣把依賴局限在具體某個(gè)方法內(nèi)部,后續(xù)要替換只需要更改一處即可,也就是上層業(yè)務(wù)不依賴底層實(shí)現(xiàn)。
不是axios有什么功能,我的網(wǎng)絡(luò)請(qǐng)求怎么調(diào)用,而是我想怎么調(diào)用網(wǎng)絡(luò)請(qǐng)求,怎么利用axios進(jìn)行實(shí)現(xiàn), 也就是要遵循"依賴倒置原則"。

我們可以實(shí)現(xiàn)一個(gè)request方法,來(lái)完成網(wǎng)絡(luò)請(qǐng)求,內(nèi)部調(diào)用axios,同時(shí)進(jìn)行通用的錯(cuò)誤處理。

// request.js
import axios from 'axios'

export default function request(config) {
    return new Promise((function (resolve, reject) {
        axios(config).then(res => {
            if (res.code && res.code === 200) {
                resolve(res)
            } else if (res.code === 401) {
                //其他各種錯(cuò)誤處理類似
                showToast("網(wǎng)絡(luò)請(qǐng)求失敗")
                window.location.href = '/#/login'
            } else {
                reject(res)
            }
        }).catch(e => {
            reject(e)
        })
    }))
}

['get', 'post', 'put', 'delete'].forEach(method => {
    return function (url, params, body, options) {
        return request({
            method,
            url,
            params,
            body,
            ...options
        })
    }
})

針對(duì)接口重復(fù)問(wèn)題,我們可以創(chuàng)建一個(gè) api.js文件,來(lái)配置各個(gè)接口地址。

export default {
    newDetail: '/api/v1/news/detail/:id'
    // 其他接口地址
}

針對(duì)接口調(diào)用重復(fù)問(wèn)題,我們可以封裝一個(gè)service層,所有頁(yè)面調(diào)用均通過(guò)調(diào)用service層方法來(lái)實(shí)現(xiàn)。 service/news.js

import request from './utils/request'
import api from './api'

export default {
    getNewsDetail(id) {
        return request.get(api.newDetail, {id}).then(res => {
            return res.data
        })
    }
}

修改之前和修改之后對(duì)比如下,通過(guò)增加中間層service.js、封裝網(wǎng)絡(luò)請(qǐng)求方法request.js以及封裝接口地址配置常量api.js, 大大降低網(wǎng)絡(luò)請(qǐng)求的復(fù)用,同時(shí)也不再依賴第三方axios,實(shí)現(xiàn)了解耦。

[圖片上傳失敗...(image-5a8149-1669635426525)]

職責(zé)不單一

單一職責(zé)的函數(shù)或者組件,就像是一個(gè)積木塊,而不滿足單一職責(zé)特性的函數(shù)或組件就像是一個(gè)功能模塊。

單一職責(zé)的積木塊可以進(jìn)行各種排列組合,綻放出強(qiáng)大的生命力,而多功能的模塊,只適用于特定的業(yè)務(wù)場(chǎng)景,復(fù)用性則大大降低。

[圖片上傳失敗...(image-cdd289-1669635426525)]

比如我們要實(shí)現(xiàn)一個(gè)下載一段文字的功能,這個(gè)功能分為兩步,第一步將文字內(nèi)容轉(zhuǎn)為url,第二步通過(guò)創(chuàng)建a標(biāo)簽實(shí)現(xiàn)下載。 假如實(shí)現(xiàn)的downloadText方法如下:

function downloadText(text, filename) {
    //根據(jù)text內(nèi)容,創(chuàng)建url
    let blob = new Blob([content]);
    let url = URL.createObjectURL(blob);

    //創(chuàng)建a標(biāo)簽,通過(guò)模擬a標(biāo)簽的點(diǎn)擊實(shí)現(xiàn)下載
    let eleLink = document.createElement('a');
    eleLink.download = filename;
    eleLink.style.display = 'none';
    eleLink.href = url
    document.body.appendChild(eleLink);
    eleLink.click();
    document.body.removeChild(eleLink);
}

很明顯這里不符合單一職責(zé),假如我現(xiàn)在不是根據(jù)text文本進(jìn)行下載,而是給定一個(gè)具體src下載,可能還要再寫個(gè)downloadUrl方法,很明顯,二者之間有很多重復(fù)。

function downloadText(text, filename) {
    //根據(jù)text內(nèi)容,創(chuàng)建url
    let blob = new Blob([text]);
    let url = URL.createObjectURL(blob);

    //創(chuàng)建a標(biāo)簽,通過(guò)模擬a標(biāo)簽的點(diǎn)擊實(shí)現(xiàn)下載
    let eleLink = document.createElement('a');
    eleLink.download = filename;
    eleLink.style.display = 'none';
    eleLink.href = url
    document.body.appendChild(eleLink);
    eleLink.click();
    document.body.removeChild(eleLink);
}

function downloadUrl(url, filename) {
    //創(chuàng)建a標(biāo)簽,通過(guò)模擬a標(biāo)簽的點(diǎn)擊實(shí)現(xiàn)下載
    let eleLink = document.createElement('a');
    eleLink.download = filename;
    eleLink.style.display = 'none';
    eleLink.href = url
    document.body.appendChild(eleLink);
    eleLink.click();
    document.body.removeChild(eleLink);
}

根據(jù)單一職責(zé)原則對(duì)downloadText方法進(jìn)行拆分,可以拆成兩個(gè),一個(gè)根據(jù)文本生成url,一個(gè)根據(jù)url進(jìn)行下載,如果還想保留downloadText, 只需要組合這兩個(gè)小方法即可,這樣我們一下就產(chǎn)出了3個(gè)通用方法。


function createUrlByText(text) {
    let blob = new Blob([text]);
    return URL.createObjectURL(blob);
}

function downloadUrl(url, filename) {
    let eleLink = document.createElement('a');
    eleLink.download = filename;
    eleLink.style.display = 'none';
    eleLink.href = url
    document.body.appendChild(eleLink);
    eleLink.click();
    document.body.removeChild(eleLink);
}

//對(duì)上面的單一職責(zé)功能進(jìn)行組合
function downloadText(text, filename) {
    let url = createUrlByText(text)
    downloadUrl(url, filename)
}

其實(shí)功能實(shí)現(xiàn)并沒(méi)有本質(zhì)的區(qū)別,只是簡(jiǎn)單的進(jìn)行拆分,使其滿足單一職責(zé)原則,即大大提高了復(fù)用性。

數(shù)據(jù)之間的重復(fù)

數(shù)據(jù)之間的重復(fù),也是經(jīng)常出現(xiàn)的一種重復(fù)問(wèn)題,也就是能用1個(gè)字段表示的數(shù)據(jù),不要用2個(gè)或多個(gè)字段表示。 通??梢杂?個(gè)基礎(chǔ)的數(shù)據(jù)字段,其他字段可以通過(guò)這個(gè)基礎(chǔ)字段來(lái)計(jì)算,而不是維護(hù)其他幾個(gè)額外字段。

比如要實(shí)現(xiàn)如下的一個(gè)列表,一共有3個(gè)字段:列表數(shù)據(jù)list、已選中數(shù)量selectedCount、總數(shù)量totalCount。

[圖片上傳失敗...(image-b5edd6-1669635426525)]

假如維護(hù)這三個(gè)字段,每次當(dāng)列表數(shù)據(jù)發(fā)生變化時(shí)(可能是選擇狀態(tài)變化,也可能是增刪數(shù)據(jù)), 都要小心設(shè)置selectedCount、totalCount,否則就會(huì)出現(xiàn)數(shù)據(jù)不一致的bug,這其實(shí)是一種邏輯上的重復(fù)。

我們可以只維護(hù)一個(gè)基礎(chǔ)數(shù)據(jù)list,selectedCount和totalCount都可以通過(guò)對(duì)list進(jìn)行計(jì)算而得到, 可能是寫一個(gè)方法每次刷新組件時(shí)重新計(jì)算,也可以利用vue中的計(jì)算屬性。


<script>
export default {
  data() {
    return {
      list: [
        {
          name: '蘋果',
          selected: false
        },
        {
          name: '橘子',
          selected: true
        },
        {
          name: '香蕉',
          selected: true
        }
      ]
    }
  },
  computed: {
    selectedCount() {
      return this.list.filter(item => item.selected).length
    },
    totalCount() {
      return this.list.length
    }
  }
}
</script>

這樣我們只需要對(duì)list進(jìn)行處理即可,其他兩個(gè)字段自動(dòng)計(jì)算出來(lái),避免數(shù)據(jù)不一致問(wèn)題。

復(fù)用和耦合

復(fù)用雖好,但是也不能貪杯,因?yàn)閺?fù)用就意味著耦合,不合理的復(fù)用會(huì)導(dǎo)致嚴(yán)重的耦合,代碼可讀性及可維護(hù)性變差。

不合理的復(fù)用,甚至不如不進(jìn)行復(fù)用,所以在進(jìn)行復(fù)用抽象時(shí),應(yīng)該慎重。

充斥著各種if-else的復(fù)用

有的復(fù)用并不是真正的復(fù)用,而是將一些功能集中到了一個(gè)函數(shù)內(nèi)部,然后再內(nèi)部進(jìn)行大量的if-else判斷,實(shí)際是把邏輯復(fù)雜性轉(zhuǎn)移到了函數(shù)內(nèi)部, 由于存在大量的分支判斷,復(fù)雜度呈現(xiàn)指數(shù)式增長(zhǎng)。

比如應(yīng)用的安裝、重啟、銷毀、切換版本等都使用一個(gè)函數(shù),表面上都復(fù)用了operate函數(shù),但是卻在函數(shù)內(nèi)部進(jìn)行大量的分支判斷,各自處理各自的事情,
而且由于各個(gè)操作需要的傳參可能還不一樣,導(dǎo)致雖然共用一個(gè)函數(shù),但是傳參不一樣,內(nèi)部處理邏輯也沒(méi)有復(fù)用,還增加了耦合, 導(dǎo)致想要搞明白某個(gè)操作的處理流程,非常復(fù)雜,這樣的復(fù)用有什么意義呢?

<!--不合理的復(fù)用-->
<template>
  <div>
    <el-button @click="operate(row, 'install')">部署</el-button>
    <el-button @click="operate(row, 'restart')">重啟</el-button>
    <el-button @click="operate(row, 'destroy')">銷毀</el-button>
    <el-button @click="operate(row, 'upgrade-version', row.package_version)">切換版本</el-button>
  </div>
</template>
<script>
export default {
  methods: {
    operate(row, opt, package_version) {
      if (opt === 'install') {
        //...
      }
      if (opt === 'destroy') {
        //...
      } else if (['install', 'update-values', 'update-default', 'upgrade-version'].includes(opt)) {
        //...
      } else {
        if (['restart'].includes(opt)) {
          //...
        } else {
          //...
        }
      }
    }
  }
}
</script>

上面這個(gè),不如針對(duì)不同的操作,直接對(duì)應(yīng)一個(gè)獨(dú)立的處理函數(shù),如果多個(gè)函數(shù)之間有復(fù)用,可以抽取出來(lái)公共函數(shù),但是每個(gè)操作的處理流程是清晰的。
修改之后,每個(gè)交互對(duì)應(yīng)的操作是明確的,由于拆分了,傳參也簡(jiǎn)化了,每個(gè)操作內(nèi)部沒(méi)有了分支判斷,邏輯也簡(jiǎn)化了。

<!--修改之后-->
<template>
  <div>
    <el-button @click="install(row)">部署</el-button>
    <el-button @click="restart(row)">重啟</el-button>
    <el-button @click="destroy(row)">銷毀</el-button>
    <el-button @click="upgradeVersion(row,row.package_version)">切換版本</el-button>
  </div>
</template>
<script>
export default {
  methods: {
    install(row) {
      //安裝處理流程
      this.commonOperate()
    },
    restart(row) {
      //重啟處理流程
      this.commonOperate()
    },
    destroy(row) {
      //銷毀處理流程
      this.commonOperate()
    },
    upgradeVersion(row, version) {
      //切換版本處理流程
      this.commonOperate()
    },
    commonOperate() {
      //...
    }
  }
}
</script>

盡量不要復(fù)用不同業(yè)務(wù)的處理流程,不同業(yè)務(wù)的流程后續(xù)很可能向著不同的方向發(fā)展,強(qiáng)行復(fù)用只會(huì)增加復(fù)雜度和耦合,我們可以抽象各個(gè)流程的公共處理方法,
比如多個(gè)操作的接口請(qǐng)求是一致的,但是每個(gè)操作的參數(shù)是不同的,那么可以在不同的業(yè)務(wù)處理方法中準(zhǔn)備不同的參數(shù),然后調(diào)用統(tǒng)一的網(wǎng)絡(luò)請(qǐng)求方法完成接口調(diào)用。

還有一種類似的問(wèn)題,出現(xiàn)在UI的復(fù)用上,根據(jù)不同的屬性,比如應(yīng)用的操作類型(安裝、重啟等待),渲染不同的頁(yè)面,由于充斥大量的分支, 很難搞明白某個(gè)操作對(duì)應(yīng)那些UI,修改某個(gè)bug時(shí)很容易引入新的bug。


<template>
  <div>
    <div v-if="operate === 'install'">***</div>
    <div v-else>***</div>
    <div v-if="['install', 'start'].includes(operate)">***</div>
  </div>
</template>
<script>
export default {
  props: ['operate']
}
</script>

綜上,當(dāng)遇到有大量分支判斷時(shí),就要考慮這個(gè)復(fù)用是否合理了。

入口文件盡量不要復(fù)用

所謂的入口文件就是和用戶直接進(jìn)行交互的文件,比如不同路由對(duì)應(yīng)的頁(yè)面是一種入口文件,不同按鈕對(duì)應(yīng)的handle函數(shù)也是一種入口文件, 入口文件耦合著業(yè)務(wù)邏輯,最好不進(jìn)行復(fù)用。

比如兩個(gè)路由對(duì)應(yīng)的頁(yè)面非常相似,如果我們直接復(fù)用這個(gè)路由文件,那么就會(huì)在路由文件的實(shí)現(xiàn)中,進(jìn)行各種if-else判斷,來(lái)區(qū)分環(huán)境, 而且很難了解不同路由到底會(huì)對(duì)頁(yè)面產(chǎn)生哪些影響。

比如,route1和route2復(fù)用User組件,那么User組件內(nèi)部肯定要進(jìn)行各種條件判斷,來(lái)針對(duì)route1和route2呈現(xiàn)不同的效果,
那么要問(wèn)route1和route2的表現(xiàn)在User組件有什么不同,你必須去仔細(xì)閱讀User的實(shí)現(xiàn)才能知曉。

//假設(shè)兩個(gè)頁(yè)面很相似,復(fù)用了路由入口文件
const routes = [
    {
        path: 'route1',
        name: 'route1',
        component: User,
    },
    {
        path: 'route2',
        name: 'route2',
        component: User,
    },
]

User組件實(shí)現(xiàn)


<template>
  <div>
    <!-- 入口文件只能通過(guò)路由不同的參數(shù)來(lái)區(qū)分到底是從哪個(gè)路由進(jìn)來(lái)的,呈現(xiàn)不同效果 -->
    <div v-if="$route.params.test === '**'">
      **
    </div>
    <div v-else>
      **
    </div>
  </div>
</template>
<script>
export default {
  mounted() {
    // 入口文件只能通過(guò)路由不同的參數(shù)來(lái)區(qū)分到底是從哪個(gè)路由進(jìn)來(lái)的,呈現(xiàn)不同效果
    if (this.$route.params.test === '**') {
      //...
    } else {
      //...
    }
  }
}
</script>

復(fù)用了入口文件后,很難說(shuō)清楚不同入口有什么區(qū)別,如果入口文件不復(fù)用,我們可以把公共內(nèi)容封裝成組件,然后在不同的入口文件中進(jìn)行調(diào)用,
調(diào)用時(shí)傳遞明確的屬性,很清楚整體的邏輯。

比如route1對(duì)應(yīng)User1文件,route2對(duì)應(yīng)User2文件

//假設(shè)兩個(gè)頁(yè)面很相似,復(fù)用了路由入口文件
const routes = [
    {
        path: 'route1',
        name: 'route1',
        component: User1,
    },
    {
        path: 'route2',
        name: 'route2',
        component: User2,
    },
]

User1組件實(shí)現(xiàn)如下,User1組件中調(diào)用組件User,同時(shí)傳遞屬性過(guò)去

<template>
  <User :can-add="true" :can-delete="false" />
</template>

User2組件實(shí)現(xiàn)如下,User2組件也調(diào)用復(fù)用的組件User,同時(shí)傳遞所需屬性過(guò)去

<template>
  <User :can-add="false" :can-delete="true" />
</template>

User組件實(shí)現(xiàn)

<template>
  <div v-if="canAdd">
    添加
  </div>
  <div v-if="canDelete">
    刪除
  </div>
</template>
<script>
export default {
  props:['canAdd', 'canDelete']
}
</script>

我們通過(guò)抽取User組件,實(shí)現(xiàn)了主要功能的復(fù)用,同時(shí)又避免了入口文件的復(fù)用,每個(gè)入口文件傳遞什么屬性都很容易看到,也很容易理解其中邏輯。

類似的,不同按鈕對(duì)應(yīng)的操作函數(shù)盡量不要復(fù)用,因?yàn)椴煌陌粹o就代表不同的業(yè)務(wù)流程,很難說(shuō)多個(gè)業(yè)務(wù)的流程始終能保持一致,可能初始時(shí),
兩個(gè)按鈕操作基本一樣,但是隨著后續(xù)需求變化,這個(gè)復(fù)用的函數(shù)就開(kāi)始增加各種if-else以應(yīng)對(duì)需求變化,導(dǎo)致可讀性越來(lái)越差,耦合越來(lái)越多。

不能因?yàn)殚L(zhǎng)得相似就復(fù)用

假如某個(gè)管理系統(tǒng)的2個(gè)列表頁(yè)非常相似,比如一個(gè)是資訊列表頁(yè),一個(gè)是商品列表頁(yè),它們都有一個(gè)添加按鈕、一個(gè)按名稱搜索的輸入框,一個(gè)表格,
而且表格的列都只有名稱、創(chuàng)建人、創(chuàng)建時(shí)間三列,那么我們應(yīng)該復(fù)用嗎?

很顯然,這樣的情況是不能復(fù)用的,隨著業(yè)務(wù)的發(fā)展,資訊和商品,一定會(huì)向著兩個(gè)不同的方向發(fā)展,不可避免的后續(xù)會(huì)增加各種邏輯判斷,
而且因?yàn)檫@種耦合還會(huì)造成各種意外的bug。

我們應(yīng)該復(fù)用其中的組件單元,比如統(tǒng)一的button組件、搜索表單、表格組件,通過(guò)組合基礎(chǔ)組件,來(lái)完成兩個(gè)頁(yè)面的開(kāi)發(fā)。

類似的還有表單校驗(yàn)的規(guī)則,比如名稱和描述,可能初始的校驗(yàn)規(guī)則一致,就進(jìn)行了復(fù)用,但是本質(zhì)上,名稱和描述是不同的業(yè)務(wù)元素,
他們的校驗(yàn)規(guī)則并沒(méi)有完全的相關(guān)性,很可能后續(xù)往著不同的方向發(fā)展。但是如果是多個(gè)表單的同一個(gè)業(yè)務(wù)元素的校驗(yàn),則是可以復(fù)用的,
可以預(yù)料到如果該元素在這個(gè)頁(yè)面變化了校驗(yàn),在其他頁(yè)面也應(yīng)該會(huì)變化。

是否能復(fù)用,要看邏輯上有沒(méi)有相關(guān)性,而不是僅僅因?yàn)殚L(zhǎng)得像就復(fù)用,復(fù)用的代價(jià)就是耦合。

本章小結(jié)

  • 重復(fù)可能是工作模式導(dǎo)致的,比如由于溝通不暢,缺少必要的基礎(chǔ)設(shè)施(如組件庫(kù)),知識(shí)沒(méi)有共享等造成了團(tuán)隊(duì)的工作重復(fù),集體效率和質(zhì)量的降低。
  • 缺少抽象會(huì)帶來(lái)重復(fù),越具體的越不容易復(fù)用,抽象可以增強(qiáng)復(fù)用性
  • 單一職責(zé)的代碼更容易復(fù)用
  • 要注意數(shù)據(jù)之間的重復(fù),防止出現(xiàn)數(shù)據(jù)不一致的情況
  • 不合理的復(fù)用會(huì)帶來(lái)耦合以及可讀性的降低
  • 當(dāng)一個(gè)共用的函數(shù)或組件出現(xiàn)大量分支的時(shí)候,說(shuō)明復(fù)用出現(xiàn)了問(wèn)題,可能需要進(jìn)行拆分
  • 入口文件盡量不要重復(fù),這樣可以增強(qiáng)代碼的可讀性
  • 是否復(fù)用取決于邏輯相關(guān)性,不能因?yàn)榇a相似而復(fù)用

前端同學(xué)歡迎添加vx好友交流:_hit757_

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容