聊聊架构:Easy Clean architecture on Android
作者简介
本文由 小鄧子 原创并授权发布,未经原作者允许请勿转载。
本文主要介绍了,如何构建一个整洁干净,并且健壮的 Android 架构。写的非常好,希望大家会喜欢。
小鄧子 的博客地址:
http://www.jianshu.com/p/3edcf85539a6
在我这几年的学习和成长中,深刻的意识到搭建一个 Android 应用架构是件非常痛苦的事,它不仅要满足不断增长的业务需求,还要保证架构自身的整洁,这让事情变得非常具有挑战,但我们必须这样做,因为健壮的 Android 架构是一款优秀 APP 的基础。
本文的代码示例可以从 Github 中获得,仓库地址是:
https://github.com/SmartDengg/android-easy-cleanarchitecture
Why we need an architecture?
Android 入门要求始终不高,因为 Android Framework 会帮我们做很多事,甚至不需要通过深入的学习就能写出一个简单的 APP ,比如说在 Activity
或 Fragment
中摆放几个 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 中定义了所有子类可能用到的变量,它看起来就像一个万能 “上帝” 。
某一天你良心发现,或是出于其他原因,已经无法再添加代码了,于是为它写了很多帮助类,看起来就像这样:
不经意间,你已经埋下了黑色的炸弹。
看上去,业务逻辑被转移到了帮助类中,Activity
中的代码减少了,不再那么臃肿,帮助类缓解了“万能类”的压力,但随着项目的成长,业务的扩大,同时这些帮助类也变多,那个时候又要按照业务继续拆分它们, APIHelperThis
、 APIHelperThat
等等。原来的问题又出现了,测试成本还在,维护成本好像又增加了,那些混乱并且难以复用的程序又回来了,我们的努力好像都白费了。
然而你写这个万能类的初衷是什么,想快捷、方便的使用一些功能函数吗,尤其希望在子类中能够很快的拿到。
无论什么理由这种创造 “上帝类” 的做法都应该尽量避免,我们不应该把重点放在编写那些大而全的类,而是投入精力去编写那些易于维护和测试的低耦合类,如果可以的话,最好不要让业务逻辑进入纯净的 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 ,如下图所示:
在 Domain 中定义业务逻辑规则,在 UI 中实现界面交互, Model 则是业务逻辑的具体实现方式( Android framework )。箭头方向代表依赖关系,内层抽象,外层具体,外层知道内层,内层不了解外层。
具体到 Android 中的框架结构如下图所示:
你可能有些困惑,为什么 Domain 指向 Data ?既然 Domain 包含业务逻辑,它就应该是应用程序的中心,它不应该依赖 Model ,按照前面提到的原则, Domain 是抽象的, Model 是具体的,应该是 Model 依赖 Domain ,而不是 Domain 依赖 Model 。
其实这很好理解,也是我始终强调的,这里所说的“依赖”并不是指配置在 gradle
中的 dependency ,你应该将它理解为 “知道”,“了解”,“意识” ,图中的箭头代表了调用关系,而非模块间的依赖关系。我们应该能够理解:抽象是理论,依赖是实践,抽象是应用的逻辑布局,依赖是应用的组合策略。对于框架结构的理解,我们应该跳出代码层面,不要局限在惯性思维中,否则很快就会陷入逻辑混乱的怪圈。
与调用关系对应的就是数据流的走向:
在 App 中接受用户的行为,根据 domain 中定义的业务规则,访问 model 中的真实数据,然后依次返回,最终更新界面,这就是一个完整的数据流走向。
为了更方便理解,我对项目进行了简单的拆解,并在图中加上了类的用例描述,它看起来就像这样:
对上图所表示内容做一下总结:
首先,项目被分为三层:
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 图中演示的那样,组合你的 MVPView
和 MVPPresenter
,让它们更容易被管理和维护。
首先定义 BaseView
和 BasePresenter
,在 BaseView
中我是用了 RxJava 的 Observable
作为结果类型。
假设你有一个根据城市ID获取该城市已上映电影的需求,那么你可以这样组合你的 MovieView
和 MoviePresenter
接口:
泛型的加入,有效保证了数据的类型安全。
接下来实现你自己的 XXXPresenter
和 XXXView
接口的实现类,就像这样:
关于示例中的 UseCase.Request
来自于Clean Architecture: Dynamic Parameters in Use Cases:在 XXXUseCase
中创建静态内部类 Request
作为动态请求参数的容器。其实这很好理解,而且也完全正确,因为UseCase
就是你定义业务规则的地方,把 业务(请求)条件与业务规则 定义组合在一起不仅容易理解也更方便管理。不过我会在下篇文章中介绍另一种动态参数方式,也是我一直在使用的。
总结
我相信你和我一样,在搭建框架的过程中遭遇着各式各样的挑战,从错误中吸取教训,不断优化代码,调整依赖关系,甚至重新组织模块结构,这些你做出的改变都是想让架构变得更健壮,我们一直希望应用程序能够变得易开发易维护,这才是真正意义上的团队受益。
不得不说,搭建应用架构的方式多种多样,而且我认为,没有万能的,一劳永逸的架构,它应该是不断迭代更新,适应业务的。所以说,你可以按照文中提供的思路,尝试着结合业务来构建你的应用程序。
另外值得一提的是,如果你想做的更好,可以为你的项目加入模板化,组件化等策略,因为并没有说一个项目只能使用一种框架结构。
本文参考:
Clean Architecture。https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
Clean Architecture:Dynamic Parameters in Use Cases。 https://fernandocejas.com/2016/12/24/clean-architecture-dynamic-parameters-in-use-cases/
推荐阅读: