接受一個(gè)不及格的業(yè)務(wù)線,我是如何維護(hù)和重構(gòu)的

項(xiàng)目背景

IM聊天功能作為整個(gè)電商功能的補(bǔ)充和重要支撐,相信很多的電商App都會(huì)集成這么一個(gè)功能,但是大多數(shù)公司的IM功能相信都是集成的融云或者環(huán)信的SDK。

但是相信作為電商的有力支撐,IM的消息對(duì)于各個(gè)公司來說都有不同的業(yè)務(wù)需求,也就是說普通的圖片、文字、紅包甚至語音這種常用的消息類型并不能有力支撐起一個(gè)IM的業(yè)務(wù)。我們公司的IM業(yè)務(wù)功能正式在這種背景下,完全從前端到后端都是完全由自己人設(shè)計(jì)、開發(fā)以及維護(hù)的。

我是在17年年中左右正式接手了我司的IM業(yè)務(wù)功能,剛開始是起因于解決線上的一個(gè)bug,于是開始梳理了一下代碼邏輯,于是。。。懵逼了好久好久好久。
雖然IM的代碼已經(jīng)在線上跑了很久了,在我開始解決bug之前貌似有大概有大半年將近一年的的時(shí)間沒有人來維護(hù),但是從架構(gòu)設(shè)計(jì)上、業(yè)務(wù)代碼實(shí)現(xiàn)等點(diǎn)來看,質(zhì)量十分堪憂。

梳理代碼

我司IM的架構(gòu)設(shè)計(jì)大概是在幾年以前,長連接協(xié)議使用的是WebSocket,業(yè)務(wù)邏輯有一些復(fù)雜,目測(cè)從數(shù)據(jù)邏輯到業(yè)務(wù)邏輯再到UI邏輯,代碼量可能會(huì)接近5w這個(gè)量級(jí),所以說一開始就一行一行的看代碼邏輯顯然是不太理智的。

梳理第一步

第一步的主要目的是熟悉代碼的主要脈絡(luò),于是我開始有序的梳理沿著數(shù)據(jù)流向梳理主干,要點(diǎn)如下:

  • 分析各個(gè)數(shù)據(jù)模型(model)。
  • 整理各個(gè)HTTP請(qǐng)求的API。
  • 整理并備注各個(gè)Notification的Key,并標(biāo)記使用場景。
  • 整理各個(gè)Delegate回調(diào)函數(shù)的使用場景。
  • 整理并備注各個(gè)枚舉值的含義以及使用場景。

因?yàn)榇a歷史久遠(yuǎn)且開發(fā)維護(hù)人員換了好幾茬之后,代碼邏輯比較混亂,剛開始梳理的時(shí)候大部分時(shí)間都花在了備注各種代碼上。

梳理第二步

第一步之后,我其實(shí)已經(jīng)對(duì)于IM的架構(gòu)邏輯開始有了一個(gè)初步的比較寬泛的了解。第二步的的主要目的是整理在使用的主要的幾個(gè)組件:

  • 數(shù)據(jù)庫的初始化創(chuàng)建以及使用邏輯。
  • HTTP代碼的初始化創(chuàng)建以及使用邏輯。
  • 長連接代碼的初始化創(chuàng)建以及使用邏輯,特別是與服務(wù)端溝通和?;畹牟糠?。
  • 針對(duì)以上基礎(chǔ)控件的二次封裝控件的整理。

梳理了上面幾個(gè)之后,我陸續(xù)整理了如下:

  • 數(shù)據(jù)庫的表結(jié)構(gòu)設(shè)計(jì)、初始化創(chuàng)建以及銷毀等邏輯。
  • 了解HTTP的接口,進(jìn)一步了解了基礎(chǔ)的業(yè)務(wù)設(shè)計(jì)。
  • 通過和服務(wù)端的同事溝通,明確了當(dāng)前的長連接協(xié)議下,兩端是如何?;?、溝通數(shù)據(jù)以及溝通狀態(tài)的。
  • 各個(gè)API的使用場景以及使用邏輯。

梳理第三步

上面兩步,基本上把最核心的工具整理完成,下面就開始將代碼邏輯串起來,整理業(yè)務(wù)邏輯:

  • 群聊的收發(fā)消息邏輯。
  • 私聊的收發(fā)消息邏輯。
  • 登錄、退出登錄、更換賬號(hào)登錄以及綁定賬號(hào)之后的業(yè)務(wù)邏輯。
  • 自后臺(tái)喚醒之后的業(yè)務(wù)邏輯。
  • 收到推送消息之后的業(yè)務(wù)邏輯。

整理好了以上之后,我利用流程圖工具ProcessOn創(chuàng)建了大概有10張左右的流程表,數(shù)據(jù)流向和業(yè)務(wù)邏輯一清二楚。

特別是聊天的數(shù)據(jù)邏輯,從HTTP請(qǐng)求、長連接推送以及數(shù)據(jù)庫操作,無所不包。但是圖整理完之后,對(duì)于主要流程幾乎是掌握的比較清楚了,即使回頭有忘記的,回頭查看表之后就會(huì)一清二楚,不僅便于我熟悉邏輯,對(duì)以后的維護(hù)也很有益處。

維護(hù)

遇到的困境

雖然代碼質(zhì)量堪憂,但是代碼中的bug還算比較少,所以在解決線上bug這個(gè)問題上,沒有遇到過多的麻煩。但是其中也有幾個(gè)bug十分麻煩,找了好久才找到問題的原因。

其中有一個(gè),找原因大概找了一周多的時(shí)間,最后定位問題在我們的賬號(hào)系統(tǒng)上。因?yàn)闅v史原因,我們的IM業(yè)務(wù)和其他業(yè)務(wù)是兩套賬號(hào)體系,中間經(jīng)歷過一次賬號(hào)變更,但是由于某些奇葩的原因(有部分業(yè)務(wù)經(jīng)歷了hack),導(dǎo)致部分用戶的賬號(hào)沒有成功的過渡賬號(hào)變更,導(dǎo)致了一些問題。

表象原因是代碼邏輯混亂,bug原因復(fù)雜,無法復(fù)現(xiàn)并難以定位問題。
但更深的原因是時(shí)間久遠(yuǎn),我們對(duì)當(dāng)時(shí)的代碼設(shè)計(jì)以及業(yè)務(wù)邏輯變更毫無記錄,導(dǎo)致無法快速定位到問題的原因。

思考

對(duì)于一個(gè)邏輯復(fù)雜的業(yè)務(wù),首先要考慮的是易拓展、易維護(hù)和模塊組件間的高度解耦。

對(duì)于一個(gè)成熟的業(yè)務(wù)來說,我認(rèn)為易于維護(hù)性更為重要,因?yàn)闃I(yè)務(wù)已經(jīng)進(jìn)入成熟階段的話就意味著功能增加的幾率比較低,那么對(duì)于線上bug修復(fù)和小修小補(bǔ)的優(yōu)化就是主要工作。
我司的IM就是這樣的,大家看梳理的代碼的第一步應(yīng)該能看出來,我耗費(fèi)了大量的時(shí)間去做各種備注,然而,這寫備注應(yīng)該在開發(fā)階段就應(yīng)該寫好了。包括對(duì)于之前兩套賬號(hào)系統(tǒng)存在的原因,包括變更一次賬號(hào)的原因,是否需要有一個(gè)詳細(xì)的記錄?

那么對(duì)于這種這種有助于后期維護(hù)的記錄,我認(rèn)為我們需要對(duì)重要的業(yè)務(wù)變更以及業(yè)務(wù)設(shè)計(jì)要有一個(gè)詳細(xì)的記錄,無論是自己作為記錄還是為后面接手的同學(xué)做個(gè)參考,我認(rèn)為都是極為重要的。

重構(gòu)

思索需求

經(jīng)過最開始的了解和一段時(shí)間維護(hù),我發(fā)現(xiàn)遇到的最大麻煩是數(shù)據(jù)邏輯、業(yè)務(wù)邏輯、和UI操作邏輯混到了一起,簡直可以說是牽一發(fā)而動(dòng)全身。特別是數(shù)據(jù)邏輯十分混亂,因?yàn)閿?shù)據(jù)邏輯的混亂,導(dǎo)致我對(duì)于后面業(yè)務(wù)邏輯的變更十分費(fèi)力,測(cè)試成本也是指數(shù)級(jí)上漲,另外UI邏輯雜亂,適配iPhone X的時(shí)候也遇到了一些小麻煩。

現(xiàn)在的架構(gòu)設(shè)計(jì)的原因是什么?這樣設(shè)計(jì)業(yè)務(wù)需求是否合理?是否有優(yōu)化的空間?

分析現(xiàn)狀與判斷未來

無論是客戶端還是服務(wù)端,亦或是兩端數(shù)據(jù)交互上,IM業(yè)務(wù)的架構(gòu)設(shè)計(jì)本身就存在很多問題。因?yàn)闀r(shí)間短暫,不太可能一次性解決所有的問題,特別是對(duì)于服務(wù)端來說,一次性的大規(guī)模重構(gòu)可能性極低。

對(duì)于現(xiàn)在來說:

  • UI重構(gòu)是首當(dāng)其沖的, 在保證現(xiàn)有邏輯的基礎(chǔ)上,重新設(shè)計(jì)UI層的邏輯結(jié)構(gòu),保證代碼的復(fù)用性和可擴(kuò)展性,為了將來有可能的業(yè)務(wù)升級(jí)留足空間。
  • 梳理基礎(chǔ)組件,比如HTTP、長連接協(xié)議和數(shù)據(jù)庫,還有其他的一些工具類,通過封裝成組件和組件引用來是他們能從業(yè)務(wù)邏輯中獨(dú)立出來。便于獨(dú)立維護(hù)、升級(jí)甚至替換
  • 將原有的數(shù)據(jù)操作邏輯從UI邏輯中完整抽離出來,需要達(dá)到向下對(duì)基礎(chǔ)組件要有封裝和控制,向上對(duì)業(yè)務(wù)邏輯要有承接,而且依然要做到耦合度盡量的低。
  • 因?yàn)镮M部分,我司的兩個(gè)App這部分代碼有90%的代碼重合,需要考慮兩端通用的問題。

架構(gòu)設(shè)計(jì)

  • 工廠模式
  • 瘦Model
  • 去model化的Cell
  • 項(xiàng)目優(yōu)先,分離核心業(yè)務(wù)模塊組成pod
  • 考慮業(yè)務(wù)變更可能性,盡可能向上保持API穩(wěn)定性
  • KVO進(jìn)行反向傳值

著手動(dòng)工

UI層重構(gòu)

UI層的重構(gòu)是最先開始的,無論架構(gòu)怎么變,UI層都是直接面向用戶的,直接承載了公司的業(yè)務(wù)功能實(shí)現(xiàn),所以為了靈活適應(yīng)業(yè)務(wù)的升級(jí)或變化,給用戶一個(gè)好用流暢的入口,UI層設(shè)計(jì)上要盡可能的靈活。耦合盡可能的小,流暢性上要有保證。

重構(gòu)思路相對(duì)簡單,重寫View和Controller,去除冗余復(fù)雜的UI邏輯代碼,規(guī)范并統(tǒng)一第三方框架使用,封裝公用組件,隔離膠水代碼,設(shè)計(jì)靈活的UI結(jié)構(gòu)。

因?yàn)镮M系統(tǒng)的最主要UI仍然是TableView,所以針對(duì)TableView的各種優(yōu)化就是重中之重,我著重說一下我對(duì)于復(fù)雜Cell類型的設(shè)計(jì)方案。

1、共有的組件有很多,比如時(shí)間、頭像、背景氣泡等等,所以說子類繼承父類是最基礎(chǔ)的方案。
2、棄置Autolayout的UI書寫方式,完全用Frame來寫UI布局。Cell高度以及內(nèi)部UI組件的布局和位置,通過異步計(jì)算并緩存為LayoutModel,通過這種方式降低計(jì)算的重復(fù)耗時(shí)操作。
3、有的消息類型只是負(fù)責(zé)展示,但是有的確有相對(duì)復(fù)雜的業(yè)務(wù)邏輯,但是為了防止Cell代碼的膨脹,采用了瘦Cell的方式分離邏輯,力求使Cell盡量只負(fù)責(zé)UI的承載和展示,增加helper層處理相關(guān)邏輯。
4、因?yàn)殡m然是相同的一個(gè)數(shù)據(jù),但是呈現(xiàn)的方式會(huì)存才差異化,所以采用瘦Model的形式,通過創(chuàng)建Helper對(duì)取到的原始數(shù)據(jù)進(jìn)行相對(duì)應(yīng)的加工,直接提供給業(yè)務(wù)邏輯處理好的數(shù)據(jù)。在AFNetworking給的Demo中,是一個(gè)典型的胖Model的例子,倒不是說他的例子不好,只是隨著業(yè)務(wù)邏輯的復(fù)雜以及生數(shù)據(jù)和熟數(shù)據(jù)的差異越來越大的時(shí)候,胖Mode的代碼量會(huì)幾何級(jí)數(shù)般的膨脹,所以還是要因地制宜,具體情況具體分析。
5、利用Factory模式分離出關(guān)于復(fù)雜Cell類型的判斷,包括初始化、賦值等。
6、使用KVO取代delegate進(jìn)行反向傳值,用以減少代碼耦合。

關(guān)于如何保證UI性能,可參考我的另一篇Blog,iOS 性能優(yōu)化的探索.

最終結(jié)果,第一個(gè)完整case的UI層Controller代碼,從3000行直接縮減到了1200行,Controller中沒有復(fù)雜的多方數(shù)據(jù)處理邏輯,復(fù)雜的邏輯判斷。只作為UI展示以及接口調(diào)用,完全剝離了數(shù)據(jù)邏輯的處理,所有的處理邏輯由下面的數(shù)據(jù)邏輯層處理。

數(shù)據(jù)邏輯層重構(gòu)

我再分析了業(yè)務(wù)需求并設(shè)計(jì)了架構(gòu)之后,決定重構(gòu)以自下而上的順序來進(jìn)行,于是第一部分就是對(duì)于數(shù)據(jù)邏輯層的重構(gòu)。步驟如下:
此部分,分為以下三層結(jié)構(gòu):

1、業(yè)務(wù)數(shù)據(jù)邏輯層
2、適配器層(adapter)
3、基礎(chǔ)組件服務(wù)層(server)
1、業(yè)務(wù)數(shù)據(jù)邏輯層

這部分的主要作用是直接受到UI層的調(diào)用,負(fù)責(zé)長連接以及短連接的建立,數(shù)據(jù)庫的初始化操作等。
向上直接承接UI邏輯和業(yè)務(wù)邏輯,是高度面向業(yè)務(wù)封裝的接口。比如在發(fā)送照片消息的時(shí)候,只需要將調(diào)用API傳入Image對(duì)象,其他的流程比如說是上傳資源以及組成message對(duì)象等,則不需要上一層調(diào)用和考慮。

所以這一層盡可能的會(huì)很薄,不會(huì)有特別多的邏輯代碼。

2、適配器層(adapter)

這部分的主要工作是承上啟下,承接上一層的面向業(yè)務(wù)的封裝,調(diào)用下一層基礎(chǔ)組件服務(wù)層的接口,可以說絕大多數(shù)的接口封裝都集中在這一層。因?yàn)槲覀冇行I(yè)務(wù)的長連接和短連接的使用上不是很合理,所以我將長短連接都封裝到了一個(gè)網(wǎng)絡(luò)服務(wù)的類中,此后假如長短連接的業(yè)務(wù)產(chǎn)生了變化,但仍可以保持向上的接口穩(wěn)定性。

舉個(gè)例子說,當(dāng)推送來一條消息之后,是通過長連接,但是需要收到數(shù)據(jù)之后在進(jìn)行AFN的操作完成消息體完整數(shù)據(jù)的獲取,之后要存入數(shù)據(jù)庫并且將是否讀取狀態(tài)設(shè)置為NO,當(dāng)用戶讀取當(dāng)前消息之后,將這部分消息還要更新為已讀狀態(tài)。

這部分操作涉及到了所有的組件的操作,但是反饋到最上面一層的時(shí)候,大概只是新的消息,并且是完整的消息,然后再刷新UI。所以說,這一層的業(yè)務(wù)量比較大,幾乎是要按照各種標(biāo)準(zhǔn)操作,完整的處理好所有組件的接口。

3、基礎(chǔ)組件服務(wù)層(server)

這部分基本可以說是基于IM本身的業(yè)務(wù)特點(diǎn),對(duì)于基礎(chǔ)組件的調(diào)用封裝。
包括:

1、數(shù)據(jù)庫部分,對(duì)于IM消息的數(shù)據(jù)結(jié)構(gòu),封裝的對(duì)于數(shù)據(jù)庫的創(chuàng)建,以及增刪改查等接口。以及基于業(yè)務(wù)的一些接口,例如一次性設(shè)置當(dāng)前聊天的所有消息為已讀狀態(tài)等接口。
2、AFN部分,這部分相對(duì)來說就很簡單了,基本上是依賴于AFN封裝的接口,比如獲取當(dāng)前User的詳細(xì)信息等。
3、長連接部分,包括對(duì)于長連接協(xié)議的創(chuàng)建連接、斷連以及心跳超時(shí)上報(bào)等操作,也包括了發(fā)送消息和收到消息回調(diào)等底層操作。

拆分之后的Manager層代碼量所見到原來的40%左右,于是改名為Session層。

基礎(chǔ)組件層的重構(gòu)以及封裝

這部分因?yàn)閷儆诠玫幕A(chǔ)組件,所以相對(duì)來說只是基礎(chǔ)組件的比如說AFN以及數(shù)據(jù)庫(FMDB)是整個(gè)App的組成,所以沒什么其他的操作,只是單純做了一層邏輯上分層。

但是對(duì)于長連接我們做了一些定制,比如:

1、增加了重連的邏輯機(jī)制。
2、增加創(chuàng)建連接以及斷掉連接時(shí)候各種狀態(tài)的判斷等。

主要任務(wù)還是集中在對(duì)于協(xié)議庫本身的邏輯補(bǔ)充和健壯性優(yōu)化等。

其他操作

1、創(chuàng)建枚舉文件,擴(kuò)展標(biāo)準(zhǔn)化的枚舉變量。
2、合并以及分割Model,隨著業(yè)務(wù)的擴(kuò)展,原來的Model設(shè)計(jì)已經(jīng)不符合當(dāng)下的業(yè)務(wù)發(fā)展,根據(jù)現(xiàn)在固定的業(yè)務(wù),重新設(shè)計(jì)了Model的集成關(guān)系,對(duì)于分化嚴(yán)重的也做了重新分割。
3、分離并封裝了膠水代碼到一個(gè)大的工具類,便于調(diào)用和調(diào)試。

走過的彎路

1、過度思考代碼解耦合而忽略了業(yè)務(wù)邏輯復(fù)雜性,錯(cuò)將組件化各組件的解耦合的邏輯應(yīng)用在了本來就是高耦合的MVC架構(gòu)上。嘗試使用去model化的Cell,但是實(shí)際操作環(huán)節(jié)發(fā)現(xiàn)增加了大量的邏輯判斷,無形中將Model本該處理的業(yè)務(wù)邏輯轉(zhuǎn)接到Controller和View上,表面上看上去API簡潔到家,但是上手代碼量并不算小,不利于維護(hù)。

2、在一開始采用了MVCS的設(shè)計(jì)重構(gòu)UI層,簡化Controller中對(duì)于Model的處理,在Store中進(jìn)行了主動(dòng)和被動(dòng)網(wǎng)絡(luò)邏輯、本地?cái)?shù)據(jù)庫調(diào)用等。但事實(shí)上最后通過封裝統(tǒng)一入口的方式將獲取數(shù)據(jù)的邏輯全部從UI邏輯層剝離開,下沉到了數(shù)據(jù)邏輯層,對(duì)于UI層來說只需要考慮的是進(jìn)行了調(diào)用獲取數(shù)據(jù)的API操作或者是被動(dòng)受到了新的數(shù)據(jù),不需要考慮數(shù)據(jù)來自于服務(wù)端、Cache還是本地?cái)?shù)據(jù)庫,也不需要考慮后面的邏輯。

3、對(duì)于UI層和下一層的數(shù)據(jù)溝通,雖然采用了KVO的方式回調(diào),降低了耦合性,但是仍然存在參數(shù)復(fù)雜的情況下,傳遞過多的Key的情況,導(dǎo)致解析稍顯困難和復(fù)雜。

4、Cell的繼承,看上去是一個(gè)很直觀的設(shè)計(jì),但是隨著重構(gòu)代碼量的增加以及業(yè)務(wù)變化發(fā)現(xiàn)繼承過程中會(huì)存在很多問題,通過面向協(xié)議等方式或許可以解決繼承中的問題。

總結(jié)以及思考

架構(gòu)設(shè)計(jì)的時(shí)候,一定要預(yù)判用戶的使用習(xí)慣,判斷未來的業(yè)務(wù)導(dǎo)向,盡可能的降低代碼侵入性和耦合性。對(duì)于性能產(chǎn)生的影響的地方,通過以上幾點(diǎn)來設(shè)計(jì)架構(gòu)。
架構(gòu)設(shè)計(jì)分層要清晰,API設(shè)計(jì)要盡可能簡潔,避免暴露過多的接口和參數(shù),避免模塊之間的緊耦合,UI設(shè)計(jì)要盡可能靈活。
重構(gòu)前,需要思考切入點(diǎn),是從上值下、從下至上,還是模塊化抽離。

已從這家公司離職,部分邏輯全憑記憶整理,如果有疏漏或錯(cuò)誤,還請(qǐng)大家海涵。

Refrence

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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