整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          “整潔架構(gòu)”和商家前端的重構(gòu)之路

          “整潔架構(gòu)”和商家前端的重構(gòu)之路

          |陳子煜 得物技術(shù)

          1. 背景

          團(tuán)隊(duì)歸屬于后方業(yè)務(wù)支撐部門,組內(nèi)的項(xiàng)目都以pc中后臺(tái)應(yīng)用為主。對(duì)比移動(dòng)端應(yīng)用,代碼庫比較龐大,業(yè)務(wù)邏輯也相對(duì)復(fù)雜。在持續(xù)的迭代過程中,我們發(fā)現(xiàn)當(dāng)前的代碼倉庫仍然有不少可以優(yōu)化的點(diǎn):

          • 可以減弱對(duì)ui框架的依賴

          21年前端平臺(tái)決定技術(shù)棧統(tǒng)一遷移到React生態(tài),后續(xù)平臺(tái)的基礎(chǔ)建設(shè)也都圍繞React展開,這就使得商家使用Vue生態(tài)做開發(fā)的系統(tǒng)面臨技術(shù)棧遷移的難題,將業(yè)務(wù)邏輯和UI框架節(jié)藕變得異常重要。

          • 代碼風(fēng)格可以更加統(tǒng)一

          隨著代碼量和團(tuán)隊(duì)成員的增加,應(yīng)用里風(fēng)格迥異的代碼也越來越多。為了能夠持續(xù)迅速的進(jìn)行迭代,團(tuán)隊(duì)急需一套統(tǒng)一的頂層代碼架構(gòu)設(shè)計(jì)方案。

          • 可以集成自動(dòng)化測(cè)試用例

          隨著業(yè)務(wù)變得越來越復(fù)雜,在迅速的迭代過程中團(tuán)隊(duì)需要頻繁地對(duì)功能進(jìn)行回歸,因此我們對(duì)于自動(dòng)化單測(cè)用例的訴求也變的越來越強(qiáng)烈。

          為了完成以上的優(yōu)化,四組對(duì)現(xiàn)有的應(yīng)用架構(gòu)做了一次重構(gòu),而重構(gòu)的核心就是整潔架構(gòu)。

          2. 整潔架構(gòu)(The Clean Architecture)

          整潔架構(gòu)(The clean architecture)是由 Robert C. Martin (Uncle Bob)在2012年提出的一套代碼組織的理念,其核心主要是依據(jù)各部分代碼作用的不同將其拆分成不同的層次,在各層次間制定了明確的依賴原則,以達(dá)到以下目的:

          1. 與框架無關(guān):無論是前端代碼還是服務(wù)端代碼,其邏輯本身都應(yīng)該是獨(dú)立的,不應(yīng)該依賴于某一個(gè)第三方框架或工具庫。一套獨(dú)立的代碼可以把第三方框架等作為工具使用。
          2. 可測(cè)試:代碼中的業(yè)務(wù)邏輯可以在不依賴ui、數(shù)據(jù)庫、服務(wù)器的情況下進(jìn)行測(cè)試
          3. 和ui無關(guān):代碼中的業(yè)務(wù)邏輯不應(yīng)該和ui做強(qiáng)綁定。比如把一個(gè)web應(yīng)用切換成桌面應(yīng)用,業(yè)務(wù)邏輯不應(yīng)該受到影響。
          4. 和數(shù)據(jù)庫無關(guān):無論數(shù)據(jù)庫用的是mysql還是mongodb,無論其怎么變,都不該影響到業(yè)務(wù)邏輯。
          5. 和外部服務(wù)無關(guān):無論外部服務(wù)怎么變,都不影響到使用該服務(wù)的業(yè)務(wù)邏輯。

          為了實(shí)現(xiàn)以上目的,整潔架構(gòu)把應(yīng)用劃分成了entities、use cases、interface adapters(MVC、MVP等)、Web/DB等至少四層。這套架構(gòu)除了分層之外,在層與層之間還有一個(gè)非常明確的依賴關(guān)系,外層的邏輯依賴內(nèi)層的邏輯

          Entity

          entities封裝了企業(yè)級(jí)的業(yè)務(wù)邏輯和規(guī)則。entities沒有什么固定的形式,無論是一個(gè)對(duì)象也好,是一堆函數(shù)的集合也好,唯一的標(biāo)準(zhǔn)就是能夠被企業(yè)的各個(gè)應(yīng)用所復(fù)用。

          Use Case

          entities封裝了企業(yè)里最通用的一部分邏輯,而應(yīng)用各自的業(yè)務(wù)邏輯就都封裝在use case里面。日常開發(fā)中最常見的對(duì)于某個(gè)模型的crud操作就屬于usecase這一層。

          Interface Adapter

          這一層類似于膠水層,需要負(fù)責(zé)內(nèi)圈的entity和use case同外圈的external interfaces之間的數(shù)據(jù)轉(zhuǎn)化。需要把外層服務(wù)的數(shù)據(jù)轉(zhuǎn)化成內(nèi)層entity和usecase可以消費(fèi)的數(shù)據(jù),反之亦然。如上面圖上畫的,這一層有時(shí)候可能很簡(jiǎn)單(一個(gè)轉(zhuǎn)化函數(shù)), 有時(shí)候可能復(fù)雜到包含一整個(gè)MVC/MVP的架構(gòu)。

          External Interfaces

          我們需要依賴的外部服務(wù),第三方框架,以及需要糊的頁面UI都?xì)w屬在這一層。這一層完全不感知內(nèi)圈的任何邏輯,所以無論這一層怎么變(ui變化),都不應(yīng)該影響到內(nèi)圈的應(yīng)用層邏輯(usecase)和企業(yè)級(jí)邏輯(entity)。

          依賴原則

          在整潔架構(gòu)的原始設(shè)計(jì)中,并不是強(qiáng)制一定只能寫這么四層,根據(jù)業(yè)務(wù)的需要還可以拆分的更細(xì)。不過無論怎么拆,都需要遵守前面提到的從外至內(nèi)的依賴原則。即entity作為企業(yè)級(jí)的通用邏輯,不能依賴任何模塊。而外層的ui等則可以使用usecase、entity。

          3. 重構(gòu)

          前面介紹了當(dāng)前代碼庫目前的一些具體問題,而整潔架構(gòu)的理念正好可以幫助我們優(yōu)化代碼可維護(hù)性。

          作為前端,我們的業(yè)務(wù)邏輯不應(yīng)該依賴視圖層(ui框架及其生態(tài)),同時(shí)應(yīng)當(dāng)保證業(yè)務(wù)邏輯的獨(dú)立性和可復(fù)用性(usecase & entity)。最后,作為數(shù)據(jù)驅(qū)動(dòng)的端應(yīng)用,要保證應(yīng)用視圖渲染和業(yè)務(wù)邏輯等不受數(shù)據(jù)變動(dòng)的影響(adapter & entity)。

          根據(jù)以上的思考,我們對(duì)“整潔架構(gòu)”做了如下落地。

          Entities

          對(duì)于前端應(yīng)用來說,在entity層我們只需要將服務(wù)端的生數(shù)據(jù)做一層簡(jiǎn)單的抽象,生成一個(gè)貧血對(duì)象給后續(xù)的渲染和交互邏輯使用。

          
          interface IRawOrder {
            amount: number
            barCode: string
            orderNo: string
            orderType: string
            skuId: number
            deliveryTime: number
            orderTime: number
            productImg: string
            status: number
          }
          
          export default function buildMakeOrder({
            formatTimestamp,
            formatImageUrl,
          }: {
            formatTimestamp: (timestamp: number, format?: string)=> string
            formatImageUrl: (
              image: string,
              config?: { width: number; height: number },
            )=> string
          }) {
            return function makeOrder(raw?: IRawOrder) {
              if (!raw || !raw.orderNo) {
                 Monitor.warn('臟數(shù)據(jù)')
                 return null;
              }
              return {
                amount: raw.amount,
                barCode: raw.barCode,
                orderNo: raw.orderNo,
                orderType: raw.orderType,
                skuId: raw.skuId,
                status: raw.status,
                statusDescription: selectStatusDescription(raw.status),
                deliveryTime: formatTimestamp(raw.deliveryTime),
                orderTime: formatTimestamp(raw.orderTime),
                productImg: formatImageUrl(raw.productImg),
              }
            }
          }
          
          function selectStatusDescription(status: number): string {
            switch (status) {
              case 0:
                return '待支付'
              case 1:
                return '待發(fā)貨'
              case 2:
                return '待收貨'
              case 3:
                return '已完成'
              default:
                return ''
            }
          }

          以上是商家后臺(tái)訂單模型的entity工廠函數(shù),工廠主要負(fù)責(zé)對(duì)服務(wù)端返回的生數(shù)據(jù)進(jìn)行加工處理,讓其滿足渲染層和邏輯層的要求。除了抽象數(shù)據(jù)之外,可以看到在entity工廠還對(duì)數(shù)據(jù)進(jìn)行了校驗(yàn),將臟數(shù)據(jù)、不符合預(yù)期的數(shù)據(jù)全部處理掉或者進(jìn)行兜底(具體操作要看業(yè)務(wù)場(chǎng)景)。

          有一點(diǎn)需要注意的是,在設(shè)計(jì)entity的時(shí)候(尤其是基礎(chǔ)entity)需要考慮復(fù)用性。舉個(gè)例子,在上面orderEntity的基礎(chǔ)上,我們通過簡(jiǎn)單的組合就可以生成一個(gè)虛擬商品訂單entity:

          import { makeOrder } from '@/entities'
          
          export default function buildMakeVirtualOrder() {
            return function makeVirtualOrder(raw?: IRawPresaleOrder) {
               const order=makeOrder(raw)
          
               if(! order || !raw.virtualOrderType) {
                   Monitor.warn('臟數(shù)據(jù)')
                   return null
               }
          
               return {
                   ...order,
                   virtualOrderType: raw.virtualOrderType,
                   virtualOrderDesc: selectVirtualOrderDesc(raw.virtualOrderType)
               }
            }
          }

          如此一來,我們就通過entity層達(dá)到了2個(gè)目的:

          1. 把前端的邏輯和服務(wù)端接口數(shù)據(jù)隔離開,無論服務(wù)端怎么變,前端后續(xù)的渲染、業(yè)務(wù)代碼不需要變,我們只需要變更entitiy工廠函數(shù);并且經(jīng)過entity層處理過后,所有流入后續(xù)渲染&交互邏輯的數(shù)據(jù)都是可靠的;對(duì)于部分異常數(shù)據(jù),前端應(yīng)用可以第一時(shí)間發(fā)現(xiàn)并報(bào)警。
          2. 通過對(duì)業(yè)務(wù)模型進(jìn)行抽象,實(shí)現(xiàn)了模塊間的組合、復(fù)用。另外,抽象出的entity對(duì)代碼的維護(hù)性也有非常大的幫助,開發(fā)者可以非常直觀的知道所使用的entity所包含的所有字段。

          Usecase

          usecase這一層即是圍繞entity展開的一系列crud操作,以及為了頁面渲染做的一些聯(lián)動(dòng)(通過ui store實(shí)現(xiàn))。由于當(dāng)前架構(gòu)的原因(沒有bff層),usecase還可能承擔(dān)部分微服務(wù)串聯(lián)的工作。

          舉個(gè)例子,商家后臺(tái)訂單頁面在渲染前有一堆準(zhǔn)備邏輯:

          1. 根據(jù)route的query參數(shù)以及一些商家類型參數(shù)來決定默認(rèn)選中哪個(gè)tab
          1. 根據(jù)是國內(nèi)商家還是境外商家,調(diào)用對(duì)應(yīng)的供應(yīng)商接口來更新供應(yīng)商下拉框

          現(xiàn)在大致的實(shí)現(xiàn)是:

          {
              mounted() {
                  const { subType }=this.$route.query
                  /*
                      7-15行處理了幾種分支鏈路場(chǎng)景下對(duì)subType的賦值問題
                  */
                  if (Number(subType)===0 || subType) {
                    this.subType=subType.toString()
                  } else {
                    if (this.user.merchant.typeId===4) {
                      this.subType=this.tabType.cross
                    } else {
                      this.subType=this.tabType.ordinarySpot
                    }
                  }
          
                  /*
                      getAllLogisticsCarrier有沒有對(duì)subType賦值呢?光看這段代碼完全不確定
                  */
                  this.getAllLogisticsCarrier()
                  /*
                      21-22行又多出來一個(gè)分支需要對(duì)subType進(jìn)行再次賦值
                  */
                  if (this.isPersonPermission && !this.crossUser) {
                    this.subType=this.tabType.warehouse
                  }
              },
          
              getAllLogisticsCarrier() {
                  let getCarrier=API.getAllLogisticsCarrier
                  if (this.crossUser) {
                    getCarrier=API.getOrderShipAllLogistics
                  }
          
                  getCarrier({}).then(res=> {
                    if (res.code===200) {
                      const options=[]
          
                      .......... // 給options賦值
          
                      this.options2=options
          
                    }
                  })
              },
          }

          我們能看到7-15、24-125行對(duì)this.subType進(jìn)行了賦值。但由于我們無法確定20行的函數(shù)是否也對(duì)this.subType進(jìn)行了賦值,所以光憑mounted函數(shù)的代碼我們并不能完全確定subType的值究竟是什么,需要跳轉(zhuǎn)到getAllLogisticsCarrier函數(shù)確認(rèn)。這段代碼在這里已經(jīng)做了簡(jiǎn)化,實(shí)際的代碼像getAllLogisticsCarrier這樣的調(diào)用還有好幾個(gè),要想搞清楚邏輯就得把所有函數(shù)全看一遍,代碼的可讀性一般。同時(shí),由于函數(shù)都封裝在ui組件里,因此要想給函數(shù)覆蓋單測(cè)的話也需要一些改造。

          為了解決問題,我們將這部分邏輯都拆分到usecase層:

          // prepare-order-page.ts
          import { tabType } from '@/constants'
          
          interface IParams {
            subType?: number
            merchantType: number
            isCrossUser: boolean
            isPersonPermission: boolean
          }
          
          /*
              做依賴倒置主要是為了方便后續(xù)的單測(cè)和復(fù)用
          */
          export default function buildPrepareOrderPage({
            queryLogisticsCarriers,
          }: {
            queryLogisticsCarriers: ()=> Promise<{ carriers: ICarrires }>
          }) {
            return async function prepareOrderPage(params: IParams) {
              const activeTab=selectActiveTab(params)
          
              const { carriers }=queryLogisticsCarriers(params.isCrossUser)
          
              return {
                activeTab,
                carriers,
              }
            }
          }
          
          function selectActiveTab({
            subType,
            isCrossUser,
            isPersonPermission,
            merchantType,
          }: IParams) {
            if (isPersonPermission && !isCrossUser) {
              return tabType.warehouse
            }
          
            if (Number(subType)===0 || subType) {
              return subType.toString()
            }
          
            if (merchantType===4) {
              return tabType.cross
            }
          
            return tabType.ordinarySpot
          }
          // query-logistics-carriers
          export default function buildQueryLogisticsCarriers({
            fetchAllLogisticsCarrier,
            fetchOrderShipAllLogistics,
          }: {
            fetchAllLogisticsCarrier: ()=> Promise<{ data: {carriers: ICarrires }}>
            fetchOrderShipAllLogistics: ()=> Promise<{ data: {carriers: ICarrires }}>
          }) {
            return async function queryLogisticsCarriers(isCrossUser: boolean) {
              if (isCrossUser) {
                return fetchAllLogisticsCarrier()
              }
          
              return fetchOrderShipAllLogistics()
            }
          }
          
          // index.vue
          {
              mounted() {
                  const {activeTab, carriers}=prepareOrderPage(params)
          
                  this.subType=activeTab;
                  this.options=buildCarrierOptions(carriers) // 將carries轉(zhuǎn)換成下拉框option
              }
          }

          首先,可以看到所有usecase一定是一個(gè)純函數(shù),不會(huì)存在副作用的問題。

          其次,prepareOrderPage usecase專門為訂單頁定制,拆分后一眼就能看出來訂單頁的準(zhǔn)備工作需要干決定選中的tab和拉取供應(yīng)商列表兩件事情。而另一個(gè)拆分出來的queryLogisticsCarriers則是封裝了商家后臺(tái)跨境、國內(nèi)兩種邏輯,后續(xù)無論跨境還是國內(nèi)的邏輯如何變更,其影響范圍被限制在了queryLogisticsCarriers函數(shù),我們需要對(duì)其進(jìn)行功能回歸;而對(duì)于prepareOrderPage來說,queryLogisticsCarriers只是()=> Promise<{ carriers: ICarrires }>的一個(gè)實(shí)現(xiàn)而已,其內(nèi)部調(diào)用queryLogisticsCarriers的邏輯完全不受影響,不需要進(jìn)行回歸。

          最后,而由于我們做了依賴倒置,我們可以非常容易的給usecase覆蓋單測(cè):

          
          import buildPrepareOrderPage from '@/utils/create-goods';
          
          function init() {
            const queryLogisticsCarriers=jest.fn();
          
            const prepareOrderPage=buildPrepareOrderPage({ queryLogisticsCarriers });
          
            return {
              prepareOrderPage,
              queryLogisticsCarriers,
            };
          }
          
          describe('訂單頁準(zhǔn)備邏輯', ()=> {
            it('當(dāng)用戶是國內(nèi)商家且在入倉白名單上,在打開訂單頁時(shí),默認(rèn)打開入倉tab', async ()=> {
              const { prepareOrderPage }=init();
              const params={
                  merchantType: 2
                  isCrossUser: false
                  isPersonPermission: true
              }
          
              const { activeTab }=await prepareOrderPage(params)
          
              expect(activeTab).toEqual({tabType.warehouse});
            });
          
             it('當(dāng)用戶是跨境商家,在打開訂單頁時(shí),默認(rèn)打開跨境tab', async ()=> {
              const { prepareOrderPage }=init();
              const params={
                  merchantType: 4
                  isCrossUser: true
                  isPersonPermission: true
              }
          
              const { activeTab }=await prepareOrderPage(params)
          
              expect(activeTab).toEqual({tabType.cross});
            });
          
            ......
          });

          單測(cè)除了進(jìn)行功能回歸之外,它的描述(demo里使用了Given-When-Then的格式,由于篇幅的原因,關(guān)于單測(cè)的細(xì)節(jié)在后續(xù)的文章再進(jìn)行介紹)對(duì)于了解代碼的邏輯非常非常非常有幫助。由于單測(cè)和代碼邏輯強(qiáng)行綁定的緣故,我們甚至可以將單測(cè)描述當(dāng)成一份實(shí)時(shí)更新的業(yè)務(wù)文檔。

          除了方便寫單測(cè)之外,在通過usecase拆分完成之后,ui組件真正成為了只負(fù)責(zé)“ui”和監(jiān)聽用戶交互行為的組件,這為我們后續(xù)的React技術(shù)棧遷移奠定了基礎(chǔ);通過usecase我們也實(shí)現(xiàn)了很不錯(cuò)的模塊化,對(duì)于使用比較多的一些entity,他的crud操作可以通過獨(dú)立的usecase具備了在多個(gè)頁面甚至應(yīng)用間復(fù)用的能力。

          Adapter

          上面usecase例子中的fetchAllLogisticsCarrier就是一個(gè)adapter,這一層起到的作用是將外部系統(tǒng)返回的數(shù)據(jù)轉(zhuǎn)化成entity,并以一種統(tǒng)一的數(shù)據(jù)格式返回回來。

          這一層很核心的一點(diǎn)即是可以依賴entity的工廠函數(shù),將接口返回的數(shù)據(jù)轉(zhuǎn)化成前端自己設(shè)計(jì)的模型數(shù)據(jù),保證流入usecase和ui層的數(shù)據(jù)都是經(jīng)過處理的“干凈數(shù)據(jù)”。除此之外,通常在這一層我們會(huì)用一種固定的數(shù)據(jù)格式返回?cái)?shù)據(jù),比如例子中的 {success: boolean, data?: any}。這樣做主要是為了抹平對(duì)接多個(gè)系統(tǒng)帶來的差異性,同時(shí)減少多人協(xié)作時(shí)的溝通成本。

          
          type Request=(url: string, params: Record<string, any>)=> Promise<any>;
          import makeCarrier from '@/entities/makeCarrier'
          
          
          export default function buildFetchAllLogisticsCarrier({request}: {request: Request}) {
            return async function fetchAllLogisticsCarrier() {
              // TODO: 異常處理
              const response=await request('/fakeapi', info)
          
              if (!response || !resposne.code===200) {
                  return { 
                      success: false
                  }
              }
          
              return {
                success: true,
                data: {
                    carriers: response.list?.map(makeCarrier)
                }
              }
            }
          }

          通過Adapter + entity的組合,我們基本形成了前端應(yīng)用和后端服務(wù)之間的防腐層,使得前端可以在完全不清楚接口定義的情況下完成ui渲染、usecase等邏輯的開發(fā)。在服務(wù)端產(chǎn)出定義后,前端只需要將實(shí)際接口返回適配到自己定義的模型(通過entity)即可。這一點(diǎn)對(duì)前端的測(cè)試周提效非常非常非常重要,因?yàn)榉栏瘜拥拇嬖冢覀兛梢栽跍y(cè)試周完成需求評(píng)審之后根據(jù)prd的內(nèi)容設(shè)計(jì)出業(yè)務(wù)模型,并以此完成需求開發(fā),在真正進(jìn)入研發(fā)周后只需要和服務(wù)端對(duì)接完成adapter這一層的適配即可。

          在實(shí)踐過程中,我們發(fā)現(xiàn)在對(duì)接同一個(gè)系統(tǒng)的時(shí)候(對(duì)商家來說就是stark服務(wù))各個(gè)adapter對(duì)于異常的處理幾乎一模一樣(上述的11-15行),我們可以通過Proxy對(duì)其進(jìn)行抽離實(shí)現(xiàn)復(fù)用。當(dāng)然,后續(xù)我們也完全有機(jī)會(huì)根據(jù)接口定義來自動(dòng)生成adapter。

          UI

          在經(jīng)過前面的拆分之后,無論咱們的UI層用React還是Vue來寫,要做的工作都很簡(jiǎn)單了:

          1. 監(jiān)聽交互事件并調(diào)用對(duì)應(yīng)的usecase來進(jìn)行響應(yīng)
          1. 通過usecase來獲取entity數(shù)據(jù)進(jìn)行渲染

          由于entity已經(jīng)做了過濾和適配處理,所以在ui層我們可以放心大膽的用,不需要再寫一堆莫名其妙的判斷邏輯。另外由于entity是由前端自己定義的模型,無論開發(fā)過程中服務(wù)端接口怎么變,受影響的都只有entity工廠函數(shù),ui層不會(huì)受到影響。

          最后,在ui層我們還剩下令人頭痛的技術(shù)棧遷移問題。整個(gè)團(tuán)隊(duì)目前使用vue的項(xiàng)目有10個(gè),按迭代頻率和項(xiàng)目規(guī)模遷移的方案可以分為兩類:

          • 迭代頻繁的大應(yīng)用:主要包括代碼行數(shù)較多、邏輯較為復(fù)雜的幾個(gè)中大型應(yīng)用。這些應(yīng)用想要一把梭直接完成遷移成本極高,但同時(shí)每個(gè)迭代又有相當(dāng)?shù)男枨蟆;谶@種情況,對(duì)于這三個(gè)應(yīng)用我們采取了微前端的方式進(jìn)行遷移。每個(gè)應(yīng)用分別起一個(gè)對(duì)應(yīng)的React應(yīng)用,對(duì)于新頁面以及部分邏輯已經(jīng)完全和ui解藕遷移成本不高的業(yè)務(wù),都由React應(yīng)用來承接,最后通過module federation的方式實(shí)現(xiàn)融合。
          • 迭代不頻繁的小應(yīng)用:剩下的應(yīng)用均是復(fù)雜度不高的小應(yīng)用,這部分應(yīng)用迭代的需求不多,以維護(hù)為主。因此我們的方案是對(duì)現(xiàn)有邏輯進(jìn)行整潔架構(gòu)重構(gòu),在ui和邏輯分層之后直接對(duì)ui層進(jìn)行替換完成遷移。

          4. 后續(xù)

          通過整潔架構(gòu)我們形成了統(tǒng)一的編碼規(guī)范,在前端應(yīng)用標(biāo)準(zhǔn)化的道路上邁下了堅(jiān)實(shí)的一步。可以預(yù)見的是整個(gè)標(biāo)準(zhǔn)化的過程會(huì)非常漫長(zhǎng),我們會(huì)陸續(xù)往標(biāo)準(zhǔn)中增加新的規(guī)范使其更加完善,短期內(nèi)在規(guī)劃中的有:

          • 單測(cè)即文檔:上面提到了usecase通過依賴倒置來配合單測(cè)落地,后續(xù)團(tuán)隊(duì)期望將一些業(yè)務(wù)邏輯的實(shí)現(xiàn)細(xì)則通過單測(cè)的描述來進(jìn)行沉淀,解決業(yè)務(wù)文檔實(shí)時(shí)性的問題。
          • 完善監(jiān)控體系:前端常遇到的3種異常包括 代碼邏輯異常、性能瓶頸(渲染卡頓、內(nèi)存不足等)、數(shù)據(jù)導(dǎo)致異常。對(duì)于數(shù)據(jù)異常,我們可以在entity層映射的過程中加入對(duì)異常數(shù)據(jù)的埋點(diǎn)上報(bào)來填補(bǔ)目前監(jiān)控的空白。(代碼邏輯異常通過sentry已經(jīng)監(jiān)控,性能監(jiān)控對(duì)于中后臺(tái)應(yīng)用不需要)

          后續(xù)在標(biāo)準(zhǔn)逐漸穩(wěn)定之后,我們也期望基于穩(wěn)定的規(guī)范進(jìn)行一些工程化的實(shí)踐(比如根據(jù)mooncake文檔自動(dòng)生成adapter層、基于usecase實(shí)現(xiàn)功能開關(guān)等),敬請(qǐng)期待。

          參考鏈接:

          The Clean Architecture:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

          Module Federationhttps://webpack.js.org/concepts/module-federation/

          Anti-corruption Layer pattern:https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer


          *文/陳子煜

          關(guān)注得物技術(shù),每周一三五晚18:30更新技術(shù)干貨
          要是覺得文章對(duì)你有幫助的話,歡迎評(píng)論轉(zhuǎn)發(fā)點(diǎn)贊~

          譯自: https://opensource.com/article/18/10/book-to-website-epub-using-pandoc

          作者: Kiko Fernandez-reyes

          譯者: jlztan

          通過 Markdown 和 Pandoc,可以做到編寫一次,發(fā)布兩次。

          Pandoc 是一個(gè)命令行工具,用于將文件從一種標(biāo)記語言轉(zhuǎn)換為另一種標(biāo)記語言。在我 對(duì) Pandoc 的簡(jiǎn)介 一文中,我演示了如何把 Markdown 編寫的文本轉(zhuǎn)換為網(wǎng)頁、幻燈片和 PDF。

          在這篇后續(xù)文章中,我將深入探討 Pandoc ,展示如何從同一個(gè) Markdown 源文件生成網(wǎng)頁和 ePub 格式的電子書。我將使用我即將發(fā)布的電子書《 面向?qū)ο笏枷氲?GRASP 原則 》為例進(jìn)行講解,這本電子書正是通過以下過程創(chuàng)建的。

          首先,我將解釋這本書使用的文件結(jié)構(gòu),然后介紹如何使用 Pandoc 生成網(wǎng)頁并將其部署在 GitHub 上;最后,我演示了如何生成對(duì)應(yīng)的 ePub 格式電子書。

          你可以在我的 GitHub 倉庫 Programming Fight Club 中找到相應(yīng)代碼。

          設(shè)置圖書結(jié)構(gòu)

          我用 Markdown 語法完成了所有的寫作,你也可以使用 HTML 標(biāo)記,但是當(dāng) Pandoc 將 Markdown 轉(zhuǎn)換為 ePub 文檔時(shí),引入的 HTML 標(biāo)記越多,出現(xiàn)問題的風(fēng)險(xiǎn)就越高。我的書按照每章一個(gè)文件的形式進(jìn)行組織,用 Markdown 的 H1 標(biāo)記(#)聲明每章的標(biāo)題。你也可以在每個(gè)文件中放置多個(gè)章節(jié),但將它們放在單獨(dú)的文件中可以更輕松地查找內(nèi)容并在以后進(jìn)行更新。

          元信息遵循類似的模式,每種輸出格式都有自己的元信息文件。元信息文件定義有關(guān)文檔的信息,例如要添加到 HTML 中的文本或 ePub 的許可證。我將所有 Markdown 文檔存儲(chǔ)在名為 parts 的文件夾中(這對(duì)于用來生成網(wǎng)頁和 ePub 的 Makefile 非常重要)。下面以一個(gè)例子進(jìn)行說明,讓我們看一下目錄,前言和關(guān)于本書(分為 toc.md、preface.md 和 about.md 三個(gè)文件)這三部分,為清楚起見,我們將省略其余的章節(jié)。

          關(guān)于本書這部分內(nèi)容的開頭部分類似:

          # About this book {-}

          ## Who should read this book {-}

          Before creating a complex software system one needs to create a solid foundation.

          General Responsibility Assignment Software Principles (GRASP) are guidelines to assign

          responsibilities to software classes in object-oriented programming.

          每一章完成后,下一步就是添加元信息來設(shè)置網(wǎng)頁和 ePub 的格式。

          生成網(wǎng)頁

          創(chuàng)建 HTML 元信息文件

          我創(chuàng)建的網(wǎng)頁的元信息文件(web-metadata.yaml)是一個(gè)簡(jiǎn)單的 YAML 文件,其中包含 <head> 標(biāo)簽中的作者、標(biāo)題、和版權(quán)等信息,以及 HTML 文件中開頭和結(jié)尾的內(nèi)容。

          我建議(至少)包括 web-metadata.yaml 文件中的以下字段:

          ---

          title: <a href="/grasp-principles/toc/">GRASP principles for the Object-oriented mind</a>

          author: Kiko Fernandez-Reyes

          rights: 2017 Kiko Fernandez-Reyes, CC-BY-NC-SA 4.0 International

          header-includes:

          - |

          ```{=html}

          <link rel="stylesheet">

          <link rel="stylesheet">

          ```

          include-before:

          - |

          ```{=html}

          <p>If you like this book, please consider

          spreading the word or

          <a >

          buying me a coffee

          </a>

          </p>

          ```

          include-after:

          - |

          ```{=html}

          <div class="footnotes">

          <hr>

          <div class="container">

          <nav class="pagination" role="pagination">

          <ul>

          <p>

          <span class="page-number">Designed with</span> ?? <span class="page-number"> from Uppsala, Sweden</span>

          </p>

          <p>

          <a rel="license" ><img alt="Creative Commons License" style="border-width:0" src="http://www.jungjaehyung.com/uploadfile/2024/1009/20241009093826494.png" /></a>

          </p>

          </ul>

          </nav>

          </div>

          </div>

          ```

          ---

          下面幾個(gè)變量需要注意一下:

          • header-includes 變量包含將要嵌入 <head> 標(biāo)簽的 HTML 文本。
          • 調(diào)用變量后的下一行必須是 - |。再往下一行必須以與 | 對(duì)齊的三個(gè)反引號(hào)開始,否則 Pandoc 將無法識(shí)別。{=html} 告訴 Pandoc 其中的內(nèi)容是原始文本,不應(yīng)該作為 Markdown 處理。(為此,需要檢查 Pandoc 中的 raw_attribute 擴(kuò)展是否已啟用。要進(jìn)行此檢查,鍵入 pandoc --list-extensions | grep raw 并確保返回的列表包含名為 + raw_html 的項(xiàng)目,加號(hào)表示已啟用。)
          • 變量 include-before 在網(wǎng)頁開頭添加一些 HTML 文本,此處我請(qǐng)求讀者幫忙宣傳我的書或給我打賞。
          • include-after 變量在網(wǎng)頁末尾添加原始 HTML 文本,同時(shí)顯示我的圖書許可證。

          這些只是其中一部分可用的變量,查看 HTML 中的模板變量(我的文章 Pandoc簡(jiǎn)介 中介紹了如何查看 LaTeX 的模版變量,查看 HTML 模版變量的過程是相同的)對(duì)其余變量進(jìn)行了解。

          將網(wǎng)頁分成多章

          網(wǎng)頁可以作為一個(gè)整體生成,這會(huì)產(chǎn)生一個(gè)包含所有內(nèi)容的長(zhǎng)頁面;也可以分成多章,我認(rèn)為這樣會(huì)更容易閱讀。我將解釋如何將網(wǎng)頁劃分為多章,以便讀者不會(huì)被長(zhǎng)網(wǎng)頁嚇到。

          為了使網(wǎng)頁易于在 GitHub Pages 上部署,需要?jiǎng)?chuàng)建一個(gè)名為 docs 的根文件夾(這是 GitHub Pages 默認(rèn)用于渲染網(wǎng)頁的根文件夾)。然后我們需要為 docs 下的每一章創(chuàng)建文件夾,將 HTML 內(nèi)容放在各自的文件夾中,將文件內(nèi)容放在名為 index.html 的文件中。

          例如,about.md 文件將轉(zhuǎn)換成名為 index.html 的文件,該文件位于名為 about(about/index.html)的文件夾中。這樣,當(dāng)用戶鍵入 http://<your-website.com>/about/ 時(shí),文件夾中的 index.html 文件將顯示在其瀏覽器中。

          下面的 Makefile 將執(zhí)行上述所有操作:

          # Your book files

          DEPENDENCIES=toc preface about

          # Placement of your HTML files

          DOCS=docs

          all: web

          web: setup $(DEPENDENCIES)

          @cp $(DOCS)/toc/index.html $(DOCS)

          # Creation and copy of stylesheet and images into

          # the assets folder. This is important to deploy the

          # website to Github Pages.

          setup:

          @mkdir -p $(DOCS)

          @cp -r assets $(DOCS)

          # Creation of folder and index.html file on a

          # per-chapter basis

          $(DEPENDENCIES):

          @mkdir -p $(DOCS)/$@

          @pandoc -s --toc web-metadata.yaml parts/$@.md \

          -c /assets/pandoc.css -o $(DOCS)/$@/index.html

          clean:

          @rm -rf $(DOCS)

          .PHONY: all clean web setup

          選項(xiàng) - c /assets/pandoc.css 聲明要使用的 CSS 樣式表,它將從 /assets/pandoc.cs 中獲取。也就是說,在 <head> 標(biāo)簽內(nèi),Pandoc 會(huì)添加這樣一行:

          <link rel="stylesheet" href="/assets/pandoc.css">

          使用下面的命令生成網(wǎng)頁:

          make

          根文件夾現(xiàn)在應(yīng)該包含如下所示的文件結(jié)構(gòu):

          .---parts

          | |--- toc.md

          | |--- preface.md

          | |--- about.md

          |

          |---docs

          |--- assets/

          |--- index.html

          |--- toc

          | |--- index.html

          |

          |--- preface

          | |--- index.html

          |

          |--- about

          |--- index.html

          部署網(wǎng)頁

          通過以下步驟將網(wǎng)頁部署到 GitHub 上:

          1. 創(chuàng)建一個(gè)新的 GitHub 倉庫
          2. 將內(nèi)容推送到新創(chuàng)建的倉庫
          3. 找到倉庫設(shè)置中的 GitHub Pages 部分,選擇 Source 選項(xiàng)讓 GitHub 使用主分支的內(nèi)容

          你可以在 GitHub Pages 的網(wǎng)站上獲得更多詳細(xì)信息。

          我的書的網(wǎng)頁 便是通過上述過程生成的,可以在網(wǎng)頁上查看結(jié)果。

          生成電子書

          創(chuàng)建 ePub 格式的元信息文件

          ePub 格式的元信息文件 epub-meta.yaml 和 HTML 元信息文件是類似的。主要區(qū)別在于 ePub 提供了其他模板變量,例如 publisher 和 cover-image 。ePub 格式圖書的樣式表可能與網(wǎng)頁所用的不同,在這里我使用一個(gè)名為 epub.css 的樣式表。

          ---

          title: 'GRASP principles for the Object-oriented Mind'

          publisher: 'Programming Language Fight Club'

          author: Kiko Fernandez-Reyes

          rights: 2017 Kiko Fernandez-Reyes, CC-BY-NC-SA 4.0 International

          cover-image: assets/cover.png

          stylesheet: assets/epub.css

          ...

          將以下內(nèi)容添加到之前的 Makefile 中:

          epub:

          @pandoc -s --toc epub-meta.yaml \

          $(addprefix parts/, $(DEPENDENCIES:=.md)) -o $(DOCS)/assets/book.epub

          用于產(chǎn)生 ePub 格式圖書的命令從 HTML 版本獲取所有依賴項(xiàng)(每章的名稱),向它們添加 Markdown 擴(kuò)展,并在它們前面加上每一章的文件夾路徑,以便讓 Pandoc 知道如何進(jìn)行處理。例如,如果 $(DEPENDENCIES 變量只包含 “前言” 和 “關(guān)于本書” 兩章,那么 Makefile 將會(huì)這樣調(diào)用:

          @pandoc -s --toc epub-meta.yaml \

          parts/preface.md parts/about.md -o $(DOCS)/assets/book.epub

          Pandoc 將提取這兩章的內(nèi)容,然后進(jìn)行組合,最后生成 ePub 格式的電子書,并放在 Assets 文件夾中。

          這是使用此過程創(chuàng)建 ePub 格式電子書的一個(gè) 示例 。

          過程總結(jié)

          從 Markdown 文件創(chuàng)建網(wǎng)頁和 ePub 格式電子書的過程并不困難,但有很多細(xì)節(jié)需要注意。遵循以下大綱可能使你更容易使用 Pandoc。

          • HTML 圖書:
          • 使用 Markdown 語法創(chuàng)建每章內(nèi)容
          • 添加元信息
          • 創(chuàng)建一個(gè) Makefile 將各個(gè)部分組合在一起
          • 設(shè)置 GitHub Pages
          • 部署
          • ePub 電子書:
          • 使用之前創(chuàng)建的每一章內(nèi)容
          • 添加新的元信息文件
          • 創(chuàng)建一個(gè) Makefile 以將各個(gè)部分組合在一起
          • 設(shè)置 GitHub Pages
          • 部署

          via: https://opensource.com/article/18/10/book-to-website-epub-using-pandoc

          作者: Kiko Fernandez-Reyes 選題: lujun9972 譯者: jlztan 校對(duì): wxy

          本文由 LCTT 原創(chuàng)編譯, Linux中國 榮譽(yù)推出

          點(diǎn)擊“了解更多”可訪問文內(nèi)鏈接

          作者:xybaby 
          來源:https://www.cnblogs.com/xybaby/p/11335829.html
          

          出整潔的代碼,是每個(gè)程序員的追求。

          《clean code》指出,要想寫出好的代碼,首先得知道什么是骯臟代碼、什么是整潔代碼;然后通過大量的刻意練習(xí),才能真正寫出整潔的代碼。

          WTF/min是衡量代碼質(zhì)量的唯一標(biāo)準(zhǔn),Uncle Bob在書中稱糟糕的代碼為沼澤(wading),這只突出了我們是糟糕代碼的受害者。

          國內(nèi)有一個(gè)更適合的詞匯:屎山,雖然不是很文雅但是更加客觀,程序員既是受害者也是加害者。

          對(duì)于什么是整潔的代碼,書中給出了大師們的總結(jié):

          • Bjarne Stroustrup:優(yōu)雅且高效;直截了當(dāng);減少依賴;只做好一件事
          • Grady booch:簡(jiǎn)單直接
          • Dave thomas:可讀,可維護(hù),單元測(cè)試
          • Ron Jeffries:不要重復(fù)、單一職責(zé),表達(dá)力(Expressiveness)

          其中,我最喜歡的是表達(dá)力(Expressiveness)這個(gè)描述,這個(gè)詞似乎道出了好代碼的真諦:用簡(jiǎn)單直接的方式描繪出代碼的功能,不多也不少。

          本文記錄閱讀《clean code》之后個(gè)人“深有同感”或者“醍醐灌頂”的一些觀點(diǎn)。

          命名的藝術(shù)

          坦白的說,命名是一件困難的事情,要想出一個(gè)恰到好處的命名需要一番功夫,尤其我們的母語還不是編程語言所通用的英語。

          不過這一切都是值得了,好的命名讓你的代碼更直觀,更有表達(dá)力。

          好的命名應(yīng)該有下面的特征:

          名副其實(shí)

          好的變量名告訴你:是什么東西,為什么存在,該怎么使用

          如果需要通過注釋來解釋變量,那么就先得不那么名副其實(shí)了。

          下面是書中的一個(gè)示例代碼,展示了命名對(duì)代碼質(zhì)量的提升

          避免誤導(dǎo)

          • 不要掛羊頭賣狗肉
          • 不要覆蓋慣用縮略語

          這里不得不吐槽前兩天才看到的一份代碼,居然使用了 l 作為變量名;而且,user居然是一個(gè)list(單復(fù)數(shù)都沒學(xué)好!!)

          有意義的區(qū)分

          代碼是寫給機(jī)器執(zhí)行,也是給人閱讀的,所以概念一定要有區(qū)分度。

          # bad
          def copy(a_list, b_list):
           pass
           
          # good
          def copy(source, destination):
           pass
          

          使用讀的出來的單詞

          如果名稱讀不出來,那么討論的時(shí)候就會(huì)像個(gè)傻鳥

          使用方便搜索的命名

          名字長(zhǎng)短應(yīng)與其作用域大小相對(duì)應(yīng)

          避免思維映射

          比如在代碼中寫一個(gè)temp,那么讀者就得每次看到這個(gè)單詞的時(shí)候翻譯成其真正的意義

          注釋

          有表達(dá)力的代碼是無需注釋的。

          The proper use of comments is to compensate for our failure to express ourself in code.

          注釋的適當(dāng)作用在于彌補(bǔ)我們用代碼表達(dá)意圖時(shí)遇到的失敗,這聽起來讓人沮喪,但事實(shí)確實(shí)如此。

          The truth is in the code, 注釋只是二手信息,二者的不同步或者不等價(jià)是注釋的最大問題。

          書中給出了一個(gè)非常形象的例子來展示:用代碼來闡述,而非注釋

          bad
          // check to see if the employee is eligible for full benefit
          if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
          good
          if (employee.isEligibleForFullBenefits())
          

          因此,當(dāng)想要添加注釋的時(shí)候,可以想想是否可以通過修改命名,或者修改函數(shù)(代碼)的抽象層級(jí)來展示代碼的意圖。

          當(dāng)然,也不能因噎廢食,書中指出了以下一些情況屬于好的注釋

          1. 法務(wù)信息
          2. 對(duì)意圖的注釋,為什么要這么做
          3. 警示
          4. TODO注釋
          5. 放大看似不合理之物的重要性

          其中個(gè)人最贊同的是第2點(diǎn)和第5點(diǎn),做什么很容易通過命名表達(dá),但為什么要這么做則并不直觀,特別涉及到專業(yè)知識(shí)、算法的時(shí)候。

          另外,有些第一感覺“不那么優(yōu)雅”的代碼,也許有其特殊愿意,那么這樣的代碼就應(yīng)該加上注釋,說明為什么要這樣,比如為了提升關(guān)鍵路徑的性能,可能會(huì)犧牲部分代碼的可讀性。

          最壞的注釋就是過時(shí)或者錯(cuò)誤的注釋,這對(duì)于代碼的維護(hù)者(也許就是幾個(gè)月后的自己)是巨大的傷害,可惜除了code review,并沒有簡(jiǎn)單易行的方法來保證代碼與注釋的同步。

          函數(shù)

          函數(shù)的單一職責(zé)

          一個(gè)函數(shù)應(yīng)該只做一件事,這件事應(yīng)該能通過函數(shù)名就能清晰的展示。判斷方法很簡(jiǎn)單:看看函數(shù)是否還能再拆出一個(gè)函數(shù)。

          函數(shù)要么做什么 do_sth, 要么查詢什么 query_sth。最惡心的就是函數(shù)名表示只會(huì) query_sth, 但事實(shí)上卻會(huì) do_sth, 這使得函數(shù)產(chǎn)生了副作用。比如書中的例子

          函數(shù)的抽象層級(jí)

          每個(gè)函數(shù)一個(gè)抽象層次,函數(shù)中的語句都要在同一個(gè)抽象層級(jí),不同的抽象層級(jí)不能放在一起。比如我們想把大象放進(jìn)冰箱,應(yīng)該是這個(gè)樣子的:

          def pushElephantIntoRefrige():
           openRefrige()
           pushElephant()
           closeRefrige()
          

          函數(shù)里面的三句代碼在同一個(gè)層級(jí)(高度)描述了要完成把大象放進(jìn)冰箱這件事順序相關(guān)的三個(gè)步驟。

          顯然,pushElephant 這個(gè)步驟又可能包含很多子步驟,但是在pushElephantIntoRefrige 這個(gè)層級(jí),是無需知道太多細(xì)節(jié)的。

          當(dāng)我們想通過閱讀代碼的方式來了解一個(gè)新的項(xiàng)目時(shí),一般都是采取廣度優(yōu)先的策略,自上而下的閱讀代碼,先了解整體結(jié)構(gòu),然后再深入感興趣的細(xì)節(jié)。

          如果沒有對(duì)實(shí)現(xiàn)細(xì)節(jié)進(jìn)行良好的抽象(并凝練出一個(gè)名副其實(shí)的函數(shù)),那么閱讀者就容易迷失在細(xì)節(jié)的汪洋里。

          某種程度看來,這個(gè)跟金字塔原理也很像



          每一個(gè)層級(jí)都是為了論證其上一層級(jí)的觀點(diǎn),同時(shí)也需要下一層級(jí)的支持;同一層級(jí)之間的多個(gè)論點(diǎn)又需要以某種邏輯關(guān)系排序。

          pushElephantIntoRefrige 就是中心論點(diǎn),需要多個(gè)子步驟的支持,同時(shí)這些子步驟之間也有邏輯先后順序。

          函數(shù)參數(shù)

          函數(shù)的參數(shù)越多,組合出的輸入情況就愈多,需要的測(cè)試用例也就越多,也就越容易出問題。

          輸出參數(shù)相比返回值難以理解,這點(diǎn)深有同感,輸出參數(shù)實(shí)在是很不直觀。

          從函數(shù)調(diào)用者的角度,一眼就能看出返回值,而很難識(shí)別輸出參數(shù)。輸出參數(shù)通常逼迫調(diào)用者去檢查函數(shù)簽名,這個(gè)實(shí)在不友好。

          向函數(shù)傳入Boolean(書中稱之為 Flag Argument)通常不是好主意。

          尤其是傳入True or False后的行為并不是一件事情的兩面,而是兩件不同的事情時(shí)。這很明顯違背了函數(shù)的單一職責(zé)約束,解決辦法很簡(jiǎn)單,那就是用兩個(gè)函數(shù)。

          Dont repear yourself

          在函數(shù)這個(gè)層級(jí),是最容易、最直觀實(shí)現(xiàn)復(fù)用的,很多IDE也難幫助我們講一段代碼重構(gòu)出一個(gè)函數(shù)。

          不過在實(shí)踐中,也會(huì)出現(xiàn)這樣一種情況:一段代碼在多個(gè)方法中都有使用,但是又不完全一樣,如果抽象成一個(gè)通用函數(shù),那么就需要加參數(shù)、加if else區(qū)別。這樣就有點(diǎn)尷尬,貌似可以重構(gòu),但又不是很完美。

          造成上述問題的某種情況是因?yàn)椋@段代碼也違背了單一職責(zé)原則,做了不只一件事情,這才導(dǎo)致不好復(fù)用,解決辦法是進(jìn)行方法的細(xì)分,才能更好復(fù)用。也可以考慮template method來處理差異的部分。

          測(cè)試

          非常慚愧的是,在我經(jīng)歷的項(xiàng)目中,測(cè)試(尤其是單元測(cè)試)一直都沒有得到足夠的重視,也沒有試行過 TDD。正因?yàn)槿笔В鸥辛己脺y(cè)試的珍貴。

          我們常說,好的代碼需要有可讀性、可維護(hù)性、可擴(kuò)展性,好的代碼、架構(gòu)需要不停的重構(gòu)、迭代,但自動(dòng)化測(cè)試是保證這一切的基礎(chǔ),沒有高覆蓋率的、自動(dòng)化的單元測(cè)試、回歸測(cè)試,誰都不敢去修改代碼,只能任其腐爛。

          即使針對(duì)核心模塊寫了單元測(cè)試,一般也很隨意,認(rèn)為這只是測(cè)試代碼,配不上生產(chǎn)代碼的地位,以為只要能跑通就行了。

          這就導(dǎo)致測(cè)試代碼的可讀性、可維護(hù)性非常差,然后導(dǎo)致測(cè)試代碼很難跟隨生產(chǎn)代碼一起更新、演化,最后導(dǎo)致測(cè)試代碼失效。

          所以說,臟測(cè)試 - 等同于 - 沒測(cè)試。

          因此,測(cè)試代碼的三要素:可讀性,可讀性,可讀性。

          對(duì)于測(cè)試的原則、準(zhǔn)則如下:

          • You are not allowed to write any production code unless it is to make a failing unit test pass. 沒有測(cè)試之前不要寫任何功能代碼
          • You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 只編寫恰好能夠體現(xiàn)一個(gè)失敗情況的測(cè)試代碼
          • You are not allowed to write any more production code than is sufficient to pass the one failing unit test. 只編寫恰好能通過測(cè)試的功能代碼

          測(cè)試的FIRST 準(zhǔn)則:

          1. 快速(Fast)測(cè)試應(yīng)該夠快,盡量自動(dòng)化。
          2. 獨(dú)立(Independent) 測(cè)試應(yīng)該應(yīng)該獨(dú)立。不要相互依賴
          3. 可重復(fù)(Repeatable) 測(cè)試應(yīng)該在任何環(huán)境上都能重復(fù)通過。
          4. 自我驗(yàn)證(Self-Validating) 測(cè)試應(yīng)該有bool輸出。不要通過查看日志這種低效率方式來判斷測(cè)試是否通過
          5. 及時(shí)(Timely) 測(cè)試應(yīng)該及時(shí)編寫,在其對(duì)應(yīng)的生產(chǎn)代碼之前編寫

          主站蜘蛛池模板: 狠狠爱无码一区二区三区| 中文字幕人妻丝袜乱一区三区| 天天看高清无码一区二区三区| 日韩视频在线观看一区二区| 日本免费一区二区三区四区五六区 | 亚洲国产欧美一区二区三区| 中文字幕一区日韩在线视频| 一区二区高清在线| 无码人妻精品一区二区三区不卡 | 成人精品一区二区激情| 91精品国产一区| 秋霞午夜一区二区| 久久精品综合一区二区三区| 一区二区三区高清在线| 国产激情一区二区三区在线观看| 午夜无码一区二区三区在线观看 | 波多野结衣高清一区二区三区| 福利一区二区在线| 天堂va视频一区二区| 午夜精品一区二区三区在线视| 夜色阁亚洲一区二区三区| 日韩精品一区在线| 丰满人妻一区二区三区免费视频 | 精品无人区一区二区三区| 久久久无码精品国产一区| 久久久久无码国产精品一区| 亚洲国产精品无码久久一区二区| 成人无码一区二区三区| 日韩制服国产精品一区| 国产午夜福利精品一区二区三区| 国产一区二区精品尤物| 久久久91精品国产一区二区| 亚洲av无码一区二区三区乱子伦 | 人妻互换精品一区二区| 成人精品一区二区激情| 国产一区玩具在线观看| 亚洲av无码一区二区三区乱子伦 | 亚洲一区二区免费视频| 国产伦精品一区二区三区视频猫咪 | 精品国产一区二区三区香蕉 | 成人中文字幕一区二区三区|