Android项目重构之路:架构篇

原创文章,转载请注明:转载自Keegan小钢
并标明原文链接:http://keeganlee.me/post/android/20150605
微信订阅号:keeganlee_me
写于2015-06-05


Android项目重构之路:架构篇
Android项目重构之路:界面篇
Android项目重构之路:实现篇


去年10月底换到了新公司,做移动研发组的负责人,刚开始接手android项目时,发现该项目真的是一团糟。首先是其架构,是按功能模块进行划分的,本来按模块划分也挺好的,可是,他却分得太细,总共分为了17个模块,而好几个模块也就只有两三个类而已。但应用本身其实比较简单,要按功能模块来分的话,最多五个模块就够了。另外,有好多模块划分也很模糊,也有很多类按其功能其实可以属于多个模块的,也有些类定义不明确,做了不该做的事。有时候,我要找一个界面的Activity,按照其功能应该属于A模块的,可是在A模块里却找不到,于是,我只好去AndroidManifest文件里找了,找到才发现原来在B模块里。也有时候,我要找另一个界面的Activity,可我看遍了所有模块,也没看出这个界面应该属于哪个模块,没法子,又只能去AndroidManifest文件里找了,找到才发现竟然在C模块里。代码也是又乱又臭,导致出现一大堆bug又不好找,改好一个bug又出现另一个。整个项目从架构到代码都是又臭又乱,开发人员只是不停地改bug,根本没法做新功能,更别谈扩展了。当时,公司已经有为不同客户定制化app的需求,而现有的架构完全无法满足这样的需求。因此,我决定重构,搭建一个易维护、易扩展、可定制的项目。

我将项目分为了四个层级:模型层、接口层、核心层、界面层。模型层定义了所有的模型;接口层封装了服务器提供的API;核心层处理所有业务逻辑;界面层就处理界面的展示。几个层级之间的关系如下图所示:

下面展开说明具体的每个层次:

接口层

接口层封装了网络底层的API,并提供给核心层调用。刚开始,为了简单,该层的核心类我只定义了4个:

  1. PostEngine,请求引擎类,对请求的发送和响应结果进行处理;
  2. Response,响应类,封装了Http请求返回的数据结构;
  3. Api,接口类,定义了所有接口方法;
  4. ApiImpl,接口实现类,实现所有接口方法。

PostEngine将请求封装好发送到服务器,并对响应结果的json数据转化为Response对象返回。Response其实就是响应结果的json数据实体类,json数据是有固定结构的,分为三类,如下:

{"event": "0", "msg": "success"}
{"event": "0", "msg": "success", "obj":{...}}
{"event": "0", "msg": "success", "objList":[{...}, {...}], "currentPage": 1, "pageSize": 20, "maxCount": 2, "maxPage": 1}

event为返回码,0表示成功,msg则是返回的信息,obj是返回的单个数据对象,objList是返回的数据对象数组,currentPage表示当前页,pageSize则表示当前页最多对象数量,maxCount表示对象数据总量,maxPage表示总共有多少页。根据此结构,Response基本的定义如下:

public class Response<T> {
    private String event;
    private String msg;
    private T obj;
    private T objList;
    private int currentPage;
    private int pageSize;
    private int maxCount;
    private int maxPage;

    //getter和setter方法
    ...    
}

每个属性名称都要与json数据对应的名称相一致,否则无法转化。obj和objList用泛型则可以转化为相应的具体对象了。

Api接口类定义了所有的接口方法,方法定义类似如下:

public Response<Void> login(String loginName, String password);
public Response<VersionInfo> getLastVersion();
public Response<List<Coupon>> listNewCoupon(int currentPage, int pageSize);

ApiImpl则实现所有Api接口了,实现代码类似如下:

@Override
public Response<Void> login(String loginName, String password) {
    try {
        String method = Api.LOGIN;
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("loginName", loginName));
        params.add(new BasicNameValuePair("password", EncryptUtil.makeMD5(password)));
        TypeToken<Response<Void>> typeToken = new TypeToken<Response<Void>>(){};
        return postEngine.specialHandle(method, params, typeToken);
    } catch (Exception e) {
        //异常处理
    }
}

实现中将请求参数和返回的类型定义好,调用PostEngine对象进行处理。
接口层的核心基本上就是这些了。

核心层

核心层介于接口层和界面层之间,主要处理业务逻辑,集中做数据处理。向上,给界面层提供数据处理的接口,称为Action;向下,调用接口层向服务器请求数据。向上的Action中定义的方法类似如下:

public void getCustomer(String loginName, CallbackListener<Customer> callbackListener);

这是一个获取用户信息的方法,因为需要向接口层请求服务器Api数据,所以添加了callback监听器,在callback里对返回的数据结果进行操作。CallbackListener就定义了一个成功和一个失败的方法,代码如下:

public interface CallbackListener<T> {
    /**
     * 请求的响应结果为成功时调用
     * @param data  返回的数据
     */
    public void onSuccess(T data);

    /**
     * 请求的响应结果为失败时调用
     * @param errorEvent 错误码
     * @param message    错误信息
     */
    public void onFailure(String errorEvent, String message);
}

接口的实现基本分为两步:

  1. 参数检查,检查参数的合法性,包括非空检查、边界检查、有效性检查等;
  2. 使用异步任务调用接口层的Api,返回响应结果。

需要注意的是,Action是面向界面的,界面上的数据可能需要根据不同情况调用不同的Api。
后续扩展可以在这里添加缓存,但也要视不同情况而定,比如有些变化太快的数据,添加缓存就不太适合了。

界面层

界面层处于最上层,其核心就是负责界面的展示。
因为公司有为不同商户定制不同app的需求,因此,这里就需要建立多个app的界面,这是一个很麻烦的事情,还好,Android Studio提供了很方便的方法可以大大减少工作量,主要通过设置Gradle,不同app可以添加不同的productFlavors。
界面层package的定义我也并不按照旧版的功能模块划分,而根据不同类型划分,主要分为以下几个包:

其中,activity、adapter、fragment各自都有一个基类,做统一的处理,比如定义了一些共用的常量、对象和方法等。
界面层是最复杂,最容易变得混乱不堪,最容易出问题的层级。所以,从架构到代码,很多东西都需要设计好,以及规范好,才能保证程序易维护、易扩展。后续的文章里将会详细分享下我在这方面的经验。

模型层

模型层横跨所有层级,封装了所有数据实体类,基本上也是跟json的obj数据一致的,在接口层会将obj转化为相应的实体类,再通过Action传到界面层。另外,模型层还定义了一些常量,比如用户状态、支付状态等。在Api里返回的是用1、2、3这样定义的,而我则用枚举类定义了这些状态。用枚举类定义,就可以避免了边界的检查,同时也更明了,谁会记得那么多1、2、3都代表什么状态呢。然而用枚举类定义的话,就必须能将1、2、3转化为相应的枚举常量。这里,我提供两种实现方式:
1.使用gson的@SerializedName标签,比如0为FALSE,1为TRUE,则可以如下定义:

public enum BooleanType {
    @SerializedName("0")
    FALSE,
    @SerializedName("1")
    TRUE
}

2.通过定义一个value,如下:

public enum BooleanType {
    FALSE("0"),
    TRUE("1");

    private String value;

    BooleanType(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

通过gson的方式,直接访问TRUE或FALSE就会自动序列化为1或0;如果通过第二种方式,因为没有序列化,则需要通过getValue方式获取1或0。

结束

以上就是最基本的架构了,讲得比较简单,只列了几个核心的东西。并没有进一步去扩展,扩展是下一步的事情了,后续的文章里会慢慢展开。


扫描以下二维码即可关注订阅号。

  • bdll2015-06-09 14:45

    很好,楼主总结的全面,有几个个人建议
    1、接口层的返回objList我觉得是多余了,多多少少包含了业务逻辑,有obj我觉得足够了
    2、为什么是在接口层转化相应的实体类?应该由核心层吧,这样接口层不会有什么耦合

    如果楼主把基础架构开源讨论就更好了

  • keegan7042015-06-09 17:02

    @bdll,谢谢你的建议。
    1、首先obj和objList并不是我定义的,是我们的后台人员早定义好的;当然,如果由我来定义,我会更喜欢命名为data,那样显得更明了。
    2、首先,接口层是要根据Api协议文档去封装接口的,包括哪个接口返回什么对象,如果转化移到了核心层,那等于核心层也要去看Api协议文档,这样才知道哪个接口要转化为哪个对象,这样就达不到分层的作用了。核心层不应该还要去看协议文档,这是接口层做的事情。
    最后,开源的建议很好,这样的话我还需要再花些时间将其调整为通用的架构才行。

  • wjj2015-06-10 11:53

    谢谢楼主分享,现在我们的项目主要分为接口层,界面层,模型层,我们将核心层的代码写在了界面层,界面层做的就是拿到数据,检查数据,请求接口,显示数据更新ui,现在想代码重构,不太明白核心层这块怎么搞,菜鸟一枚,谢谢!

  • keegan7042015-06-10 11:58

    @wjj 你可以把拿数据,检查数据,请求接口都放到核心层,显示数据更新UI才在界面层做。

  • wjj2015-06-10 12:49

    @keegan704 核心层这块写个DAO,DAOImpl,接口中写个监听回调(失败,成功,空),核心层这块主要就是数据检查,请求接口,回调界面层。(但有个困惑,我们以前将这块放在界面层的原因是Android这块逻辑不怎么多,暂时不涉及缓存什么的,目前这种情况觉得再分个层累赘,核心层这块如果这么做的话基本就是个检查数据,回调界面层更新ui)

  • keegan7042015-06-10 13:54

    @wjj 你觉得Android这块逻辑不多,那可能是你们有很多东西还没做,或者是现在代码还不多。等东西多了,可能就会觉得有些乱了。分层就是为了能有更好的可读性、维护性、扩展性,减低耦合性。界面层就应该只关心界面如何展示,对于数据怎么检查,是从缓存还是网络取数据,这些交给核心层去做就好了。另外,如果你们团队以后扩展了,分层好的话那就可以有人专门做界面,有人专门做数据逻辑。

  • wjj2015-06-10 15:58

    谢谢,希望楼主有时间把基础架构开源学习学习

  • Robin2015-06-18 14:46

    @keegan704 楼主的架构网络请求能用线程吗?怎么用?

  • keegan7042015-06-18 15:02

    @Robin 在核心层用自定义的异步任务请求网络,异步任务继承自AsyncTask完成的

  • Robin2015-06-18 15:02

    @keegan704 楼主的架构网络请求能用线程吗?怎么用?

  • Robin2015-06-18 15:05

    @keegan704 哦,谢谢,刚才刷新界面,自动重发了一遍问题,可以删掉

  • ssd2015-06-23 16:53

    新手来阅读,谢谢!

  • Gao2015-06-26 07:55

    非常期待楼主能做成一个通用的架构,学习一下

  • keegan7042015-06-26 09:54

    @Gao 授人以鱼不如授人以渔,我写这个系列的文章是为了教一些新手怎么去构建,而不是直接拿来用。现在也只在写实现篇,写完也会将源代码开源。

  • Gao2015-06-26 09:57

    @keegan704 倒不是直接拿来用,主要是有代码更能深入理解,期待楼主

  • aspen2015-07-03 14:31

    为什么不考虑使用现有的开源框架呢?

  • keegan7042015-07-05 18:21

    @aspen 你说什么框架呢?

  • cuixbo2015-07-15 16:47

    请问你们的service,receiver,application放在哪些包里呢

  • enjoyzhao2015-07-16 19:46

    先对博主赞一个,我大致搜索了好多都是介绍单个实例的,只有这个博上面介绍项目框架的,给了我很大帮助。不过我最近看到一个类似也不错的框架,分享给有需要的人 共同学习 实例地址:https://github.com/saulmm/Material-Movies

  • keegan7042015-07-18 17:01

    @cuixbo service,receiver,application这些也是android组件元素,当然和activity一样是放在界面层。

  • keegan7042015-07-18 17:05

    @enjoyzhao Material-Movies我看了一下,其框架的确也挺不错,可以学习学习

  • core2015-07-21 17:51

    先对博主无私的分享点个赞,我感觉这个架构设计还是有少许瑕疵,把这个架构放在一个项目上看不出问题,但有多个项目去开发使用接口类,我感觉移植性可能会差点。如果吧接口层与模型层解偶后可能会更好点!一点建议,仅供参考!

  • keegan7042015-07-25 21:13

    @core 谢谢你的建议。我们现有的架构已经是支持多个项目的,因为每个层次都是分离的,并不存在移植性的问题。不过这系列文章没有提到怎么适配多个项目的情况。这系列文章只是讲架构的一些基础东西,所以没有考虑那么多,根据实际情况也是需要做些调整的。这系列文章更重要的其实是想传达一些如何架构的思想。

  • fly-tiger2015-08-14 01:52

    真不错,Response 这个看不太懂,返回的Type类型是不是多余的,建议楼主把登录啊,版本检查,错误检查,分享功能,拿数据等等这些抽象出来弄个通用框架我们深入了解一下

  • chenzj2015-08-28 19:48

    不知道博主有没有接触过Eventbus.如果针对架构方面来讲.处理网络层次可以通过他来解耦.不捆绑Act/Fra/Service.大概方式为在baseAct/baseFra/baseService中进行Event的注册.并且在onMainThread的方法中传baseEvent事件.执行期performance(Context context,Fragement fragment)方法.有一個常驻的EventManger来单例出来负责post/注册/注销事件.有一個baseEvent抽象类.如创建一個HelloEvent.同时会创建与其逻辑相关的接口(用以实际的Act/Fra/Service调用).HelloInterface.当post HelloEvent事件的时候.在其实现BaseEvent的performance方法.通过形参传输过来的Act/Fra.判断该传过来的Act/Fra是否实现了和我们的HelloInterface.同时把Act/Fra强转为HelloInterface然后调用其定义的方法.这样完全一套逻辑代码就完成了很好的sdk开发.话说的有点多.有点乱.不知道博主能否理解我的表述.

  • keegan7042015-08-29 10:55

    @chenzj 的确是有点乱,但我看不出“处理网络层次可以通过他来解耦”是怎么解耦的?EventBus本身是一个组件之间的通信库,怎么处理网络层次呢?

  • chenzj2015-08-29 11:20

    @keegan704 其实刚好是在我公司项目实际运用了.因为我可能多个Act/Fra都需要访问同一个接口.每次如果用类似博主的方式回调回来再处理.相对来讲感觉在不同的Act/Fra中要写不少代码.如果类似我上述讲的方式.如果我们是在做sdk.我们使用的时候只需要调用一下网络接口的方法.然后同时实现对应逻辑的接口即可.甚至Eventbus本来就在主线程中跑.我们不需要类似平常回调那样又在Handler中进行处理.相关数据的处理全部都在我们sdk中实现了.使用者只需要收到List/Bean后对UI进行改动即可.个人感觉这样用的会挺好的样子..

  • keegan7042015-08-29 14:32

    @chenzj 我还是不清楚你们具体是怎样的实现,我的理解就是,你们这套东西更多的是界面层的优化。如果有兴趣细聊,可以发信息到我的微信订阅号。

  • peak2015-09-05 11:29

    "模型层横跨所有层级"
    -博主没有理解层的概念

  • keegan7042015-09-05 11:32

    @peak 那你解释下层的概念?

  • peak2015-09-05 11:42

    可以参考tcp/ip协议分层的架构:
    1.每一层有自己的职责。
    2.上层不知道下层的实现细节,只知道下层提供的功能接口。
    3.下层不知道上层的存在。

    所以不存在有一层可以横跨所有层级。
    你对model层的理解更像是一个模块,所有的其他模块都可以访问依赖,这种方式很常见但不可靠。model layer是一个app架构最重要的一环,如果所有的类都对model layer有读写权限,model layer相当于不存在,只是把model的类放到了一个工程目录里。

  • peak2015-09-05 12:31

    再啰嗦几句。
    我在网上翻过很多关于架构的文章,android也好,iOS也好,谈的更多的都是对工程结构的划分,涉及架构的部分非常少。
    很多移动项目都有自己的架构方式,很难分出好坏高低。但只要谈到架构,都不是简单分个层,划个模块就了事。架构是一种规范和强约束,是结合具体项目之后用工程的思维去搭建代码的方式。要符合所有组员包括架构师自己的理解,大家能认同在这一套方式之下一起开发搭建业务。这种前提之下,代码是谁写的并不重要,所有的代码理解起来都像自己写的一样自然。

    个人觉得一个好的架构要能清晰的回答以下几个问题:

    1. 如何让model layer稳定,一致,可靠?
      a. 稳定是指限制model的写权限,如果一个model向全局变量一样谁都可以改,调试维护起来必然麻烦。
      b. 一致性,应用层不同模块由相同的model驱动。不能出现activity A界面更新了,activity B展示的还是老数据。
      c. 可靠,model与db的交互方式,所有的增删改都不能丢失。

    2. 如何让应用层代码合理的解耦内聚?
      无论是iOS的controller还是android的activity,都容易变成fat MVC。怎么样找到一种方式去拆分这部分代码很重要。如果一个工程师能清楚的知道他每一个函数该放到哪一个类,这样应用层才好维护。 关于这一块有很多成熟的方案了,mvc,mvvm,mvp等等,但这些是方案,细节还需要架构师自己敲定。

    3. 要有清晰的data flow。
      一个app说到底是关于data的变化和展现。从用户输入采集数据,上报服务器,加工再展示。这个流程能不能在你的架构里看出清晰的data flow很重要。工程师在遇到bug的时候第一反应是查数据是不是出了问题,头脑里有data flow就能快速的定位问题。

    希望博主后续文章能阐述下上面几个问题。

  • keegan7042015-09-05 15:49

    @peak Thx for ur questions! 问题很有深度。
    首先,关于model层,更准确地来说,的确是一个模块,之所以叫层,主要是因为它和其他三个层属于平级的概念,所以就简单点也称为了层。
    接着,我想说说目前的一些现状和看法。就如你说的,网上关于移动项目的架构方面的文章非常少,更多的只是说说工程结构而已。为什么会这样呢?主要是因为目前大部分的移动项目还是比较小,开发团队的人员也不多,所以并不需要考虑太多解耦、扩展之类的问题,同样也不需要谈什么架构了。另外,在移动端这一块,懂得架构的人也不多。而且,移动端的架构,并不是将后端架构直接移植过来就适用的。因此,目前对移动端架构的需求其实还是比较小的。但是,随着移动技术的发展,像淘宝、微信之类的超级app会越来越多,对移动端架构的需求也会越来越多,分层解耦、自动化测试、持续交付等等需求会越来越多。在这样的大背景下,广大的移动端程序猿们就有必要学学移动端的架构。而我这三篇系列文章,只是移动端架构的入门篇,力求简单,而没有考虑太多优化。
    好了,扯得有点多,最后再来说说你的三个问题:
    1、目前这个入门架构,model layer的稳定、一致、可靠性还是有保障的,model的数据是在接口层写入的,再从API传到数据层再传到界面层,中间也不会有丢失。确切地说,model layer的稳定、一致、可靠性是受制于API的,因为这个架构的model是跟API返回的json数据模型一致的。
    2、为什么要抽出一个核心层,主要目的就是为了解耦activity,这跟MVP的思想是类似的。
    3、data flow也是清晰的,从界面层,到核心层,最后到接口层,一条线下来,哪里出的问题是很容易定位的。
    欢迎继续讨论~~

  • vudiy2015-09-14 10:03

    感谢楼主的分享,以及各位评论者提供的意见,都是学习的资料,谢大家

  • loutao2015-09-14 16:30

    學習到了,必須點贊!

  • river2015-09-30 15:59

    要是有demo就好了

  • river2015-09-30 15:59

    要是有demo就好了,楼主什么时候给个demo,方便理解哈

  • keegan7042015-09-30 16:17

    @river Demo在github上,地址在实现篇最后

  • 凯子哥2015-10-08 21:38

    恩恩,看了有收获

  • grass2015-10-14 11:02

    写的很好,讨论非常精彩,@peak问得问题很有针对性

  • 刚子2015-10-22 13:57

    写的真好,楼主,可以加你为好友吗?我的qq是328584523

  • 刚子2015-10-22 14:11

    楼主,我问下那个对网络请求的那个PostEngine如果是用于请求,然后页面刷新的怎么办,还有就是子线程中的处理

  • keegan7042015-10-22 15:34

    @刚子 PostEngine只是对请求的封装处理,页面刷新和子线程处理这些是表示层的事,跟它没有直接关系的啊~有问题也可以微信公众号联系我

  • chn2015-11-04 15:24

    经常用枚举的话,性能开销大,楼主怎么看?

  • keegan7042015-11-04 15:41

    @chn 用枚举并不会带来什么性能问题啊,你的项目里枚举难道会多过于其他类吗?用了那么多类,几百上千个类,性能开销大吗?

  • doney2015-11-05 18:15

    同感,这个obj和ObjList应该可以合并成 T data来用才比较合适。

  • keegan7042015-11-06 09:26

    @doney 我也想的,但是我们的接口本身已经是分开的,我也没权利去把它合并啊~~

  • petma2015-11-11 09:42

    现在一般都用android的注解来取代枚举。现在都不推荐使用枚举了

  • yizi2015-11-12 11:26

    我敢说,作者肯定是从java后台转过来的,这些我个人觉得在安卓里面做不是很合适

  • keegan7042015-11-12 12:03

    @yizi 我从09年开始做java web,从11年开始做android。不过,为什么觉得不是很合适呢?你至少也要说下理由啊?

  • yizi2015-11-12 15:44

    首先读的开头,找activity的那段觉得你找的太累了。找一个页面,有一个很简单的方法,就是在activity的基类中的onCreate方法里面,打印log--getClass().getName(),这样你每点开一个页面就可以直接看到当前activity的包名加类名。然后是是分类的那段,还是习惯以模块划分。

  • yizi2015-11-12 16:04

    另外不好意思,我那样随意论不太好,可以麻烦你删掉我这些评论吗。感谢作者的分享!

  • keegan7042015-11-15 20:30

    @yizi 不太明白你的评论?好像跟前面的问题无关?另外,评论是无法删除的

  • guanxp2015-11-19 08:43

    这种根据不同类型划分包的策略违反了共同重用原则,一个很明显的问题是各个包之间会存在很多原本不需要的import。

  • keegan7042015-11-19 09:35

    @guanxp 那你觉得该如何优化?

  • unknown2015-11-19 11:30

    我想说的是...移动本身是快速发展的,各种快速迭代.如果楼主这样的架构实现动态部署 代价会不会太大了? 与其这样分层,为何不如把模块的概念换成容器的概念? 而不是像楼主这样的水平开发模式.
    简单的描述一下我的看法:
    可以把水平转化成为垂直(常规的是界面,业务,通信,数据模型.把每个模块转化成容器,而每个容器又具有界面层业务层,模型层,通信层[通信层直接引用了一个pulic libry,解决代码重复的问题])
    而每个容器之间因为处于同级,是不允许直接访问的.可能需要一些设计模式来支撑一些数据的共享和访问,来达到容器的绝对独立,这是动态部署最核心的一部分.测试层面来说 也不会因为你改了容器a而影响其他容器。

  • 汉代风云2015-11-19 11:54

    平时写代码还是太随意,怎么方便怎么来,逻辑处理直接写在工具类里,哈哈

  • keegan7042015-11-19 13:50

    @unknown 我觉得我现在的这个架构才是垂直模式吧,垂直方向上的分层,而你说的容器模式才是水平上扩展的模式。其实,用哪种架构更合适,都要看具体项目的需求是怎样的。而我采用这种架构,是因为我们项目的需求在通信和业务逻辑层方面基本固定,但界面需要适配多个app,分层是最合适的。如果想QQ、微信、支付宝这样的,那用你说的容器模式则更适合,只是它们的架构也并不只是容器这么简单,也有用了插件化开发的模式。