高質量App的架構設計與思考!
最近在做一功能不大、業務也不復雜的小眾App,以往做App是發現自己從來沒有考慮過一些架構方面的問題,只是按照自己以往的習慣去寫程式碼,忽略了App的設計。本次分享主要包含一些開發App的小經驗和技巧,來一次App開發與設計的分享。
先和分享下一下實體類的設計與組織形式
實體類的組織
在做App開發的時候有很多的實體類,專案越複雜實體類就會越多,經過我的一番思考大致這可以將實體分為以下幾大數:
- 面向資料庫的
- 服務端返回的資料實體
- 用於渲染View的實體(使用Databinding)
一般情況下實體類的操作會經過以下步驟:
- App請求伺服器獲取資料
- 將資料存入資料庫(可選)
- 渲染頁面展示資料
現在的實體的產生只用在請求伺服器資料的時候才需要新建,後續的資料庫、頁面渲染其實是可以使用一套實體:
先不說這樣做的行不行,首先三個地方使用同一實體就會引起欄位歧義比如伺服器資料有Id、本地資料也有Id,那兩個id欄位就有衝突了不得不改欄位名。
另一種情況渲染和資料本身並不會一一對應,有時候後端資料給的是一個純數字而前端頁面顯示的是字串兩個都對應不上,強行放在一起會起來更多的問題。
所為實體類的的正確組織形式應該是:相互隔離、互不干擾:
資料實體的在渲染之前都需要準備好,比如在ViewModel中將int型的資料轉換成文字型的資料然後再使用Databinding+頁面渲染實體來渲染頁面。
優雅的處理網路資料
現在Android開發使用的網路庫大部分都是Okhttp + Retrofit
,使用Retrofit網路互動變的非常簡單一個Service介面就能搞定一切,美茲茲~~,現在大部分後端返回的資料都會是以下形式:
{
"code":0,
"data": {},
"msg": ""
}
雖然不能涵蓋所有,但還是可以非常讚的資料、訊息、成功與否啥都有!對於前面主要是關注data
欄位,其他msg
、code
等都屬於輔助欄位。前端對應的實體物件應該是這樣的(假程式碼):
public class ApiResponse<T> { private int code; private T data; private String msg; }
對應的Service那就得定義成這樣(使用了RxJava):
public intface UserService {
@GET("/xx/{id}")
Single<ApiResponse<UserInfo> getUserInfoById(@Path("id") Long userId);
}
從介面中可以看出來,方法的返回值就包了幾層,如果要拿data
欄位需要經過:ApiResponse -> UserInfo
,而且在拿之前還要判斷code
欄位:
...
if(ApiResponse.code == 0){
UserInfo info = ApiResponse.getData();
}
...
為了消除這些冗餘的程式碼可以使用CallAdapter
來使Service方法返回的資料直接就是實體類:
public intface UserService {
@GET("/xx/{id}")
Single<UserInfo> getUserInfoById(@Path("id") Long userId);
}
CallAdapter
的程式碼就不貼了,可以自行查詢。這樣做帶來的另外一個問題就是業務程式碼如何判斷介面是否成功或失敗,前端必需友好的把錯誤提示給使用者而不是一直搞個Loading在那裡瞎轉~~。現階段最方便的的錯誤傳遞方式是使用Java異常,前端可以定義業務異常或網路異常:
public class BizException extends RuntimeException {
...
}
在CallAdapter
中檢查ApiResponse的返回值是否成功:
if(!ApiResponse != 0){
throw new BizExcepiton(ApiResponse);
}
如果後端返回業務異常那前端就對應丟擲一個BizExcepiton
,如果是http錯誤如:404、400那可以丟擲HttpException
。除了BizExcepiton
和HttpException
外還可使用特定的異常比如後端返回密碼錯誤異常:
public class InvalidPasswordException extends BizException {
...
}
如需特殊處理,也可以滿足要求。
健壯的資料層
現在很多應用都開發使用MVVM開發模式資料層都使用Repository
來表示,面向資料驅動的開發模式,頁面變化都需要隨著資料變更而更新,資料發生變化然後頁面再做出響應。Repository的拆分要細一點,不建議簡單的弄個UserRepository
包含登陸、註冊、更新密碼等等操作,設計Repository
的一些想法:
- 面向介面程式設計
- 保持單一原則
- 功能邊界要清晰(如:登陸、註冊可以分開)
- 業務邏輯儘可能的少(複雜的業務考慮Presenter)
一個判斷是否是好的設計的辦法可以這樣:一個登陸頁面從Activive/Fragment到ViewModel再到Repository,有沒有多餘的程式碼。比如上面說的UserRepository
包含登陸、註冊但是在一個登陸頁面就不需要有註冊功能,從登陸頁面上來看註冊的程式碼就是多餘的(有些App登陸/註冊在一個頁面的~~)。
一個包含登陸、註冊的UserRepository
簡單圖:
另外一點是儘量將repository使用到的一些東西集中管理,可引入一個基礎的repository:
public class SimpleRepository {
protected final <T> T getService(Class<T> clz){
return Services.getService(clz);
}
}
做為SimpleRepository
的子類,就不需要考慮從哪裡獲取service的問題。
簡潔的UI層
UI層面可以分為ViewModel和View(Activity/Fragment), View的職責應當只有二點:
- 展示業務資料
- 收集業務資料
例如一些資料的組織、判斷都不應該出現在View中比如:
if (Strings.isNullOrEmpty(phone)) {
...
return;
}
if (Strings.isNullOrEmpty(pwd)) {
...
return;
}
像上面這類的程式碼都不應該出現在View中,而在放置在ViewModel裡面,View只收集使用者資料傳遞給ViewModel由它來進行資料校驗。再比如像這樣的if/else程式碼也應該放置在ViewModel中:
int age = 10;
String desc = "";
if(age < 18){
desc = "青年";
}else if(age < 29){
desc = "中年";
}
如果資料的顯示和資料的收集過多,建議使用Databinding來進行雙向繫結資料。再搭配LiveData
使View作為觀察者實時監聽資料變化:
registerViewModel.getRegistryResult().observe(this, new SimpleObserver<RegistryInfo>(this));
一旦資料發生變化LiveData
就會通知Observer更新,通過DataBinding更新各個頁面資料。
再說ViewModel應該只包含一些簡單的判斷、檢查、打通資料的程式碼,如果業務過於複雜可以考慮加Presetner,如果真的超級複雜那可以反思下這個複雜的邏輯應不應該放在前端,能不能放在後端呢