Easy Clean architecture on Android

96
作者 小鄧子
2017.08.10 11:47* 字数 4360

在我这几年的学习和成长中,深刻的意识到搭建一个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
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
Clean architecture modules

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

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

clean architecture structure
clean architecture structure

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

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

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

clean architecture data stream
clean architecture data stream

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

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

clean architecture UML
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 !

Android原创集