Easy Clean architecture on Andro

作者: 小鄧子 | 来源:发表于2017-08-10 11:47 被阅读5725次

    在我这几年的学习和成长中,深刻的意识到搭建一个Android应用架构是件非常痛苦的事,它不仅要满足不断增长的业务需求,还要保证架构自身的整洁,这让事情变得非常具有挑战,但我们必须这样做,因为健壮的Android架构是一款优秀APP的基础。本文的代码示例可以从github中获得,仓库地址是android-easy-cleanarchitecture

    Why we need an architecture?

    Android入门要求始终不高,因为Android Framework会帮我们做很多事,甚至不需要通过深入的学习就能写出一个简单的APP,比如说在ActivityFragment中摆放几个View用来展示到屏幕上,后台耗时任务放在Service中执行,组件之间使用Broadcast传递数据,由此看来“人人都能成为Android工程师”,真的是这样吗?

    当然不是!!!

    如果我们如此天真的开始编程,迟早会为此付出代价。那些依赖关系混乱,灵活性不够高的代码将会成为我们最大的阻碍,任由发展的后果就是,导致项目一片狼藉,我们很难加入新的功能,只能对它进行重构甚至推翻重做。在开始编程前,我们不应该低估一个应用程序的复杂性,应该将你的APP看做一个拥有前端,后端和存储特性的复杂系统。

    另外,在软件工程领域,始终都有一些值得我们学习和遵守的原则,比如:单一职责原则依赖倒置原则避免副作用等等。Android Framework不会强制我们遵守这些原则,或者说它对我们没有任何限制,试想那些耦合紧密的实现类,处理大量业务逻辑的Activity或Fragment,随处可见的EventBus,难以阅读的数据流传递和混乱的回调逻辑等等,它们虽然不会导致系统马上崩溃,但随着项目的发展,它们会变得难以维护,甚至很难添加新的代码,这无疑会成为业务增长的可怕障碍。

    所以说,对于开发者们来讲,一个好的架构指导规范,至关重要。

    从事Android工作以来,我始终认为我们能将APP做的更好,我也遇到过很多好的坏的软件设计,自己也做过很多不同的尝试,我不断地吸取教训并做出改变,直到我遇到了Clean Architecture,我确定这就是我想要的,我决定使用它。本文的目标是分享我使用clean Architecture构建项目时所收获的经验,希望能够为你的项目改进带来灵感。

    Avoid God Activity

    可能是出于“快速迭代”,于是你集成了这个万能的Activity,它无所不能:

    • 管理自身生命周期(在正确的生命周期中处理任务)
    • 维持UI状态(配置变更时保存/回复视图状态)
    • 处理Intent(接收和发送正确的Intent)
    • 数据更新(与远程API同步数据,本地存储)
    • 线程切换
    • 业务逻辑
      ......

    甚至突破了所有的约束壁垒:在Android世界里面加入了业务代码;在BaseActivity中定义了所有子类可能用到的变量等等。它现在的确就是个“上帝”,方便且万能的“上帝”!

    随着项目的发展,它已经庞大到无法再添加代码了,于是为它写了很多帮助类,你想重构它:

    god activity

    不经意间,你已经埋下了黑色炸弹

    看上去,业务逻辑被转移到了帮助类中,Activity中的代码减少了,不再那么臃肿,帮助类缓解了“万能类”的压力,但随着项目的成长,业务的扩大,同时这些帮助类也变多,那个时候又要按照业务继续拆分它们,APIHelperThisAPIHelperThat等等。原来的问题又出现了,测试成本还在,维护成本好像又增加了,那些混乱并且难以复用的程序又回来了,我们的努力好像都白费了。

    然而你写这个万能类的初衷是什么,想快捷、方便的使用一些功能函数吗,尤其希望在子类中能够很快的拿到。

    当然,一部分人会根据不同的业务功能分离出不同的抽象类,但相对那种业务场景下,它们仍是万能的。

    无论什么理由这种创造“上帝类”的做法都应该尽量避免,我们不应该把重点放在编写那些大而全的类,而是投入精力去编写那些易于维护和测试的低耦合类,如果可以的话,最好不要让业务逻辑进入纯净的Android世界,这也是我一直努力的目标。

    Clean architecture and The Clean rule

    这种看起来像“洋葱”的环形图就是Clean Architecture,不同颜色的“环”代表了不同的系统结构,它们组成了整个系统,箭头则代表了依赖关系,

    关于它的组成细节,在这里就不做深入的介绍了,因为有太多的文章讲的比我好,比我详尽。另外值得一提的是architecture是面向软件设计的,它不应该做语言差异,而本文将主要讲述如何结合Clean Architecture构建你的Android应用程序。

    在使用clean架构搭建项目前,我阅读了大量的文章,并付诸了很多实践,我的收获很大,经验和教训告诉我一个架构的清晰和整洁离不开这三个原则:

    • 分层原则
    • 依赖原则
    • 抽象原则

    接下来我就分别阐述一下,我对这些原则的理解,以及背后的原因。

    分层原则

    首先,值得一提的是框架不会限制你对应用程序的具体分层,你可以拥有任意的层数,但是在Android中通常情况下我会划分为3层:

    • 外层:实现层
    • 中间层:接口适配层
    • 内层:业务逻辑层

    接下来,介绍下这三层所应包含的内容。

    实现层

    一句话:实现层就是Android框架层。这个地方应该是Android framework的具体实现,它应该包括所有Android的东西,也就是说这里的代码应该是解决Android问题的,是与平台特性相关的,是具体的实现细节,如,Activity的跳转,创建并加载Fragment,处理Intent或者开启Service等。

    接口适配层

    接口适配层的目的是连接业务逻辑与框架特定代码,担任外层与内层之间的桥梁。

    业务逻辑层

    最重要的是业务逻辑层,我们在这里解决所有业务逻辑,这一层不应该包含Android代码,应该能够在没有Android环境的情况下测试它,也就是说我们的业务逻辑能够被独立测试,开发和维护,这就是clean架构的主要好处。

    依赖规则

    依赖规则与箭头方向保持一致,外层”依赖“内层,这里所说的“依赖”并不是指你在gradle中编写的那些dependency语句,应该将它理解成“看到”或者“知道”,外层知道内层,相反内层不知道外层,或者说外层知道内层是如何定义抽象的,而内层却不知道外层是如何实现的。如前所述,内层包含业务逻辑,外层包含实现细节,结合依赖规则就是:业务逻辑既看不到也不知道实现细节

    对于项目工程来讲,具体的依赖方式完全取决于你。你可以将他们划入不同的包,通过包结构来管理它们,需要注意的是不要在内部包中使用外部包的代码。使用包来进行管理十分的简单,但同时也暴露了致命的问题,一旦有人不知道依赖规则,就可能写出错误的代码,因为这种管理方式不能阻止人们对依赖规则的破坏,所以我更倾向将他们归纳到不同的Android module中,调整module间的依赖关系,使内层代码根本无法知道外层的存在。

    另外值得一提的是,尽管没人能够阻止你跳过相邻的层去访问其它层的代码,但我还是强烈建议只与相邻层进行数据访问。

    抽象原则

    在依赖原则中,我已经暗示了抽象原则,顺着箭头方向由两边朝中间移动时,东西就越抽象,相对的,朝两边移动时,东西就越具体。这也是我一直反复强调的,内圈包含业务逻辑,外圈包含实现细节

    接下来我会用一个例子来解释抽象原则:

    在内层定一个抽象接口Notification,一方面,业务逻辑可以直接使用它来向用户显示通知,另一方面,我们也可以在外层实现该接口,使用Android framework提供的NotificationManager来显示通知。业务逻辑使用的只是通知接口,它不了解实现细节,不知道通知是如何实现的,甚至不知道实现细节的存在。

    这很好演示了如何使用抽象原则。当抽象与依赖结合后,就会发现使用抽象通知的业务逻辑看不到也不知道使用Android通知管理器的具体实现,这就是我们想要的:业务逻辑不会注意到具体的实现细节,更不知道它何时会改变。抽象原则很好的帮我们做到了这一点。

    Apply on Android

    按照上面提到的分层原则,我把项目分为了三层,也就是说它有三个Android module,如下图所示:

    Clean architecture modules

    Domain中定义业务逻辑规则,在UI中实现界面交互,Model则是业务逻辑的具体实现方式(Android framework)。箭头方向代表依赖关系,内层抽象,外层具体,外层知道内层,内层不了解外层。

    具体到Android中的框架结构如下图所示:

    clean architecture structure

    你可能有些困惑,为什么Domain指向Data?既然Domain包含业务逻辑,它就应该是应用程序的中心,它不应该依赖Model,按照前面提到的原则,Domain是抽象的,Model是具体的,应该是Model依赖Domain,而不是Domain依赖Model

    其实这很好理解,也是我始终强调的,这里所说的“依赖”并不是指配置在gradle中的dependency,你应该将它理解为“知道”,“了解”,“意识”,图中的箭头代表了调用关系,而非模块间的依赖关系。我们应该能够理解:抽象是理论,依赖是实践,抽象是应用的逻辑布局,依赖是应用的组合策略。对于框架结构的理解,我们应该跳出代码层面,不要局限在惯性思维中,否则很快就会陷入逻辑混乱的怪圈。

    与调用关系对应的就是数据流的走向:

    clean architecture data stream

    app中接受用户的行为,根据domain中定义的业务规则,访问model中的真实数据,然后依次返回,最终更新界面,这就是一个完整的数据流走向。

    为了更方便理解,我对项目进行了简单的拆解,并在图中加上了类的用例描述,它看起来就像这样:

    clean architecture UML

    对上图所表示内容做一下总结:

    首先,项目被分为三层:

    • app:UI,Presenter ...
    • domain:Entity,Use case,Repository ...
    • model:DB,API ...

    其次,更细节的子模块划分:

    UI

    视图,包含所有的Android控件,负责UI展示。

    Presenter

    处理用户交互,调用适当的业务逻辑,并将数据结果发送到UI进行渲染。也就是说Presenter将担任着接口适配层的责任,连接Android实现和业务逻辑,负责数据的传递和回调。

    Entity

    实体,也就是业务对象,是应用的核心,它代表了应用的主要功能,你应该能够通过查看这些应用来判断这款应用的功能,例如,如果你有一个新闻应用,这些实体将是体育、汽车或者财经等实体类。

    Use case

    用例,即interactor,也就是业务服务,是实体的扩展,同时也是业务逻辑的扩展。它们包含的逻辑并不仅针对于一个实体,而是能处理更多的实体。一个好的用例,应该可以用通俗的语言来描述所做的事情,例如,转账可以叫做TransferMoneyUseCase。

    Repository

    抽象的核心,它们应该被定义为接口,为UseCase提供相应的输入和输出,能够直接对实体进行CRUD等操作。或者它们可以暴露一些更复杂的操作行为,如过滤,聚合等,具体的实现细节可以由外层来实现。

    DB&API

    数据库和API的实现都应该放在这里,比如上面示例中,可以将DAO,Retrofit,json解析等放在这里。它们应该能够实现在Repository中定义的接口,是具体的实现细节,能够对实体类进行直接操作。

    Show code

    你可以像前面UML图中演示的那样,组合你的MVPViewMVPPresenter,让它们更容易被管理和维护。

    首先定义BaseViewBasePresenter,在BaseView中我是用了RxJavaObservable作为结果类型。:

    public interface BaseView<T> {
    
      void showData(Observable<T> data);
    
      void showError(String errorMessage);
    }
    
    public interface BasePresenter<V> {
    
      void attachView(V view);
    
      void detachView();
    }
    

    假设你有一个根据城市ID获取该城市已上映电影的需求,那么你可以这样组合你的MovieViewMoviePresenter接口:

    
    interface MovieContract {
    
      interface Presenter<Request, Result> extends BasePresenter<View<Result>> {
        void loadData(Request request);
      }
    
      interface View<Result> extends BaseView<Result> {
        void showProgress();
      }
    }
    

    泛型的加入,有效保证了数据的类型安全

    接下来实现你自己的XXXPresenterXXXView接口的实现类,就像这样:

    class MoviePresenterImp implements MovieContract.Presenter<MovieUseCase.Request, List<MovieEntity>> {
    
      @Override public void attachView(UserContract.View<List<MovieEntity>> view) {
         /*subscribe MovieUseCase and do some initialization*/
      }
    
      @Override public void detachView() {
        /*unsubscribe MovieUseCase and release resources*/
      }
    
      @Override public void loadData(MovieUseCase.Request request) {
         /*load data from MovieUseCase*/
      }
    
    }
    
    
    class MovieActivity extends AppCompatActivity implements MovieContract.View<List<MovieEntity>> {
    
      @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /*also initialize the corresponding presenter*/
      }
    
      @Override public void showData(Observable<List<MovieEntity>> data) {
        /*show data and hide progress*/
      }
    
      @Override public void showError(String errorMessage) {
        /*show error message and hide progress*/
      }
    
      @Override public void showProgress() {
        /*show progress*/
      }
    }
    
    

    关于示例中的UseCase.Request来自于Clean Architecture: Dynamic Parameters in Use Cases:在XXXUseCase中创建静态内部类Request作为动态请求参数的容器。其实这很好理解,而且也完全正确,因为UseCase就是你定义业务规则的地方,把业务(请求)条件业务规则定义组合在一起不仅容易理解也更方便管理。不过我会在下篇文章中介绍另一种动态参数方式,也是我一直在使用的。

    总结:

    我相信你和我一样,在搭建框架的过程中遭遇着各式各样的挑战,从错误中吸取教训,不断优化代码,调整依赖关系,甚至重新组织模块结构,这些你做出的改变都是想让架构变得更健壮,我们一直希望应用程序能够变得易开发易维护,这才是真正意义上的团队受益。

    不得不说,搭建应用架构的方式多种多样,而且我认为,没有万能的,一劳永逸的架构,它应该是不断迭代更新,适应业务的。所以说,你可以按照文中提供的思路,尝试着结合业务来构建你的应用程序。

    另外值得一提的是,如果你想做的更好,可以为你的项目加入模板化,组件化等策略,因为并没有说一个项目只能使用一种框架结构。: )

    最后,希望这篇文章能够对你有所帮助,如果你有其他更好的架构思路,欢迎分享或与我交流。

    Happy coding !

    相关文章

      网友评论

      • CH_DHY:逻辑清晰,部分目前理解还有困难,期待下一篇
      • mrwangyong:项目我 down 下来好多运行错误,请问楼主自己运行过了吗
        MrTrying: @mrwangyong 项目有用到dagger,先编译一下,再看看;慢慢排查
        mrwangyong:@小鄧子 呃呃呃 我改改
        小鄧子:@mrwangyong 因为它根本无法运行,demo 的存在是为了更好的理解文章。
      • ELC1020:棒棒棒
      • 青墨一隅:我感觉跟MVP差不多呢?只不过把mvp里面的model变的更抽象和更细化了
      • 王怀智:为瑞顾得:smiley:
      • boredream:这样看的话,MVP和CleanArchitecture是一个意思嘛?
        青墨一隅:我感觉差不多只不过把mvp里面的model变的更抽象和更细化了
      • 宗仁:```
        组件之间使用Broadcast传递数组
        ```
        应该是数据吧
        小鄧子:已修改,谢谢你的指出
      • evanwo:很不错,每次看你博客都有收获
      • xiaobailong24:赞一个,不错的总结,也可以看看最新的MVVM和Android Architecture Components哦。
        https://github.com/xiaobailong24/MVVMArms
        xiaobailong24: @Bear_android 相互学习嘛
        Bear_android:这波广告打的
      • On_the_way_tl:66666 简洁代码
      • SongNick:牛逼🐮

      本文标题:Easy Clean architecture on Andro

      本文链接:https://www.haomeiwen.com/subject/rxrmhxtx.html