Clean Architecture - 清晰简洁的Android 应用架构

在我这几年的学习和成长中,深入的意识到搭建一个Android应用架构是件很是痛苦的事,它不只要知足不断增加的业务需求,还要保证架构自身的整洁,这让事情变得很是具备挑战,但咱们必须这样作,由于健壮的Android架构是一款优秀APP的基础。本文的代码示例能够从github中得到,仓库地址是*android-easy-cleanarchitecturehtml

Why we need an architecture?

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

固然不是!!!java

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

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

因此说,对于开发者们来说,一个好的架构指导规范,相当重要。github

从事Android工做以来,我始终认为咱们能将APP作的更好,我也遇到过不少好的坏的软件设计,本身也作过不少不一样的尝试,我不断地吸收教训并作出改变,直到我遇到了Clean Architecture,我肯定这就是我想要的,我决定使用它。本文的目标是分享我使用clean Architecture构建项目时所收获的经验,但愿可以为你的项目改进带来灵感。数据库

Avoid God Activity

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

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

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

随着项目的发展,它已经庞大到没法再添加代码了,因而为它写了不少帮助类,你想重构它:后端

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就是你定义业务规则的地方,把业务(请求)条件业务规则定义组合在一块儿不只容易理解也更方便管理。不过我会在下篇文章中介绍另外一种动态参数方式,也是我一直在使用的。

总结:

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

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

另外值得一提的是,若是你想作的更好,能够为你的项目加入模板化,组件化等策略,由于并无说一个项目只能使用一种框架结构。: )

最后,但愿这篇文章可以对你有所帮助,若是你有其余更好的架构思路,欢迎分享或与我交流。

相关文章
相关标签/搜索