本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
杂乱度
Android 架构演进系列是围绕着杂乱度向前推进的。
软件的首要技能任务是“管理杂乱度” —— 《代码大全》
由于低杂乱度才干降低了解本钱和沟通难度,提升应对变更的灵活性,减少重复劳动,终究进步代码质量。
架构的目的在于“将杂乱度分层”
杂乱度为什么要被分层?
若不分层,杂乱度会在同一层次打开,这样就太 … 杂乱了。
举一个杂乱度不分层的比方:
小李:“你会做什么菜?”
小明:“我会做用土鸡生的土鸡蛋配上切片的西红柿,放点油盐,开火翻炒的西红柿炒蛋。”
听了小明的答复,你还会和他做朋友吗?
小明把不同层次的杂乱度以不恰当的办法搓弄在一同,让人感觉是一种由“没有必要的具体”导致的“难以了解的杂乱”。
小李其实并不关怀土鸡蛋的来历、西红柿的切法、添加的佐料、以及烹饪办法。
这样的答复除了难以了解之外,局限性也很大。由于它太具体了!只要把土鸡蛋换成洋鸡蛋、或是西红柿片换成块、或是加点糖、或是换成电磁炉,其间任一要素产生改动,小明就不会做西红柿炒蛋了。
再举个正面的比方,TCP/IP 协议分层模型自下到上定义了五层:
- 物理层
- 数据链路成
- 网络层
- 传输层
- 应用层
其间每一层的功用都独立且清晰,这样规划的好处是缩小影响面,即单层的变动不会影响其他层。
这样规划的另一个好处是当专心于一层协议时,其他层的技能细节能够不予重视,同一时间只需求重视有限的杂乱度,比方传输层不需求知道自己传输的是 HTTP 仍是 FTP,传输层只需求专心于端到端的传输办法,是建立连接,仍是无连接。
有限杂乱度的另一面是“基层的可重用性”。当应用层的协议从 HTTP 换成 FTP 时,其基层的内容不需求做任何更改。
引子
该系列的前三篇结合“查找”这个事务场景,讲述了不运用架构写事务代码会产生的痛点:
- 低内聚高耦合的制作:控件的制作逻辑散落在遍地,散落在各种 Activity 的子程序中(子程序间相互耦合),涣散在现在和将来的逻辑中。这样的规划添加了界面刷新的杂乱度,导致代码难以了解、容易改出 Bug、难排查问题、无法复用。
- 耦合的非粘性通讯:Activity 和 Fragment 经过获取对方引证并互调办法的办法完结通讯。这种通讯办法使得 Fragment 和 Activity 耦合,从而降低了界面的复用度。而且没有一种内建的机制来轻松的完成粘性通讯。
- 上帝类:所有细节都在界面被铺开。比方数据存取,网络访问这些和界面无关的细节都在 Activity 被铺开。导致 Activity 代码不单纯、高耦合、代码量大、杂乱度高、改动源不单一、改动影响规模大。
- 界面 & 事务:界面展现和事务逻辑耦合在一同。“界面该长什么样?”和“哪些事情会触发界面重绘?”这两个独立的改动源没有做到重视点别离。导致 Activity 代码不单纯、高耦合、代码量大、杂乱度高、改动源不单一、改动影响规模大、易改出 Bug、界面和事务无法独自被复用。
具体剖析过程能够点击下面的链接:
-
写事务不必架构会怎么样?(一)
-
写事务不必架构会怎么样?(二)
-
写事务不必架构会怎么样?(三)
这一篇试着引进 MVP 架构(Model-View-Presenter)进行重构,看能不能处理这些痛点。
在重构之前,先介绍下查找的事务场景,该功用示意图如下:
事务流程如下:在查找条中输入关键词并同步展现联想词,点联想词跳转查找成果页,若无匹配成果则展现推荐流,回来时查找前史以标签形式横向铺开。点击前史可直接建议查找跳转到成果页。
将查找事务场景的界面做了如下规划:
查找页用Activity
来承载,它被分红两个部分,头部是常驻在 Activity 的查找条。下面的“查找体”用Fragment
承载,它或许呈现三种状况 1.查找前史页 2.查找联想页 3.查找成果页。
Fragment 之间的切换选用 Jetpack 的Navigation
。关于 Navigation 具体的介绍能够点击关于 Navigation 更具体的介绍能够点击Navigation 组件运用入门 | Android 开发者 | Android Developers
高耦合+低内聚
MVP 能否成为高耦合低内聚的终结者?
先来看看高耦合低内聚的代码长什么样。以查找条为例,它的交互如下:
当输入框键入内容后,显示X按钮并高亮查找按钮。点击查找跳转到查找成果页,一同查找条拉长并躲藏查找按钮。点击X时清空输入框并从查找成果页回来,查找条还原。
引证上一篇无架构的完成代码:
class TemplateSearchActivity : AppCompatActivity() {
private fun initView() {
// 查找按钮初始状况
tvSearch.apply {
isEnabled = false
textColor = "#484951"
}
// 初始状况下,清空按钮不展现
ivClear.visibility = gone
// 初始状况下,弹出查找框
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
// 监听输入框,当有内容时更新查找和X按钮状况
etSearch.addTextChangedListener(object :TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(char: CharSequence?, start: Int, before: Int, count: Int) {
val input = char?.toString() ?: ""
if(input.isNotEmpty()) {
ivClear.visibility = visible
tvSearch.apply {
textColor = "#F2F4FF"
isEnabled = true
}
}else {
ivClear.visibility = gone
tvSearch.apply {
textColor = "#484951"
isEnabled = false
}
}
}
override fun afterTextChanged(s: Editable?) { }
})
// 监听键盘查找按钮
etSearch.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val input = etSearch.text.toString() ?: ""
if(input.isNotEmpty()) { searchAndHideKeyboard() }
true
} else false
}
// 监听查找条查找按钮
tvSearch.setOnClickListener { searchAndHideKeyboard() }
}
// 跳转到查找页 + 拉长查找条 + 躲藏查找按钮 + 躲藏键盘
private fun searchAndHideKeyboard() {
vInputBg.end_toEndOf = parent_id // 拉长查找框(与父亲右边对齐)
// 跳转到查找成果页
findNavController(NAV_HOST_ID.toLayoutId()).navigate(
R.id.action_history_to_result,
bundleOf("keywords" to etSearch?.text.toString())
)
tvSearch.visibility = gone
KeyboardUtils.hideSoftInput(etSearch)
}
}
这样写的坏处如下:
1. 事务 & 界面耦合
- “界面长什么样”和“哪些事情会触发界面重绘”是两个不同的重视点,它们能够独立改动,前者由 UI 规划建议变更,后者由产品建议变更。
- 耦合添加代码量以及杂乱度,高杂乱度添加了解难度且容易出错。比方当别人接手该模块看着 1000+ 的 Activity 莫衷一是时。再比方你修正了界面展现,而另一个同学修正了事务逻辑,合代码时,你俩或许产生抵触,抵触处理欠好就会产生 Bug。
- 高耦合还降低了复用性。界面和事务耦合在一同,使得它们都无法独自被复用。即界面无法复用于另一个事务,而事务也无法复用于另一个界面。
2. 低内聚的界面制作
- 同一个控件的制作逻辑散落在各个地方,涣散在不同的办法中,涣散在现在和将来的逻辑中(回调)。
- 低内聚相同也添加了杂乱度。就比方玩剧本杀,头绪散落在场地的各个旮旯,你得先搜出头绪,然后再将他们拼凑起来,才干形成完整的认知。再比方 y=f(x),唯一x决议唯一y,而低内聚的代码就比方y=f(a,b,c,d),任意一个改动源的改动的都会影响界面状况。当UI变更时极易产生“没改全”的 Bug,对于一个小的 UI 改动,不得不查找整段代码,找出所有对控件的引证,漏掉一个便是 Bug。
查找条的事务相对简单,initView()
看上去也没那么杂乱。假如延续“高事务耦合+低制作内聚”的写法,当界面越来越杂乱之后,1000+ 行的 Activity 不是梦。
用一张图来表达所有的杂乱度在 Activity 层铺开:
事务和界面别离
事务逻辑和界面制作是两个不同的重视点,它们本能够不在一个层次中被铺开。
MVP 架构引进了 P(Presenter)层用于承载事务逻辑,完成了杂乱度分层:
interface SearchPresenter {
// 初始化
fun init()
// 回来
fun backPress()
// 清空关键词
fun clearKeyword()
// 建议查找
fun search(keyword: String, from: SearchFrom)
// 输入关键词
fun inputKeyword(keyword: String)
}
Presenter 称为事务接口
,它将所有界面能够发出的动作都表达成接口中的办法。接口是编程言语中表达“笼统”的手段。这是个了不得的发明,由于它把“做什么”和“怎么做”隔离。
界面会持有一个 Presenter 的实例,把事务逻辑托付给它,这使得界面只需求重视“做什么”,而不需求重视“怎么做”。所以事务接口做到了界面制作和事务逻辑的解耦。
事务逻辑终究会辅导界面怎么制作,在 MVP 中经过View 层
界面来表达:
interface SearchView {
fun onInit(keyword: String)
fun onBackPress()
fun onClearKeyword()
fun onSearch()
fun onInputKeyword(keyword:String)
}
Presenter 的完成者会持有一个 View 层接口实例:
class SearchPresenterImpl(private val searchView: SearchView) :SearchPresenter{
override fun init() {
searchView.onInit("")
}
override fun backPress() {
searchView.onBackPress()
}
override fun clearKeyword() {
searchView.onClearKeyword()
}
override fun search(keyword: String, from: SearchFrom) {
searchView.onSearch()
}
override fun inputKeyword(keyword: String) {
searchView.onInputKeyword(keyword)
}
}
Presenter 调用 View 层接口辅导界面制作,界面经过完成 View 层接口完成制作:
class TemplateSearchActivity : AppCompatActivity(), SearchView {
private val searchPresenter: SearchPresenter = SearchPresenterImpl(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
searchPresenter.init()
}
override fun onBackPressed() {
super.onBackPressed()
searchPresenter.backPress()
}
// 完成 View 层接口进行界面制作
override fun onInit(keyword: String) {...}
override fun onBackPress() {...}
override fun onClearKeyword() {...}
override fun onSearch() {...}
override fun onInputKeyword(keyword:String) {...}
}
别离了个寂寞?
这样的完成太脱裤子放屁了。就比方三楼搭档想给五楼搭档相同东西,非得叫顺丰快递,然后顺丰又邮寄给了申通快递。
非也!当持有一个“笼统”而不是“具体完成”时,功德就会产生!
Activity 和笼统的 SearchPresenter 接口互动,就能产生多态,即动态地替换事务逻辑的完成。
比方产品希望做一个试验,把用户分红A/B两组,A组在进入查找页的一同把上一次用户查找的前史直接展现在输入框中,B组则是展现今天的查找热词。
相同的初始化动作,相同的在输入框中键入内容,不同的是获取数据的办法,A组从本地磁盘获取查找前史,而B组从网络获取查找热词。
初始化动作对应“做什么”,输入框中键入内容对应“展现什么”,获取数据的办法对应“怎么做”。假如这些逻辑没有分层而都写在一同,那只能经过在 Activity 中的 if-else 完成:
class TemplateSearchActivity : AppCompatActivity() {
val abtest by lazy { intent.getStringExtra("ab-test") }
fun initView() {
if(abTest == "A"){
// 输入框展现查找前史
} else {
// 输入框展现查找热词
}
}
}
若这种分类评论用上瘾,Activity 代码会以极快的速度膨胀,可读性骤降,最糟糕的是一改就容易出 Bug。由于界面制作没有内聚在一点,而是散落在各种逻辑分支中,不同分支之间的逻辑或许是互斥,或是协同。。。等等总之极其杂乱。
有了笼统的 SearchPresenter 就好办了,笼统意味着能够产生多态。
多态是编程言语支撑的一种特性,这种特性使得静态的代码运行时或许产生动态的行为,这样一来编程时不需求为类型所烦恼,能够编写一致的处理逻辑而不是依靠特定的类型。”
可见运用多态能够解耦,经过言语内建的机制完成 if-else 的效果:
class TemplateSearchActivity : AppCompatActivity(), SearchView {
private val abtest by lazy { intent.getStringExtra("ab-test") }
// 依据命中试验组构建 SearchPresenter 实例
private val searchPresenter:SearchPresenter by lazy {
when(type){
"A" -> SearchPresenterImplA(this)
"B" -> SearchPresenterImplB(this)
else -> SearchPresenterImplA(this) // 默许进A试验组
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
searchPresenter.init() // 不必做任何修正,也没有 if-else
}
override fun onInit(keyword: String){
etSearch.setText(keyword, TextView.BufferType.EDITABLE)// 不必做任何修正,也没有 if-else
}
}
然后只要完成两个不同的 SearchPresenter 即可:
class SearchPresenterImplA(private val searchView: SearchView) :SearchPresenter{
override fun init() {
val keyword = loadFromLocal()// 拉取耐久化的查找前史
searchView.onInit(keyword)
}
}
class SearchPresenterImplB(private val searchView: SearchView) :SearchPresenter{
override fun init() {
val keyword = loadFromRemote()// 从网络拉取查找热词
searchView.onInit(keyword)
}
}
若运用依靠注入结构,比方 Dagger2 或 Hilt,还能把依据AB测验验组分类评论构建 Presenter 实例的逻辑简化,真正做到事务代码中无分类评论。
假如 SearchPresenter 中只有 init() 的逻辑在 AB 测场景下不同,那上述计划中其他相同的逻辑需求完成两份?
不需求,用装饰者形式就能够复用剩下的行为:
class SearchPresenterImplA(
private val searchView: SearchView,
private val presenter: SearchPresenter // 自己持有自己
) :SearchPresenter{
override fun init() {
val keyword = loadFromLocal()// 拉取耐久化的查找前史
searchView.onInit(keyword)
}
override fun backPress() {
presenter.backPress()// 完成托付给presenter
}
override fun touchSearchBar(text: String, isUserInput: Boolean) {
presenter.touchSearchBar(text, isUserInput)// 完成托付给presenter
}
override fun clearKeyword() {
presenter.clearKeyword()// 完成托付给presenter
}
override fun search(keyword: String, from: SearchFrom) {
presenter.search(keyword, from)// 完成托付给presenter
}
override fun inputKeyword(keyword: String) {
presenter.search(keyword)// 完成托付给presenter
}
}
// 像这样构建 SearchPresenterImplA
class TemplateSearchActivity : AppCompatActivity(), SearchView {
val presenter = SearchPresenterImplA(this, SearchPresenterImplB(this))
}
SearchPresenterImplA 持有另一个 SearchPresenter,而且把剩下办法的完成托付给它。
关于装饰者形式更具体的介绍能够点击运用组合的规划形式 | 美颜相机中的装饰者形式。
这样一来,就把“界面长什么样”和“AB测验”解耦,它们分处于不同的层次,前者在 Activity 归于 View 层,后者归于 Presenter 层。解耦的一同也产生了内聚,关于界面制作的常识都内聚在 Activity,关于事务逻辑的常识都内聚在 Presenter。
假定界面和事务耦合在一同,后果不堪设想。由于事务的改动是飞快的,今天是 AB 测,明天或许是从不同入口进入查找页,上报不同的埋点。相似这种情况 Activity 的逻辑会被成堆的 if-else 玩坏。
阶段性总结:
界面和事务分层之后(杂乱度被分层),它们就能独立改动(高扩展性),独立复用(高复用性),再配合上“面向笼统编程”,使得事务的逻辑分支被巧妙的躲藏起来(杂乱度被躲藏)。
有限的内聚
这样的 View 层接口定义会产生一个问题:
class TemplateSearchActivity : AppCompatActivity() {
override fun onBackPress() {
vInputBg.end_toStartOf = ID_SEARCH // 查找框右侧对齐查找按钮
ivClear.visibility = visible
}
override fun onClearKeyword() {
vInputBg.end_toStartOf = ID_SEARCH // 查找框右侧对齐查找按钮
ivClear.visibility = gone
}
override fun onSearch() {
vInputBg.end_toStartOf = parent_id // 查找框右侧对齐父容器
}
override fun onInputKeyword(keyword: String) {
ivClear.visibility = if(keyword.isNotEmpty()) visible else gone
}
}
一个控件应该长成什么样的代码仍然散落在不同办法中,就像上一篇描绘的相同。
这样容易产生“改不全”或“功用衰退”的 Bug,比方查找页新增了一个事务逻辑,一个新的 View 层接口被完成,该接口的完成需求非常当心,由于它修正的控件也会在其他 View 层接口被修正,你得保证它们不会产生抵触。
之所以会这样,是由于“View 层接口面向事务进行笼统”,其实从接口的命名就能够看出。
更好的做法是“在 View 层接口屏蔽事务动作,只关怀做怎么样的制作”:
interface SearchView {
fun initView() // 初始化
fun showClearButton(show: Boolean)// 展现X
fun highlightSearchButton(show: Boolean) // 高亮查找按钮
fun gotoSearchPage(keyword: String, from: SearchFrom) // 跳转到查找成果页
fun stretchSearchBar(stretch: Boolean) // 拉伸查找框
fun showSearchButton(highlight: Boolean, show: Boolean) // 展现查找按钮
fun clearKeyword(clear:Boolean) // 清空关键词
fun gotoHistoryPage()// 回来前史页
}
这下 View 层接口描绘的都是展现怎么样的界面,Presenter 和 Activity 的代码得做相应的修正:
class SearchPresenterImpl(private val searchView: SearchView) :SearchPresenter{
override fun init() {
searchView.initView()
}
override fun backPress() {
searchView.stretchSearchBar(false)
searchView.showSearchButton(true)
searchView.clearKeyword(true)
}
override fun clearKeyword() {
searchView.highlightSearchButton(false)
searchView.showClearButton(false)
searchView.showSearchButton(true)
searchView.stretchSearchBar(false)
searchView.clearKeyword(true)
searchView.gotoHistoryPage()
}
override fun search(keyword: String, from: SearchFrom) {
searchView.gotoSearchPage(keyword, from)
searchView.stretchSearchBar(true)
searchView.showSearchButton(false)
}
override fun inputKeyword(keyword: String) {
if (keyword.isNotEmpty()) {
searchView.showClearButton(true)
searchView.highlightSearchButton(true)
} else {
searchView.showClearButton(false)
searchView.highlightSearchButton(false)
}
}
}
这样的 Presenter 看上去就没那么“脱裤子放屁”了,它不仅仅是一个界面动作的转发者,它包含了一点事务逻辑。
对应的 Activity 修正如下:
class TemplateSearchActivity : AppCompatActivity(), SearchView {
private val searchPresenter: SearchPresenter = SearchPresenterImpl(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
searchPresenter.init()
}
override fun onBackPressed() {
super.onBackPressed()
searchPresenter.backPress()
}
override fun initView() {
etSearch.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
if (etSearch.text.toString().isNotEmpty())
searchPresenter.onSearchBarTouch(etSearch.text.toString(), true)
}
false
}
tvSearch.onClick = {
searchPresenter.search(etSearch.text.toString(), SearchFrom.BUTTON)
}
etSearch.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(char: CharSequence?, start: Int, before: Int, count: Int) {
val input = char?.toString() ?: ""
searchPresenter.inputKeyword(input)
}
override fun afterTextChanged(s: Editable?) {
}
})
etSearch.requestFocus()
KeyboardUtils.showSoftInput(etSearch)
}
override fun showClearButton(show: Boolean) {
ivClear.visibility = if (show) visible else gone
}
override fun gotoSearchPage(keyword: String, from: SearchFrom) {
runCatching {
findNavController(NAV_HOST_ID.toLayoutId()).navigate(
R.id.action_to_result,
bundleOf("keywords" to keyword)
)
}
KeyboardUtils.hideSoftInput(etSearch)
StudioReport.reportSearchButtonClick(keyword, from.typeInt)
}
override fun stretchSearchBar(stretch: Boolean) {
vInputBg.apply {
if (stretch) end_toEndOf = parent_id
else end_toStartOf = ID_SEARCH
}
}
override fun showSearchButton(highlight: Boolean, show: Boolean) {
tvSearch.apply {
visibility = if(show) visible else gone
textColor = if(highlight) "#F2F4FF" else "#484951"
isEnable = highlight
}
}
override fun clearKeyword(clear: Boolean) {
etSearch.apply {
text = null
requestFocus()
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
}
override fun gotoHistoryPage(clear: Boolean) {
findNavController(NAV_HOST_ID.toLayoutId()).popBackStack()
}
}
同一控件的制作逻辑总算内聚到一个办法中了,但不同控件的制作逻辑仍是散落在不同的办法。
不同控件的显示是有协同或互斥关系的,比方查找条拉长时,查找按钮得躲藏。但拉长查找条和查找按钮的制作分处于不同的 View 层接口,这儿就有一个潜规则:“在调拉长查找条办法的一同,必须一同调用躲藏查找按钮办法”。当 Presenter 中充满着这种潜规则时,就会产生界面状况不一致的问题。(最常见的比方,列表加载成功后,loading 还在转圈圈)
之所以会这样是由于 MVP 只是在“低内聚的界面制作”基础上往前进了一小步,做到了单个控件制作逻辑的内聚。而 MVI 又进了一步,做到了整个界面制作逻辑的内聚。(完成细节在后面的华章打开)
经过 MVP 的重构,现在架构如下图所示:
为啥看上去,比无架构计划还要杂乱一点?
没错,MVP 架构引进了新的杂乱度。首先是新增一个 Presenter 类,接着还引进了两个接口:事务接口+ View 层接口。这是完成解耦的必要代价。
引进 Presenter 层也有收益,与“杂乱度在 View 层被铺开”比较,现在的 View 层要精简得多,也单纯的多。但杂乱度被不是随便消失了,而是被分层,被搬运。从图中能够看呈现在的杂乱度聚集在 Presenter 中事务接口和 View 层接口的交互。MVI 用了一种新的思想办法来化解这个杂乱度。(后续华章会打开剖析)
总结
- MVP 引进了事务逻辑层 P(Presenter),使得界面制作和事务逻辑分隔,降低了它们的耦合,形成相互独立的界面层 V 和事务逻辑层 P。界面代码的杂乱度得以降低也变得更加单纯。
- MVP 经过接口完成界面层和事务逻辑层的双向通讯,界面层经过事务接口向事务逻辑层建议恳求。事务逻辑层经过 View 层接口辅导界面制作。接口是一种笼统手段,它把做什么和怎么做别离,为产生多态提供了便当。
- MVP 中 View 层接口的笼统应该面向“界面制作”而不是“面向事务”。这样做不仅能够让界面制作逻辑变得内聚,也让添加了代码的复用性。
推荐阅读
写事务不必架构会怎么样?(一)
写事务不必架构会怎么样?(二)
写事务不必架构会怎么样?(三)
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(一)
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(二)
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(三)
“无架构”和“MVP”都救不了事务代码,MVVM能力挽狂澜?(一)