高质量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,如果真的超级复杂那可以反思下这个复杂的逻辑应不应该放在前端,能不能放在后端呢?