新視角:如何降低前端業(yè)務(wù)復(fù)雜度?

來(lái)源:lecepin
https://juejin.cn/post/7045090471852376101

無(wú)論做業(yè)務(wù)需求還是做平臺(tái)需求的同學(xué),隨著需求的不斷迭代,通常都會(huì)出現(xiàn)邏輯復(fù)雜、狀態(tài)混亂的現(xiàn)象,維護(hù)和新增功能的成本也變的十分巨大,苦不堪言。下圖用需求、業(yè)務(wù)代碼、測(cè)試代碼做對(duì)比:


圖中分了 3 個(gè)階段:

階段 1:正常,都是線(xiàn)性增長(zhǎng)。
階段 2:需求數(shù)正常增長(zhǎng),業(yè)務(wù)代碼行數(shù)開(kāi)始增長(zhǎng),測(cè)試代碼行數(shù)大幅度增長(zhǎng)。
階段 3:業(yè)務(wù)代碼行數(shù)開(kāi)始大幅增長(zhǎng),測(cè)試代碼行數(shù)劇增(超出屏幕),而需求數(shù)開(kāi)始下降。
這可以很好的表達(dá)出,從業(yè)務(wù)最開(kāi)始,到長(zhǎng)期迭代后,復(fù)雜度提升帶來(lái)的問(wèn)題。做一個(gè)相同的需求,最開(kāi)始可能 1 天就可以搞定,但長(zhǎng)期迭代后,可能要 3 天,甚至更多,這并不是開(kāi)發(fā)人員主觀上導(dǎo)致的,而是代碼狀態(tài)的維護(hù)成本太高,做到最后經(jīng)常會(huì)出現(xiàn)牽一發(fā)而動(dòng)全身。也側(cè)面抑制了業(yè)務(wù)的迭代速度。

所以對(duì)于長(zhǎng)期迭代的產(chǎn)品,切記不要簡(jiǎn)單做,否則都是給后面挖的坑。

當(dāng)然,看問(wèn)題還是要去看本質(zhì)。根據(jù)復(fù)雜度守恒定律(泰斯勒定律),每個(gè)應(yīng)用程序都具有其內(nèi)在的、無(wú)法簡(jiǎn)化的復(fù)雜度。這一固有的復(fù)雜度都無(wú)法依照我們的意愿去除,只能設(shè)法調(diào)整、平衡。而現(xiàn)在前端的復(fù)雜度拆分主要包括:框架、通用組件、業(yè)務(wù)組件和業(yè)務(wù)邏輯,如下圖所示:


image.png
上圖中可以看到,當(dāng)把框架和通用組件建設(shè)完成后,能夠承擔(dān)的復(fù)雜度基本穩(wěn)定了,未來(lái)無(wú)輪再怎么改善或者更換其他框架,也很難再去突破天花板,對(duì)業(yè)務(wù)的復(fù)雜度的改變也微乎其微了(如果你的業(yè)務(wù)經(jīng)歷過(guò)底層框架更換,你就能體會(huì)到它到底對(duì)你的業(yè)務(wù)復(fù)雜度有沒(méi)有帶來(lái)變化了)。

我們就要去思考,到底哪里還能把復(fù)雜度給降下來(lái)。換個(gè)角度,是不是可以從業(yè)務(wù)共有的 “業(yè)務(wù)邏輯” 側(cè)去進(jìn)行突破?目前發(fā)現(xiàn)的,做業(yè)務(wù)側(cè)提效的方案中,很少有從 “業(yè)務(wù)邏輯” 視角為出發(fā)點(diǎn)去做的,更多的是聚焦在場(chǎng)景化上的提效。

把視角聚焦到 “業(yè)務(wù)邏輯” 側(cè),這里就要看所有業(yè)務(wù)中都會(huì)面臨的問(wèn)題,是什么讓業(yè)務(wù)復(fù)雜度提升上去了。這里主要存在兩點(diǎn),如下:

代碼層面
各種各樣的業(yè)務(wù)狀態(tài)導(dǎo)致的 flag 變量的劇增:即便是自己,寫(xiě)多了這種變量,也很難清楚的知道每個(gè) flag是干什么用的。
各種判斷業(yè)務(wù)狀態(tài)的 if/else:if/else 嵌套地獄估計(jì)在很多大型的業(yè)務(wù)產(chǎn)品中都能看到吧。還有內(nèi)部的各種邏輯判斷,如 isA && isB || !(isC || !isD && isE),完全看不懂,即便問(wèn) PD,時(shí)間久了她也不知道了。還有因此可能導(dǎo)致一些意識(shí)不到的 Bug。
協(xié)作層面
做業(yè)務(wù)的同學(xué)很難有全局業(yè)務(wù)視角,所以面對(duì) PD 的需求很難有話(huà)語(yǔ)權(quán)。如果需求設(shè)計(jì)不合理,只能等到你做完了,在 UAT 的階段才能發(fā)現(xiàn),然后 PD 會(huì)給你提一個(gè)新需求,讓你再去修正(雖然是 PD 的問(wèn)題,但缺乏避免 PD 犯錯(cuò)的途徑)。
測(cè)試同學(xué),測(cè)試的內(nèi)容范圍,多數(shù)情況下,取決于前端同學(xué)給定的測(cè)試范圍。而很多時(shí)候代碼的改動(dòng),前端也不確定到底哪些頁(yè)面會(huì)受影響。所以要么導(dǎo)致測(cè)試同學(xué)測(cè)試不完整,要么導(dǎo)致測(cè)試同學(xué)需要全量回歸,這可是非常巨大的測(cè)試成本。
當(dāng)其他前端開(kāi)發(fā)人員,參與到項(xiàng)目中時(shí),面臨這種復(fù)雜的項(xiàng)目也是頭大,需要花費(fèi)很大的成本梳理清楚業(yè)務(wù)與代碼的關(guān)聯(lián)。導(dǎo)致合作或者交接項(xiàng)目時(shí),困難。
我們需要通過(guò)發(fā)現(xiàn)的這些問(wèn)題,來(lái)尋找合適的解決方案。

1. 解決代碼層面的問(wèn)題
代碼層面的問(wèn)題,主要來(lái)源于 flag 變量過(guò)多,及 if/else 的嵌套及大量分支,導(dǎo)致難以修改和擴(kuò)展,任何改動(dòng)和變化都是致命的。其實(shí)這類(lèi)問(wèn)題,在設(shè)計(jì)模式中是有合適的方案——狀態(tài)模式。

1.1. 狀態(tài)模式
狀態(tài)模式主要解決的是,當(dāng)控制一個(gè)對(duì)象狀態(tài)轉(zhuǎn)換的條件表達(dá)式過(guò)于復(fù)雜時(shí)的情況。把狀態(tài)的判斷邏輯轉(zhuǎn)移到表示不同狀態(tài)的一系列類(lèi)當(dāng)中,減少相互間的依賴(lài),可以把復(fù)雜的判斷邏輯簡(jiǎn)化。

狀態(tài)模式是一種行為模式,在不同的狀態(tài)下有不同的行為,它將狀態(tài)和行為解耦。


從類(lèi)圖中可以看到,狀態(tài)模式是多態(tài)特性和面向接口的完美體現(xiàn),State 是一個(gè)接口,表示狀態(tài)的抽象,ConcreteStateA 和 ConcreteStateB 是具體的狀態(tài)實(shí)現(xiàn)類(lèi),表示兩種狀態(tài)的行為,Context 的 request() 方法將會(huì)根據(jù)狀態(tài)的變更從而調(diào)用不同 State 接口實(shí)現(xiàn)類(lèi)的具體行為方法。

狀態(tài)模式的好處是,將與特定狀態(tài)相關(guān)的行為局部化,并且將不同狀態(tài)的行為分割開(kāi)來(lái)。這樣這些對(duì)象就可以不依賴(lài)于其他對(duì)象而獨(dú)立變化了,未來(lái)增加或修改狀態(tài)流程,就不是困難的事了。

當(dāng)一個(gè)對(duì)象的行為取決于它的狀態(tài),并且它必須在運(yùn)行時(shí)刻根據(jù)狀態(tài)改變它的行為時(shí),就可以考慮使用狀態(tài)模式了。

1.2. 狀態(tài)機(jī)
狀態(tài)機(jī),全稱(chēng)有限狀態(tài)機(jī)(finite-state machine,縮寫(xiě):FSM),又稱(chēng)有限狀態(tài)自動(dòng)機(jī)(finite-state automaton,縮寫(xiě):FSA),是現(xiàn)實(shí)事物運(yùn)行規(guī)則抽象而成的一個(gè)數(shù)學(xué)模型,并不是指一臺(tái)實(shí)際機(jī)器。狀態(tài)機(jī)是圖靈機(jī)的一個(gè)子集。它是一種認(rèn)知論。從某種角度來(lái)說(shuō),我們的現(xiàn)實(shí)世界就是一個(gè)有限狀態(tài)機(jī)。

有限狀態(tài)自動(dòng)機(jī)在很多不同領(lǐng)域中是重要的,包括電子工程、語(yǔ)言學(xué)、計(jì)算機(jī)科學(xué)、哲學(xué)、生物學(xué)、數(shù)學(xué)和邏輯學(xué)。有限狀態(tài)機(jī)是在自動(dòng)機(jī)理論和計(jì)算理論中研究的一類(lèi)自動(dòng)機(jī)。在計(jì)算機(jī)科學(xué)中,有限狀態(tài)機(jī)被廣泛用于建模應(yīng)用行為、硬件電路系統(tǒng)設(shè)計(jì)、軟件工程,編譯器、網(wǎng)絡(luò)協(xié)議、和計(jì)算與語(yǔ)言的研究。它是非常成熟的一套方法論。

有限狀態(tài)機(jī)包含五個(gè)重要部分:

初始狀態(tài)值 (initial state)
有限的一組狀態(tài) (states)
有限的一組事件 (events)
由事件驅(qū)動(dòng)的一組狀態(tài)轉(zhuǎn)移關(guān)系 (transitions)
有限的一組最終狀態(tài) (final states)
更簡(jiǎn)潔的總結(jié),就三個(gè)部分:

狀態(tài) State
事件 Event
轉(zhuǎn)換 Transition
同一時(shí)刻,只可能存在一個(gè)狀態(tài)。例如,人有 “睡著” 和 “醒著” 兩個(gè)狀態(tài),同一時(shí)刻,要么 “睡著” 要么 “醒著”,不可能存在 “半睡半醒” 的狀態(tài)。

邏輯學(xué)中說(shuō),現(xiàn)實(shí)生活中描述的事物都可以抽象為命題。命題本質(zhì)上就是狀態(tài)機(jī)的 State,Event 就是命題的條件,通過(guò)命題和條件推導(dǎo)過(guò)程。而 Transition 就是命題推導(dǎo)完成的結(jié)論。

所以當(dāng)我們拿到需求的時(shí)候,首先要分離出哪些是已知的命題(State),哪些是條件(Event),哪些是結(jié)論(Transition)。而我們要通過(guò)這些已知命題和條件,推導(dǎo)出結(jié)論的過(guò)程。

1.2.1. 拿我們經(jīng)常用到的 Fetch API 來(lái)舉例子
fetch(url).then().catch()
復(fù)制代碼
有限的一組狀態(tài):


初始狀態(tài):


有限的一組最終狀態(tài):


有限的一組事件:


Idle 狀態(tài)只處理 FETCH 事件
Pending 狀態(tài)只處理 RESOLVE 和 REJECT 事件
由事件驅(qū)動(dòng)的一組狀態(tài)轉(zhuǎn)移關(guān)系:


1.3. 狀態(tài)機(jī) VS 傳統(tǒng)編碼 示例
下面采用一個(gè)小需求來(lái)對(duì)比一下區(qū)別。

1.3.1. 需求描述
根據(jù)輸入的關(guān)鍵字進(jìn)行搜索,并將搜索結(jié)果顯示出來(lái)。如下圖所示:


1.3.2. 基于傳統(tǒng)編碼
根據(jù)關(guān)鍵字拿到請(qǐng)求結(jié)果,再將結(jié)果塞回去就行了,代碼如下:

function onSearch(keyword) {
  fetch(SEARCH_URL + "?keyword=" + keyword).then((data) => {
    this.setState({ data });
  });
}
復(fù)制代碼
看似幾行代碼就把這個(gè)需求搞定了,但其實(shí)還有一些其他問(wèn)題要處理。如果接口響應(yīng)比較慢,則需要給一個(gè)用戶(hù)預(yù)期的交互,如 Loading 效果:

function onSearch(keyword) {
  this.setState({
    isLoading: true,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword).then((data) => {
    this.setState({ data, isLoading: false });
  });
}
復(fù)制代碼
還會(huì)發(fā)生出請(qǐng)求出錯(cuò)的情況:

function onSearch(keyword) {
  this.setState({
    isLoading: true,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword)
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      this.setState({
        isError: true,
      });
    });
}
復(fù)制代碼
當(dāng)然,不能忘記把 Loading 關(guān)掉:

function onSearch(keyword) {
  this.setState({
    isLoading: true,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword)
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      this.setState({
        isError: true,
        isLoading: false,
      });
    });
}
復(fù)制代碼
我們每次搜索時(shí),還需要把錯(cuò)誤清除:

function onSearch(keyword) {
  this.setState({
    isLoading: true,
    isError: false,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword)
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      this.setState({
        isError: true,
        isLoading: false,
      });
    });
}
復(fù)制代碼
這就結(jié)束了么,是不是我們把所有的 Bug 都考慮進(jìn)去了?并沒(méi)有。當(dāng)用戶(hù)在等待搜素請(qǐng)求的時(shí)候,不應(yīng)該再去搜索,所以搜索結(jié)果返回前,禁止再次發(fā)送請(qǐng)求:

function onSearch(keyword) {
  if (this.state.isLoading) {
    return;
  }

  this.setState({
    isLoading: true,
    isError: false,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword)
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      this.setState({
        isError: true,
        isLoading: false,
      });
    });
}
復(fù)制代碼
可以看到,應(yīng)用的復(fù)雜度在不斷變大,可能你經(jīng)歷的場(chǎng)景比這個(gè)小示例還要復(fù)雜的多的多。如果因?yàn)樗阉鹘涌谔貏e慢,用戶(hù)希望有一個(gè)中斷搜索的功能,那么新的需求又來(lái)了:

function onSearch(keyword) {
  if (this.state.isLoading) {
    return;
  }
  this.fetchAbort = new AbortController();

  this.setState({
    isLoading: true,
    isError: false,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword, {
    signal: this.fetchAbort.signal,
  })
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      this.setState({
        isError: true,
        isLoading: false,
      });
    });
}

function onCancel() {
  this.fetchAbort.abort();
}
復(fù)制代碼
不能落下對(duì) catch 的特殊處理,因?yàn)橹袛嗾?qǐng)求會(huì)觸發(fā) catch:

function onSearch(keyword) {
  if (this.state.isLoading) {
    return;
  }
  this.fetchAbort = new AbortController();

  this.setState({
    isLoading: true,
    isError: false,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword, {
    signal: this.fetchAbort.signal,
  })
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      if (e.name == "AbortError") {
        this.setState({
          isLoading: false,
        });
      } else {
        this.setState({
          isError: true,
          isLoading: false,
        });
      }
    });
}

function onCancel() {
  this.fetchAbort.abort();
}
復(fù)制代碼





最后還要處理沒(méi)有值的情況:

function onSearch(keyword) {
  if (this.state.isLoading) {
    return;
  }
  this.fetchAbort = new AbortController();

  this.setState({
    isLoading: true,
    isError: false,
  });

  fetch(SEARCH_URL + "?keyword=" + keyword, {
    signal: this.fetchAbort.signal,
  })
    .then((data) => {
      this.setState({ data, isLoading: false });
    })
    .catch((e) => {
      if (
        e &&
        e.name == "AbortError"
      ) {
        this.setState({
          isLoading: false,
        });
      } else {
        this.setState({
          isError: true,
          isLoading: false,
        });
      }
    });
}

function onCancel() {
  if (
    this.fetchAbort.abort &&
    typeof this.fetchAbort.abort == "function"
  ) {
    this.fetchAbort.abort();
  }
}
復(fù)制代碼
僅僅這么簡(jiǎn)單的一個(gè)小需求,從開(kāi)始幾行代碼就可以完成,到最終判斷各種邊界完成的代碼,對(duì)比一下,如下圖所示:


可以看到,這種包含各種 flag 變量和嵌套著各種 if/else 的代碼,會(huì)越來(lái)越難維護(hù),所有的邏輯只存在于你的腦子里。當(dāng)你寫(xiě)測(cè)試的時(shí)候必須從頭再梳理一遍代碼邏輯,才能寫(xiě)出來(lái)。

由于業(yè)務(wù)的高頻變化,很多業(yè)務(wù)開(kāi)發(fā)人員是不寫(xiě)單元測(cè)試的,因?yàn)槌杀咎咛?,這也導(dǎo)致了交接代碼時(shí),別人去理解你的代碼是一件很困難的事。寫(xiě)久了,你自己都可能讀不懂代碼里面的邏輯了。

這樣會(huì)導(dǎo)致:

難以測(cè)試
難以閱讀
可能含有隱藏的 Bug
難以擴(kuò)展
新功能增加時(shí)還會(huì)使邏輯進(jìn)一步混亂
1.3.3. 基于狀態(tài)機(jī)
看一下我們用狀態(tài)機(jī)的做法。記住流程:梳理出有哪些狀態(tài),每個(gè)狀態(tài)有哪些事件,經(jīng)歷了這些事件又會(huì)轉(zhuǎn)換到什么狀態(tài)。

下面是用 XState 狀態(tài)機(jī)工具的 JSON 描述:

{
  initial: "空閑",
  states: {
    空閑:{
      on:{
        搜索: '搜索中'
      }
    },
    搜索中:{
      on:{
        搜索成功: '成功',
        搜索失敗: '失敗',
        取消: '空閑'
      }},
    成功:{
      on:{
        搜索: '搜索中'
      }},
    失敗:{
      on:{
        搜索: '搜索中'
      }}
  },
}
復(fù)制代碼
沒(méi)錯(cuò),就這幾行代碼就描述清楚所有的關(guān)系了。并且,可以把它可視化出來(lái),如下圖所示:


可以看到狀態(tài)之間表達(dá)的非常清晰,結(jié)合到 View 中,也不需要再去編寫(xiě)復(fù)雜的 flag 及 if/else 了,View 中只需要知道當(dāng)前是什么狀態(tài),已及將事件發(fā)送到狀態(tài)機(jī)就可以了,其他什么都不需要做。在新增或者修改需求的情況下,只需要對(duì)狀態(tài)進(jìn)行新增或者編排就可以了。

而且可視化后,有以下變化:

清晰的看到有哪些狀態(tài)
清晰的看到每個(gè)狀態(tài)可以接受哪些事件
清晰的看到接受到事件后會(huì)轉(zhuǎn)移到什么狀態(tài)
清晰的看到到達(dá)某個(gè)狀態(tài)的路徑是怎么樣的
2. 解決協(xié)作的問(wèn)題
另一個(gè)很大的問(wèn)題是解決協(xié)作問(wèn)題,主要包括:

與測(cè)試開(kāi)人員的協(xié)作溝通
與 PD 人員的協(xié)作溝通
與其他前端開(kāi)發(fā)人員的協(xié)作溝通
與用戶(hù)的協(xié)作溝通
這里就需要引用一個(gè)可視化的概念了??梢暬?,是利用人眼的感知能力對(duì)數(shù)據(jù)進(jìn)行交互的可視表達(dá)以增強(qiáng)認(rèn)知的技術(shù) 。

所以很大程度上,可視化可以解決一大部分協(xié)作問(wèn)題。當(dāng)然,必須要確定把什么進(jìn)行可視化才是有意義的。

要想可視化,狀態(tài)工具就需要具備可序列化的能力。這也是 Redux 之類(lèi)的狀態(tài)管理工具缺乏的,主要有以下幾方面問(wèn)題:

不具備可視化的能力
狀態(tài)和數(shù)據(jù)混在一起
所有的狀態(tài)都是平級(jí)的,無(wú)法描述狀態(tài)之間的關(guān)系
2.1. 狀態(tài)圖
回到狀態(tài)機(jī)。你單純用狀態(tài)機(jī)去寫(xiě)代碼,需求數(shù)量上去了,狀態(tài)多了,會(huì)面臨 “狀態(tài)爆炸” 問(wèn)題,依然很難維護(hù),且閱讀成本巨大。

當(dāng)然,這個(gè)場(chǎng)景其實(shí)很早之前就有人考慮到了,1987 年,Harel 就發(fā)表論文,解決復(fù)雜狀態(tài)機(jī)可視化的問(wèn)題,在狀態(tài)機(jī)的基礎(chǔ)上進(jìn)一步增強(qiáng),提出狀態(tài)圖的概念。隨后,由微軟、IBM、惠普等多家公司,從 2005 到 2015 年花了 10 年時(shí)間制定了規(guī)范,并推出了 W3C 的 State Chart XML (SCXML) 規(guī)范,至此基本穩(wěn)定,各家編程語(yǔ)言也基于此規(guī)范進(jìn)行了狀態(tài)圖的封裝。

看一下,狀態(tài)機(jī)、狀態(tài)圖和手寫(xiě)代碼復(fù)雜度的對(duì)比,如下圖所示:


從圖中可以看到:

傳統(tǒng)編碼方式,隨著狀態(tài)和邏輯的增加,復(fù)雜度是線(xiàn)性增長(zhǎng)的。
使用狀態(tài)機(jī),前期復(fù)雜度很底,但隨著狀態(tài)的增多,“狀態(tài)爆炸”現(xiàn)象的出現(xiàn),復(fù)雜度也急劇增長(zhǎng)。
使用狀態(tài)圖,雖然前期成本略高,但后期的狀態(tài)和邏輯的增長(zhǎng),基本不太會(huì)影響它的復(fù)雜度。
前面給狀態(tài)機(jī)畫(huà)的圖,就是狀態(tài)圖。

狀態(tài)圖大概長(zhǎng)這樣,如下圖所示:


主要包括:

狀態(tài)
原子狀態(tài)
復(fù)合狀態(tài)
條件狀態(tài)
最終狀態(tài)
歷史狀態(tài)
初始狀態(tài)
并行狀態(tài)
偽/瞬間狀態(tài)
轉(zhuǎn)換
自動(dòng)轉(zhuǎn)換
延遲轉(zhuǎn)換
自身轉(zhuǎn)換
內(nèi)部轉(zhuǎn)換
操作
自定義操作
進(jìn)入操作
退出操作
數(shù)據(jù)操作
日志操作
事件
生成事件
延遲時(shí)間
條件
數(shù)據(jù)
調(diào)用
即使?fàn)顟B(tài)非常復(fù)雜,也可以通過(guò)狀態(tài)圖的模式進(jìn)行聚合、分組、細(xì)化,還可以通過(guò) Actor 模型進(jìn)行劃分,不會(huì)發(fā)生 “狀態(tài)爆炸” 現(xiàn)象。

2.2. 文檔化
目前對(duì)項(xiàng)目需求的描述主要有:

產(chǎn)品需求文檔(PRD)
設(shè)計(jì)稿
而這兩個(gè),在描述頁(yè)面行為上都不夠細(xì)致,PRD 幾乎不會(huì)去描述過(guò)于細(xì)節(jié)的交互行為,設(shè)計(jì)稿大概率也不會(huì)(因?yàn)闃I(yè)務(wù)交付周期上不允許在這上面花費(fèi)太多的時(shí)間)。而對(duì)于這些不清楚的、模糊的點(diǎn),就帶來(lái)了后面的問(wèn)題,針對(duì)于這些細(xì)節(jié)點(diǎn),各個(gè)角色之間的溝通成本和拉通成本。

還有一個(gè)很?chē)?yán)重的問(wèn)題,就是同步問(wèn)題。很多時(shí)候在開(kāi)發(fā)過(guò)程中,進(jìn)行需求變動(dòng),而大多數(shù)情況下,這些變動(dòng)不會(huì)重新對(duì) PRD 和設(shè)計(jì)稿進(jìn)行修改,不同角色之間去對(duì)焦及未來(lái)回顧,都是問(wèn)題。

而如果你使用狀態(tài)機(jī)開(kāi)發(fā),那這兩個(gè)問(wèn)題就可以迎刃而解。狀態(tài)機(jī)方式,要求你在開(kāi)發(fā)之前必須把所有可能的狀態(tài)都羅列出來(lái),狀態(tài)之間的關(guān)聯(lián)關(guān)系必須描述清晰?;谏傻臓顟B(tài)圖,是可以完全表達(dá)清楚所有的狀態(tài)交互及變化,且它是來(lái)源于代碼的,所以它是實(shí)時(shí)同步的,你代碼中怎么運(yùn)行的,這個(gè)狀態(tài)圖就是怎么表達(dá)的。

2.3. 角色影響
回到前面說(shuō)的,與不同角色協(xié)作的問(wèn)題上。有了狀態(tài)圖的加持,會(huì)發(fā)生什么變化:

設(shè)計(jì)師可以根據(jù)狀態(tài)圖中的不同狀態(tài),來(lái)確定哪種狀態(tài)合適用什么樣的 UI。
對(duì)于 PD,可以查看狀態(tài)圖,以了解系統(tǒng)行為,并驗(yàn)證是否滿(mǎn)足要求。
對(duì)于測(cè)試和用戶(hù),狀態(tài)圖完全充當(dāng)說(shuō)明書(shū)用,以前不知道如何才能到達(dá)某個(gè)狀態(tài),現(xiàn)在一目了然。
對(duì)于測(cè)試還有一個(gè)很大的區(qū)別,因?yàn)榛跔顟B(tài)機(jī)去寫(xiě)的,所以可以使用 Model-Based Testing,而這部分測(cè)試,可以由某些狀態(tài)機(jī)工具自動(dòng)化掉。
對(duì)于交接的前端開(kāi)發(fā)來(lái)說(shuō),有說(shuō)明書(shū)在手,每個(gè)狀態(tài)都十分清晰,能做的事也十分清晰,在具備狀態(tài)機(jī)基礎(chǔ)的情況下,是可以快速上手的。
2.4. 提升用戶(hù)體驗(yàn)度:用戶(hù)操作鏈路追蹤和分析
除了解決復(fù)雜度的問(wèn)題,基于狀態(tài)機(jī)的特性,還可以帶來(lái)一些新的思路,如用戶(hù)操作鏈路追蹤和分析。

2.4.1. 常見(jiàn)分析用戶(hù)操作鏈路方法
目前,針對(duì)于分析用戶(hù)操作鏈路的方法,主要是在頁(yè)面中的可操作標(biāo)簽上進(jìn)行埋點(diǎn),如,Button、Tab Item 等。有手動(dòng)埋點(diǎn)和自動(dòng)埋點(diǎn)。

手動(dòng)埋點(diǎn),可以按照你的意愿來(lái)收集特定區(qū)域的操作數(shù)據(jù),但成本偏高,需要一個(gè)一個(gè)的手動(dòng)接入,還可能需要自行上報(bào)數(shù)據(jù)。
自動(dòng)埋點(diǎn),通常是自動(dòng)在一些常用的標(biāo)簽上埋點(diǎn),但會(huì)存在具體的標(biāo)簽變更的問(wèn)題,且不能覆蓋所有可操作的區(qū)域,數(shù)據(jù)精度不夠。
無(wú)論使用哪種埋點(diǎn),都存在 回放噪音 的問(wèn)題。

如,上報(bào)信息里包含,“查看詳情” 按鈕的操作,那么對(duì)應(yīng)的 “詳情對(duì)話(huà)框” 一定會(huì)出來(lái)么?這個(gè)時(shí)候鏈路回放,只能去猜測(cè),認(rèn)為點(diǎn)擊了這個(gè)按鈕,就意味著這個(gè)對(duì)話(huà)框出來(lái)了。其實(shí)是不準(zhǔn)確的。

如果,頁(yè)面上新增加了一個(gè)功能,要判斷這個(gè)新功能用戶(hù)的使用量,及用戶(hù)做了哪些操作才找到這個(gè)新功能。通過(guò)這個(gè)數(shù)據(jù)來(lái)判斷新的交互設(shè)計(jì)是否存合理。在這種不精準(zhǔn)數(shù)據(jù)及 “噪音” 的回放中也是不準(zhǔn)確的。

同樣,分析頁(yè)面中的哪些部分是高頻操作,也有類(lèi)似的問(wèn)題。

2.4.2. 基于狀態(tài)機(jī)的鏈路分析方法
狀態(tài)機(jī)做這種用戶(hù)鏈路分析,是天然合適的。因?yàn)橛脩?hù)的所有操作,所有行為,本質(zhì)上就是 “狀態(tài)在接收了什么事件,要變換到什么狀態(tài)” 上的過(guò)程。這是在 View 上埋點(diǎn)的方式缺乏的。

我們只需要在每次 “狀態(tài)” 發(fā)生轉(zhuǎn)換時(shí),把狀態(tài)圖數(shù)據(jù)上報(bào)到分析平臺(tái)就可以。完全可以基于狀態(tài)的方式, 1:1 的回放用戶(hù)操作鏈路。

3. 總結(jié)
最后,總結(jié)一下?tīng)顟B(tài)機(jī)方式帶來(lái)的好處和不足。

3.1. 優(yōu)勢(shì)
比傳統(tǒng)的編碼方式,更容易理解。
基于行為建模,與視圖解耦。
更容易改變行為:組件中的行為被提取到了狀態(tài)機(jī)中,與 把行為和業(yè)務(wù)邏輯一起嵌入的組件相比,行為的更改相對(duì)容易。
更容易的理解代碼。
更容易測(cè)試
構(gòu)建狀態(tài)圖的過(guò)程必須探索所有狀態(tài),也是讓你具備業(yè)務(wù)全局視角的過(guò)程,它迫使你考慮所有可能發(fā)生的場(chǎng)景。
基于狀態(tài)圖的代碼比傳統(tǒng)代碼具有更少的 Bug 數(shù)。相關(guān)數(shù)據(jù)表示,錯(cuò)誤減少了 80% 到 90%,剩下的錯(cuò)誤也很少出現(xiàn)在狀態(tài)圖本身。
有助于處理可能會(huì)被忽視的特殊情況。
隨著復(fù)雜性的增加,狀態(tài)圖可以很好地?cái)U(kuò)展。
狀態(tài)圖是一個(gè)很好的交流工具。
3.2. 帶來(lái)的一些問(wèn)題
需要學(xué)習(xí)新的東西,狀態(tài)機(jī)是一種范式的轉(zhuǎn)化,且容易有抵觸心里,不愿意走出舒適圈。
新的格式
新的重構(gòu)技術(shù)
新的調(diào)試工具
部分人覺(jué)得可視化這種東西,沒(méi)什么用。
陌生的編碼方式,在團(tuán)隊(duì)內(nèi)可能出現(xiàn)不同的阻力。
雖然大多數(shù)人聽(tīng)過(guò)狀態(tài)機(jī),但實(shí)際的編程中離它遙遠(yuǎn),所以并不熟悉它。
編程方式的轉(zhuǎn)換,很多人需要弄清楚原來(lái)的代碼,現(xiàn)在該如何去寫(xiě),如何映射。
部分人會(huì)質(zhì)疑它的有效性。
必須有人基于這種模式實(shí)踐過(guò),對(duì)它非常了解才可以。
如果從來(lái)沒(méi)用過(guò)它,使用這種模式會(huì)無(wú)從下手,令人生畏。
3.3. 為什么用的人不多
狀態(tài)機(jī)已經(jīng)發(fā)展幾十年了,前面也說(shuō)過(guò),在非常的多場(chǎng)景有使用,像電子、嵌入式、游戲、通訊等領(lǐng)域。那為什么前端上使用較少呢(限定國(guó)內(nèi))?

除了上面列出的 “帶來(lái)的一些問(wèn)題” 中的一些點(diǎn),我覺(jué)的還有以下問(wèn)題導(dǎo)致的:

缺少指導(dǎo)圖書(shū):現(xiàn)在搜索一下關(guān)于狀態(tài)圖的前端圖書(shū)或者教程,搜索結(jié)果告訴你 0 條。資料很少(嵌入式之類(lèi)的狀態(tài)機(jī)資料還是挺多的)。
“用最簡(jiǎn)單的方式去實(shí)現(xiàn)” 的心態(tài):很多人喜歡用 if/else/switch 來(lái)解決問(wèn)題。
“你覺(jué)得你不需要” 的心態(tài):復(fù)雜度在每一個(gè) flag 變量和布爾值中蔓延。就像溫水煮青蛙,溫水中的青蛙不會(huì)注意到溫度的緩慢升高一樣,開(kāi)發(fā)人員也不會(huì)注意到復(fù)雜度的蔓延。在一些小的系統(tǒng)中運(yùn)行的很好,但隨著系統(tǒng)的迭代和變大,一個(gè)個(gè)凌亂的 if/else/switch 語(yǔ)句,它修改了各種變量的狀態(tài),以試圖維持它們的一致性。就好像你不需要狀態(tài)機(jī),直到為時(shí)已晚。
就像 RxJS、函數(shù)式編程之類(lèi)的一樣,大家都知道它很好,但就是不用它。
3.3. 總結(jié)
任何解決方案都不能解決一切問(wèn)題,一定要找到它適合的場(chǎng)景。不過(guò),現(xiàn)階段,狀態(tài)機(jī)確實(shí)是我能看到的,解決復(fù)雜業(yè)務(wù)邏輯最好的工具。

如果文中說(shuō)的問(wèn)題也發(fā)生在你身邊,且無(wú)法徹底解決,那推薦你可以嘗試一下,或許會(huì)有驚喜。

作者:lecepin


歡迎關(guān)注微信公眾號(hào) :前端印象