Java生鮮電商平台-App系統架構開發與設計
說明:閱讀此文,你可以學習到以下的技術分享
1.Java生鮮電商平台-App架構設計經驗談:接口的設計
2.Java生鮮電商平台-App架構設計經驗談:技術選型
3.Java生鮮電商平台-App架構設計經驗談:數據層的設計
4.Java生鮮電商平台-App架構設計經驗談:業務層的設計
5.Java生鮮電商平台-App架構設計經驗談:展示層的設計
接口設計:
App與服務器的通信接口如何設計得好,需要考慮的地方挺多的,在此根據我的一些經驗做一些總結分享,旨在拋磚引玉。
安全機制的設計
現在,大部分App的接口都采用RESTful架構,RESTFul最重要的一個設計原則就是,客戶端與服務器的交互在請求之間是無狀態的,也就是說,當涉及到用戶狀態時,每次請求都要帶上身份驗證信息。實現上,大部分都采用token的認證方式,一般流程是:
用戶用密碼登錄成功后,服務器返回token給客戶端;
客戶端將token保存在本地,發起后續的相關請求時,將token發回給服務器;
服務器檢查token的有效性,有效則返回數據,若無效,分兩種情況:
token錯誤,這時需要用戶重新登錄,獲取正確的token
token過期,這時客戶端需要再發起一次認證請求,獲取新的token
然而,此種驗證方式存在一個安全性問題:當登錄接口被劫持時,黑客就獲取到了用戶密碼和token,后續則可以對該用戶做任何事情了。用戶只有修改密碼才能奪回控制權。
如何優化呢?第一種解決方案是采用HTTPS。HTTPS在HTTP的基礎上添加了SSL安全協議,自動對數據進行了壓縮加密,在一定程序可以防止監聽、防止劫持、防止重發,安全性可以提高很多。不過,SSL也不是絕對安全的,也存在被劫持的可能。另外,服務器對HTTPS的配置相對有點復雜,還需要到CA申請證書,而且一般還是收費的。而且,HTTPS效率也比較低。一般,只有安全要求比較高的系統才會采用HTTPS,比如銀行。而大部分對安全要求沒那么高的App還是采用HTTP的方式。
我們目前的做法是給每個接口都添加簽名。給客戶端分配一個密鑰,每次請求接口時,將密鑰和所有參數組合成源串,根據簽名算法生成簽名值,發送請求時將簽名一起發送給服務器驗證。類似的實現可參考OAuth1.0的簽名算法。這樣,黑客不知道密鑰,不知道簽名算法,就算攔截到登錄接口,后續請求也無法成功操作。不過,因為簽名算法比較麻煩,而且容易出錯,只適合對內的接口。如果你們的接口屬於開放的API,則不太適合這種簽名認證的方式了,建議還是使用OAuth2.0的認證機制。
我們也給每個端分配一個appKey,比如Android、iOS、微信三端,每個端分別分配一個appKey和一個密鑰。沒有傳appKey的請求將報錯,傳錯了appKey的請求也將報錯。這樣,安全性方面又加多了一層防御,同時也方便對不同端做一些不同的處理策略。
另外,現在越來越多App取消了密碼登錄,而采用手機號+短信驗證碼的登錄方式,我在當前的項目中也采用了這種登錄方式。這種登錄方式有幾種好處:
不需要注冊,不需要修改密碼,也不需要因為忘記密碼而重置密碼的操作了;
用戶不再需要記住密碼了,也不怕密碼泄露的問題了;
相對於密碼登錄其安全性明顯提高了。
接口數據的設計
接口的數據一般都采用JSON格式進行傳輸,不過,需要注意的是,JSON的值只有六種數據類型:
Number:整數或浮點數
String:字符串
Boolean:true 或 false
Array:數組包含在方括號[]中
Object:對象包含在大括號{}中
Null:空類型
所以,傳輸的數據類型不能超過這六種數據類型。以前,我們曾經試過傳輸Date類型,它會轉為類似於”2016年1月7日 09時17分42秒 GMT+08:00”這樣的字符串,這在轉換時會產生問題,不同的解析庫解析方式可能不同,有的可能會轉亂,有的可能直接異常了。要避免出錯,必須做特殊處理,自己手動去做解析。為了根除這種問題,最好的解決方案是用毫秒數表示日期。
另外,以前的項目中還出現過字符串的”true”和”false”,或者字符串的數字,甚至還出現過字符串的”null”,導致解析錯誤,尤其是”null”,導致App奔潰,后來查了好久才查出來是該問題導致的。這都是因為服務端對數據沒處理好,導致有些數據轉為了字符串。所以,在客戶端,也不能完全信任服務端傳回的數據都是對的,需要對所有異常情況都做相應處理。
服務器返回的數據結構,一般為:
{
code:0,
message: "success",
data: { key1: value1, key2: value2, ... }
}
code: 返回碼,0表示成功,非0表示各種不同的錯誤
message: 描述信息,成功時為”success”,錯誤時則是錯誤信息
data: 成功時返回的數據,類型為對象或數組
不同錯誤需要定義不同的返回碼,屬於客戶端的錯誤和服務端的錯誤也要區分,比如1XX表示客戶端的錯誤,2XX表示服務端的錯誤。這里舉幾個例子:
0:成功
100:請求錯誤
101:缺少appKey
102:缺少簽名
103:缺少參數
200:服務器出錯
201:服務不可用
202:服務器正在重啟
錯誤信息一般有兩種用途:一是客戶端開發人員調試時看具體是什么錯誤;二是作為App錯誤提示直接展示給用戶看。主要還是作為App錯誤提示,直接展示給用戶看的。所以,大部分都是簡短的提示信息。
data字段只在請求成功時才會有數據返回的。數據類型限定為對象或數組,當請求需要的數據為單個對象時則傳回對象,當請求需要的數據是列表時,則為某個對象的數組。這里需要注意的就是,不要將data傳入字符串或數字,即使請求需要的數據只有一個,比如token,那返回的data應該為:
// 正確
data: { token: 123456 }
// 錯誤
data: 123456
接口版本的設計
接口不可能一成不變,在不停迭代中,總會發生變化。接口的變化一般會有幾種:
數據的變化,比如增加了舊版本不支持的數據類型
參數的變化,比如新增了參數
接口的廢棄,不再使用該接口了
為了適應這些變化,必須得做接口版本的設計。實現上,一般有兩種做法:
每個接口有各自的版本,一般為接口添加個version的參數。
整個接口系統有統一的版本,一般在URL中添加版本號,比如http://api.domain.com/v2。
大部分情況下會采用第一種方式,當某一個接口有變動時,在這個接口上疊加版本號,並兼容舊版本。App的新版本開發傳參時則將傳入新版本的version。
如果整個接口系統的根基都發生變動的話,比如微博API,從OAuth1.0升級到OAuth2.0,整個API都進行了升級。
有時候,一個接口的變動還會影響到其他接口,但做的時候不一定能發現。因此,最好還要有一套完善的測試機制保證每次接口變更都能測試到所有相關層面。
技術選型:
當你做架構設計時,必然會面臨技術選型的抉擇,不同的技術方案,架構也可能完全不同。有哪些技術選型需要做決策呢?比如,App是純原生開發,還是Web App,抑或Hybrid App?iOS開發,語言上是選擇Objective-C還是Swift?架構模式用MVC,還是MVP,或者MVVM?下面根據我的一些經驗對某些方面做點總結分享。
原生/H5
關於用原生好,還是用H5好的爭論從沒間斷過。但我覺得,脫離了實際場景來討論孰好孰壞意義不大。就說我們目前正在做的項目,先說明下背景:
不止要做Android和iOS App,也要做微信公眾號;
H5人員缺乏,只有一兩個兼職的可用,而且不可控因素很高;
我們對原生比較熟;
開發時間只有半個月。
首先,需求上來說,大部分頁面用H5實現,可以減少很多工作量。但因為不可控因素太高,而時間又短,風險太大。而我們對原生比較熟,開發效率比較高,很多東西我也控制得了,風險相對比較低。而且,我們的主推產品是App,微信屬於輔助性產品,所以,微信要求也沒那么高。因此,我決定以原生為主,H5為輔,App大部分頁面用原生完成,小部分用WebView加載H5。
另外,WebView加載H5也有兩種模式,一種是加載服務器的H5頁面,一種是加載本地的H5頁面。加載服務器的H5頁面比較簡單,WebView只要load一下URL就可以了。加載本地的H5頁面,則需要將H5文件存放在本地,包括關聯的CSS和JS文件。這種方式相對比較復雜,不過,加載速度會比第一種快很多。我們當前項目基於上面考慮,只能選擇第一種方案。
如果人員和時間資源充足的話,那又如何選型呢?毫無疑問,我會以H5為主,微信和App都有的頁面統一用H5,App專有的部分,比如導航欄、標題欄、登錄等,才用原生實現。另外,WebView里的H5有點擊事件時,也許是URL鏈接,也許是調用JS的,都不會讓它直接在該WebView里做跳轉,需要攔截下來做些原生處理后跳轉到一個新的原生頁面,原生頁面也許嵌入另一個WebView,用來展示新的H5頁面。這是簡單的例子,關於Hybrid App詳細的設計,以后再講。另外,關於H5,絕對是大趨勢,強烈建議所有App開發人員都去學習。
Objective-C/Swift
我在項目中選擇了Swift,主要基於三個原因:
Swift真的很簡潔,生產效率很高;
Swift取代Objective-C是必然的趨勢;
目前iOS只有我一個人開發,不需要顧慮到團隊里沒人懂Swift。
如果你的團隊里沒人懂Swift,那還是乖乖用Objective-C吧;如果有一兩個懂Swift的,那可以混合開發,並讓不懂的人盡快學會Swift;如果都懂了,不用想了,直接上Swift吧。
當語言上選擇了Swift,相應的一些第三方庫也面臨着選型。比如,依賴庫管理,Objective-C時代大部分用CocoaPods,Swift時代,我更喜歡Carthage。Carhage是用Swift寫的,和CocoaPods相比,輕耦合,也更靈活。我個人也不太喜歡CocoaPods,使用起來比較麻煩,耦合性也較高,我使用過程中也經常出問題,而且還總是不知道該怎么解決,要移除時也是非常麻煩。
再推薦幾個關於Swift的第三方庫:
Alamofire:Swift版本的網絡基礎庫,和AFNetworking是同一個作者
AlamofireImage:基於Alamofire的圖片加載庫
ObjectMapper:Swift版本的Json和Model轉換庫
AlamofireObjectMapper:Alamofire的擴展庫,結合了ObjectMapper,自動將JSON的Response數據轉換為了Swift對象
MVC/MVP/MVVM
先分別簡單介紹下這三個架構模式吧:
MVC:Model-View-Controller,經典模式,很容易理解,主要缺點有兩個:
View對Model的依賴,會導致View也包含了業務邏輯;
Controller會變得很厚很復雜。
MVP:Model-View-Presenter,MVC的一個演變模式,將Controller換成了Presenter,主要為了解決上述第一個缺點,將View和Model解耦,不過第二個缺點依然沒有解決。
MVVM:Model-View-ViewModel,是對MVP的一個優化模式,采用了雙向綁定:View的變動,自動反映在ViewModel,反之亦然。
架構模式上,我不會推崇說哪種模式好,每種模式都各有優點,也各有極限性。越高級的模式復雜性越高,實現起來也越難。最近火熱的微服務架構,比起MVC,復雜度不知增加了多少倍。
我在實際項目中思考架構時,也不會想着要用哪種模式,我只思考現階段,以現有的人力資源和時間資源,如何才能更快更好地完成需求,適當考慮下如何為后期擴展或重構做准備。就說我前段時間分享的Android項目重構之路系列中講的那個架構,確切地說,都不屬於上面三種架構模式之一。
數據層的設計
一個App,從根本上來說,就是對數據的處理,包括數據從哪里來、數據如何組織、數據怎么展示,從職責上划分就是:數據管理、數據加工、數據展示。相對應的也就有了三層架構:數據層、業務層、展示層。本文就先講講數據層的設計。
數據層,是三層架構中的最底層,負責數據的管理。它主要的任務就是:
調用網絡API,獲取數據;
將數據緩存到本地;
將數據交付給上一層。
根據這三個任務,數據層可以再拆分為三層:網絡層、本地數據層、交付層。
網絡層
網絡層主要就是對網絡API的封裝。關於API的設計,該系列的第一篇文章接口的設計已經講過一些。關於如何封裝,可以參考Android項目重構之路系列的架構篇和實現篇,其中接口層和本文的網絡層是一樣的。
還有一些在前面的文章中沒有提及到的,在此做一些補充。
首先是不同網絡狀態的處理。當網絡不可用時,則不應該再去調用API;當網絡可用,但不是WIFI時,有些比較耗流量的操作也應該禁止,比如上傳和下載大文件;當網絡狀態不同時,還可以采用不同的網絡策略,比如,當網絡為WIFI時,當前API可以返回更多更全面的數據,還可以預先加載相關聯的其他API。
其次,為了節省流量,接口的設計上可以對數據進行簡化。例如,對於一些列表類的接口,可以這么設計:只返回更新的部分,比如,上一次請求返回了10條按時間排序的數據,第一條數據為最新的,id為101,當發起下一次請求時,將101的id作為參數調用API,API查到該id,發現該id之后又新增了兩條數據,API則只返回新增的這兩條數據。
另外,為了保證程序的健壯性,調用API時,對入參的合法性檢查也是很有必要的。而且,也應該定義好本地的錯誤碼和錯誤信息,保證每個錯誤都能正常解析。
本地數據層
本地數據層主要就是做緩存處理,這需要設計好一套緩存策略。設計緩存策略時,有幾個問題需要考慮清楚:
哪些需要緩存?哪些不需要緩存?
緩存在哪里?數據庫?文件?還是內存?
緩存時間多長?
哪些需要緩存?
將所有數據都緩存是不明智的,不同的數據應該有不同的緩存策略,比如一個電商App,首頁的商品列表數據應該緩存,而且緩存時間應該比較長,而每個商品的詳情數據就沒必要緩存或緩存時間很短。對於一份數據需不需要緩存,判斷標准可以是:用戶查看該數據的頻率高不高?首頁商品列表是用戶每次啟動都會看到的,而每個商品的詳情用戶最多只看幾次。
緩存在哪里?
從內存讀取數據是最快的,但內存非常有限。因此,內存一般只用來緩存使用頻率非常高的數據。
文件緩存主要就是圖片、音頻、視頻了。
數據庫可以保存大量數據,主要就是用於保存商品列表、聊天記錄之類的關系型數據。
然而,不管緩存在哪里,都需要限定好緩存的容量,要定期清理,不然會越積越多。
緩存時間多長?
首先,每份緩存數據都應該設置一個緩存的有效時間,有效期的起始時間以最后一次被調用的時間為准,當該數據長時間沒有再被調用到時,就應該從緩存中清理掉。
緩存的有效時間應該設多長呢?可以短至一分鍾,長至一星期甚至一個月,具體因數據而異。一般內存的緩存時間不宜太長,程序退出基本就要全部清理了。文件緩存可以設置保留一天或一個星期,可以每隔一天清理一次。數據庫緩存再久一些也無所謂,但最好還是不要超過一個月。
交付層
交付層其實就是一個向上層開放的交互接口層,是上層向數據層獲取數據的入口。上層向數據層請求數據,它是不關心數據層的數據是從緩存獲取還是從網絡獲取的,它只關心結果,數據層能給到它想要的數據結果就OK了。因此,交付層主要就是定義一堆開放的接口或協議。
如果接口或協議非常多,那么,將接口或協議按照模塊划分也是有必要的。比如微信,按模塊划分有:IM、公眾號、朋友圈、錢包、購物、游戲等等。模塊之間應該盡量相對獨立、松耦合。
業務層的設計
業務層其實並不復雜,但是大部分開發人員對其職責並沒有理解清楚,從而使其淪落為一個數據中轉站。我之前分享過的Android項目重構之路系列中提到的核心層,其實就是這里所講的業務層。但有不少讀者反映,他們在實際項目中就只是做一下參數檢查,然后直接調用API,與展示層對接的接口基本也與API的接口一致的。這樣,業務層無疑就已經變為了一個數據中轉站。
業務層的職責
所以,設計業務層之前,對業務層的職責要先真正理解清楚。這里,我舉兩個栗子說明一下。
第一個是新用戶注冊的例子。注冊時,界面上一般都會要求用戶輸入手機號、驗證碼、密碼和確認密碼。但是,API接口一般只會有三個參數:手機號、驗證碼和密碼,不會有確認密碼。因此,調用接口之前,密碼和確認密碼的一致性檢查是必須的。同時,也要檢查這些數據是否為空、手機號是否符合規范、驗證碼是否有效、密碼有沒有包含了特殊字符等。正確姿勢就是當所有檢查都通過了之后,才調用API接口。最后,調用注冊接口成功后,可能還要再調用一次登錄接口,並可能將用戶登錄信息緩存起來,方便用戶下次啟動應用時自動登錄。所有這些都屬於業務邏輯處理,也就是業務層的工作。
第二個是涉及用戶驗證的例子。比如,在一個電商App,當用戶瀏覽某個商品,點擊購買時,App首先會判斷用戶是否已經登錄,如未登錄,則會跳轉到登錄頁面讓用戶先登錄。如果已經登錄,但token已經過期,那需要先去獲取新的token,之后才能進行下一步的購物操作。這些邏輯處理,也是業務層的工作。
因此,簡單點說,業務層就是處理業務邏輯,包括數據的檢查、業務分支的處理等。比如上面第二個例子,可能很多人就會將用戶是否已經登錄的判斷直接在界面上做處理,當確認登錄后,token也是有效的之后,才調用業務層做購買商品的操作,這就是導致業務層淪落為API的數據中轉站的直接表現。
業務層的交互
只有真正理解了業務層的職責之后,才能有效地設計業務層與外層的交互接口。
業務層向下,與數據層交互;向上,與展示層交互。
與數據層交互只是調用數據層的接口獲取數據,而與展示層交互則需要提供接口給展示層調用。因為業務處理一般屬於比較耗時的操作,主要在於底層的網絡請求比較耗時,所以提供給展示層的接口數據結果應該以異步的方式提供,因此,接口上就需要提供個回調參數,返回業務處理之后的結果。我之前分享過的Android項目重構之路:實現篇有講到一種實現方式,可參考。
展示層的設計
三層架構中,數據層和業務層都已經做過了簡單的分享,最后,就剩下展示層了。本篇就給各位分享下我在展示層設計方面的一些經驗心得。
展示層是三層架構中最復雜的一層了,需要考慮的包括但不限於界面布局、屏幕適配、文字大小、顏色、圖片資源、提示信息、動畫等等。展示層也是變化最頻繁的一個層面,每天改得最多的就是界面了。因此,展示層也是最容易變得混亂不堪的一個層面。一個良好的展示層,應該有較好的可讀性、健壯性、維護性、擴展性。
三原則
我在Android項目重構之路:界面篇中提到過三個原則,要設計好展示層,至少需要遵循好這三條基本的原則:
保持規范性:定義好開發規范,包括書寫規范、命名規范、注釋規范等,並按照規范嚴格執行;
保持單一性:布局就只做布局,內容就只做內容,各自分離好,每個方法、每個類,也只做一件事情;
保持簡潔性:保持代碼和結構的簡潔,每個方法,每個類,每個包,每個文件,都不要塞太多代碼或資源,感覺多了就應該拆分。
關於這三個原則詳細的解說,界面篇已經講過的,我這里就不再重復。在此,我只做些補充。
關於規范,Android方面,我已經分享過一套Android技術積累:開發規范,主要分為書寫規范、命名規范、注釋規范三部分。iOS方面,蘋果已經有一套Coding Guidelines,主要屬於命名方面的規范。當我們制定自己的開發規范時,首先就要遵守蘋果的這份規范,在此基礎上再加上自己的規范。
最重要的不是開發規范的制定,而是開發規范的執行。如果沒有按照開發規范去執行,那開發規范就等於形同虛設,那代碼混亂的問題依然得不到解決。
另外,Android系統本身已經對資源進行了很好的分離,字符串、顏色值、尺寸大小、圖片、動畫等等都用不同的xml文件定義。而iOS系統在這方面就遜色很多,只能自己實現,其中一種實現方案就是通過plist文件的方式實現和Android一樣的機制。
工程結構
工程結構其實就是模塊的划分,無非分為兩類:按業務划分或按組件划分。
比如一個電商App,可能會有首頁、附近、分類、我的四大模塊,工程結構也根據這四大模塊進行划分,Android可能就分為了四個模塊包:
com.domain.home 首頁
com.domain.nearby 附近
com.domain.category 分類
com.domain.user 我的
同樣的,iOS則分為四個分組:home、nearby、category、user。
之后,每個模塊下相應的頁面就放入相應的模塊。那么,問題來了,商品詳情頁應該屬於哪個模塊呢?首頁會跳轉到商品詳情頁,附近也會跳轉到商品詳情頁,分類也會跳轉到商品詳情頁,用戶查看訂單時也能跳轉到商品詳情頁。有些頁面,並不能很明顯的區分出屬於哪個模塊的。我接手過的,按業務划分的二手項目中(即不是由我搭建的項目),我要找一個頁面時,我認為應該屬於A模塊的,但在A模塊卻找不到,問了同事才知道在B模塊。類似的情況出現過很多次,而且不止出現在我身上,對業務不熟悉的開發人員都會出現這個問題。而且,對業務不熟悉的開發人員開發新的頁面或功能時,如果對業務理解不深,划分出錯,那也將成為問題,其他人員要找該頁面時更難找到了。
因此,我更喜歡按組件划分的工程結構,因為組件每個人都懂,不管對業務熟不熟悉,查找起來都明顯方便很多。Android按組件划分大致如下:
com.domain.activities 存放所有的Activity
com.domain.fragments 存放所有的Fragment
com.domain.adapters 存放所有的Adapter
com.domain.services 存放所有的Service
com.domain.views 存放所有的自定義View
com.domain.utils 存放所有的工具類
iOS的分組則大致如下:
controllers 存放所有ViewController
cells 存放所有Cell,包括TableViewCell和CollectionViewCell
views 存放所有自定義控件或對系統控件的擴展
utils 存放所有的工具類
基類的定義
Android的Activity、Fragment、Adapter,iOS的ViewController,分別定義一個基類,將大部分通用的變量和方法定義和封裝好,將減少很多工作量,而且有了統一的設置,也會減少代碼的混亂。比如我在Android項目重構之路:實現篇中提到的KBaseActivity和KBaseAdapter的實現就是例子,當然還可以抽離出更多變量和方法。
每個Activity的onCreate()方法,一般分為三步:
變量的初始化;
View的初始化;
加載數據。
因此,其實可以將onCreate()方法拆分成三個方法:
initVariables()
initViews()
loadData()
在基類中將這三個方法定義為抽象方法,由子類去實現,這樣,子類就不需要實現onCreate()方法了,只要實現更細化的上述三個方法即可。
iOS的ViewController也是同樣的方式,這里就不重復了。