最初的美好
没有历史包袱,就没有压力,就是美好的。
假设项目启动了这样一个业务------造车:生产一辆小汽车(Car
),分别在不同的零件车间(车架(Sheel
)、发动机(Engine
)、车轮(Wheel
))安装相应的零件,所有零件安装完成后回到提车车间就可以提车。
这样的需求开发起来很简单:
- 数据实体
kotlin
data class Car(
var shell: Shell? = null,
var engine: Engine? = null,
var wheel: Wheel? = null,
) : Serializable {
override fun toString(): String {
return "Car: Shell(${shell}), Engine(${engine}), Wheel(${wheel})"
}
}
data class Shell(
...
) : Serializable
data class Engine(
...
) : Serializable
data class Wheel(
...
) : Serializable
- 零件车间(以车架为例)
kotlin
class ShellFactoryActivity : AppCompatActivity() {
private lateinit var btn: Button
private lateinit var back: Button
private lateinit var status: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shell_factory)
val car = intent.getSerializableExtra("car") as Car
status = findViewById(R.id.status)
btn = findViewById(R.id.btn)
btn.setOnClickListener {
car.shell = Shell(
id = 1,
name = "比亚迪车架",
type = 1
)
status.text = car.toString()
}
back = findViewById(R.id.back)
back.setOnClickListener {
setResult(RESULT_OK, intent.apply {
putExtra("car", car)
})
finish()
}
}
}
class EngineFactoryActivity : AppCompatActivity() {
// 和安装车架流程一样
}
class WheelFactoryActivity : AppCompatActivity() {
// 和安装车架流程一样
}
- 提车车间
kotlin
class MainActivity : AppCompatActivity() {
private var car: Car? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
car = Car()
refreshStatus()
findViewById<Button>(R.id.shell).setOnClickListener {
val it = Intent(this, ShellFactoryActivity::class.java)
it.putExtra("car", car)
startActivityForResult(it, REQUEST_SHELL)
}
findViewById<Button>(R.id.engine).setOnClickListener {
val it = Intent(this, EngineFactoryActivity::class.java)
it.putExtra("car", car)
startActivityForResult(it, REQUEST_ENGINE)
}
findViewById<Button>(R.id.wheel).setOnClickListener {
val it = Intent(this, WheelFactoryActivity::class.java)
it.putExtra("car", car)
startActivityForResult(it, REQUEST_WHEEL)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != RESULT_OK) return
when (requestCode) {
REQUEST_SHELL -> {
Log.i(TAG, "安装车架完成")
car = data?.getSerializableExtra("car") as Car
}
REQUEST_ENGINE -> {
Log.i(TAG, "安装发动机完成")
car = data?.getSerializableExtra("car") as Car
}
REQUEST_WHEEL -> {
Log.i(TAG, "安装车轮完成")
car = data?.getSerializableExtra("car") as Car
}
}
refreshStatus()
}
private fun refreshStatus() {
findViewById<TextView>(R.id.status).text = car?.toString()
findViewById<Button>(R.id.save).run {
isEnabled = car?.shell != null && car?.engine != null && car?.wheel != null
setOnClickListener {
Toast.makeText(this@MainActivity, "提车咯!", Toast.LENGTH_SHORT).show()
}
}
}
companion object {
private const val TAG = "MainActivity"
const val REQUEST_SHELL = 1
const val REQUEST_ENGINE = 2
const val REQUEST_WHEEL = 3
}
}
即使是初学者也能看出来,业务实现起来很简单,通过Activity
的startActivityForResult
就能跳转到相应的零件车间,安装好零件回到提车车间就完事了。
开始迭代
往往业务的第一个版本就是这么简单,感觉也没什么好重构的。
但是业务难免会进行迭代。比如业务迭代到1.1版本:客户想要给汽车装上行车电脑,而安装行车电脑不需要跳转到另一个车间,而是在提车车间操作,但是需要很长的时间。
看起来也简单,新增一个Computer
实体类和ComputerFactoryHelper
:
kotlin
object ComputerFactoryHelper {
fun provideComputer(block: Computer.() -> Unit) {
Thread.sleep(5_000)
block(Computer())
}
}
data class Computer(
val id: Int = 1,
val name: String = "行车电脑",
val cpu: String = "麒麟90000"
) : Serializable {
override fun toString(): String {
return "$name-$cpu"
}
}
再在提车车间新增按钮和逻辑代码:
kotlin
findViewById<Button>(R.id.computer).setOnClickListener {
object : Thread() {
override fun run() {
ComputerFactoryHelper.provideComputer {
car?.computer = this
runOnUiThread { refreshStatus() }
}
}
}.start()
}
目前看起来也没啥难的,那是因为我们模拟的业务场景足够简单,但是相信很多实际项目的屎山代码,就是通过这样的业务迭代,一点一点地堆积而成的。
从迭代到崩溃
咱们来采访一下最近被一个小小的业务迭代需求搞崩溃的Android开发------小王。
记者:小王你好,听说最近你Emo了,甚至多次萌生了就地辞职的念头?
小王:最近AI不是很火吗,产品给我提了一个需求,在上传音乐时可以选择在后端生成一个AI视频,然后一起上传。
记者:哦?这不是一个小需求吗?
小王:但是我打开目前上传业务的代码就傻了啊!就说Activity吧,有:BasePublishActivity,BasePublishFinallyActivity,SinglePublishMusicActivity,MultiPublishMusicActivity,PublishFinallyActivity,PublishCutMusicFinallyActivity, Publish(好多好多)FinallyActivity... 当然,这只是冰山一角。再说上传流程。如果只上传一首音乐,需要先调一个接口
/sts
拿到一个Oss Token,再调用第三方的Oss库上传文件,拿到一个url,然后再把这个url和其他的信息(标题、标签等)组成一个HashMap,再调用一个接口/save
提交到后端,相当于调3个接口... 如果要批量上传N个音乐,就要调3 * N个接口,如果还要给每个音乐配M个图片,就要调3 * N+3 * N * M个接口... 如果上传一个音乐配一个本地视频,就要调3 * 2 * N个接口,并且,上传视频流程还不一样的是,需要在调用/save
接口之后再调用第三方Oss上传视频文件...再说数据类。上面提到上传过程中需要添加图片、视频、活动类型啥的,代码里封装了一个EditInfo类足足有30个属性!,由于是Java代码并且实现了Parcelable
接口,光一个Data类就有400多行!你以为这就完了?EditInfo需要在上传时转成PublishInfo类,PublishInfo还可以转成PublishDraft,PublishDraft可以保存到数据库中,从数据库中可以读取PublishDraft然后转成EditInfo再重新编辑...记者:(感觉小王精神状态有点问题,于是掐掉了直播画面)
相信小王的这种情况,很多业务开发同学都经历过吧。回头再看一下前面的造车业务,其实和小王的上传业务一样,就是一开始很简单,迭代个7、8个版本就开始陷入一种困境:即使迭代需求再小,开发起来都很困难。
优雅地迭代业务代码?
假如咱们想要优雅地迭代业务代码,应该怎么做呢?
小王遇到的这座屎山,咱们现在就不要去碰了,先就从前面提到的造车业务开始吧。
很多同学会想到重构 ,俺也一样。接下来,我就要讨论一下如何优雅 而安全地重构既有业务代码。
先抛出一个观点:对于程序员来说,想要保持"优雅",最重要的品质就是抽象。
❓ 这时可能有同学就要反驳我了:过早的抽象没有必要。
❗ 别急,我现在要说的抽象,并不是代码层面的抽象 ,而是对业务的抽象 ,乃至对技术思维的抽象。
什么是代码层面的抽象 ?比如刚刚的Shell/Engine/WheelFactoryActivity
,其实是可以抽象为BaseFactoryActivity
,然后通过实现其中的零件类型就行了。但我不会建议你这么做,为啥?看看小王刚才的疯言疯语就明白了。各个XxxFactoryActivity
看着差不多,但在实际项目中很可能会开枝散叶,各自迭代出不同的业务细节。到那时,项目里就是各种BaseXxxActivity
,XxxV1Activity
,XxxV2Activity
...
那什么又是业务的抽象?直接上代码:
kotlin
interface CarFactory {
val factory: suspend Car.() -> Car
}
造车业务,无论在哪个环节,都是在Car
上装配零件(或者任何出其不意的操作),然后产生一个新的Car
;另外,这个环节无论是跳转到零件车间安装零件,还是在提车车间里安装行车电脑,都是耗时操作,所以需要加suspend
关键词。
❓ 这时可能有同学说:害!你这和BaseFactoryActivity
有啥区别,不就是把抽象类换成接口了吗?
❗ 别急,我并没有要让XxxFactoryActivity
去继承CarFactory
啊,想想小王吧,这个XxxFactoryActivity
就相当于他的同事在两年前写的代码,小王肯定打死都不会想去修改这里面的代码的。
Computer
是新业务,我们只改它。首先我们根据这个接口把ComputerFactoryHelper
改一下:
kotlin
object ComputerFactoryHelper : CarFactory {
private suspend fun provideComputer(block: Computer.() -> Unit) {
delay(5_000)
block(Computer())
}
override val factory: suspend Car.() -> Car = {
provideComputer {
computer = this
}
this
}
}
那么,在提车车间就可以这样改:
kotlin
private var computerFactory: CarFactory = ComputerFactoryHelper
findViewById<Button>(R.id.computer).setOnClickListener {
lifecycleScope.launchWhenResumed {
computerFactory.factory.invoke(car)
refreshStatus()
}
}
❓ 那么XxxFactoryActivity
相关的流程又应该怎么重构呢?
Emo时间
我先反问一下大家,为啥咱们Android程序员总是盯着Activity
不放呢?
我再反问一下大家,为啥咱们Android程序员总是盯着Activity
不放呢?
我再再反问一下大家,为啥咱们Android程序员总是盯着Activity
不放呢?
甚至,很多人即使学了Compose
和Flutter
,仍然对Activity
心心念念。
当你在一千个日日夜夜里,重复地写着
XxxActivity
,写onCreate/onResume/onDestroy
,写startActivityForResult/onActivityResult
时,当你每次想要换工作,打开面经背诵Activity
的启动模式,生命周期,AMS
原理时,可曾对Activity
有过厌倦,可曾思考过编程的意义?你也曾努力查阅
Activity
的源码,学习MVP
/MVVM
/MVI
架构,试图让你的Activity
保持清洁,但无论你怎么努力,却始终活在Activity
的阴影之下。你有没有想过,咱们正在被
Activity
PUA?
说实话,作为一名INFP,本人不是很适合做程序员。相比技术栈的丰富和技术原理的深入,我更看重的是写代码的感受。如果写代码都能被PUA,那我还怎么愉快的写代码?
当我Emo了很久之后,我意识到了,我一直在被代码PUA,不光是同事的代码,也有自己的代码,甚至有Android框架,以及外国大佬不断推出的各种新技术新框架。
对对对!你们都没有问题,是我太菜了555555555
优雅转身
Emo过后,还是得回到残酷的职场啊!但是我们要优雅地转身回来!
❓ 刚才不是说要处理XxxFactoryActivity
相关业务吗?
❗ 这时我就要提到另外一种抽象:技术思维的抽象!
Activity?F*ck off!
Activity
的跳转返回啥的,也无非就是一次耗时操作嘛,咱们也应该将它抽象为CarFactory
,就是这个东东:
kotlin
interface CarFactory {
val factory: suspend Car.() -> Car
}
基于这个信念,我从记忆中想到这么一个东东:ActivityResultLauncher
。
说实话,我以前都没用过这玩意儿,但是我这时好像抓到了救命稻草。
随便搜了个教程并谢谢他,参考这篇博客,我们可以把startActivityForResult
和onActivityResult
这套流程,封装成一次异步调用。
kotlin
open class BaseActivity : AppCompatActivity() {
private lateinit var startActivityForResultLauncher: StartActivityForResultLauncher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivityForResultLauncher = StartActivityForResultLauncher(this)
}
fun startActivityForResult(
intent: Intent,
callback: (resultCode: Int, data: Intent?) -> Unit
) {
startActivityForResultLauncher.launch(intent) {
callback.invoke(it.resultCode, it.data)
}
}
}
让MainActivity
继承BaseActivity
,就可以绕过Activity
了,后面的事情就简单了。只要咱们了解过协程,就能轻易想到异步转同步这一普通操作:suspendCoroutine
。
于是,我们就可以在不修改XxxFactoryActivity
的情况下,写出基于CarFactory
的代码了。还是以车架车间为例:
kotlin
class ShellFactoryHelper(private val activity: BaseActivity) : CarFactory {
override val factory: suspend Car.() -> Car = {
suspendCoroutine { continuation ->
val it = Intent(activity, ShellFactoryActivity::class.java)
it.putExtra("car", this)
activity.startActivityForResult(it) { resultCode, data ->
(data?.getSerializableExtra("car") as? Car)?.let {
Log.i(TAG, "安装车壳完成")
shell = it.shell
continuation.resumeWith(Result.success(this))
}
}
}
}
}
然后在提车车间,和Computer
业务同样的使用方式:
kotlin
private var shellFactory: CarFactory = ShellFactoryHelper(this)
findViewById<Button>(R.id.shell).setOnClickListener {
lifecycleScope.launchWhenResumed {
shellFactory.factory.invoke(car)
refreshStatus()
}
}
最终,在我们的提车车间,依赖的就是一些CarFactory
,所有的业务操作都是抽象的。到达这个阶段,相信大家都有了自己的一些想法了(比如维护一个carFactoryList
,用Hilt
管理CarFactory
依赖,泛型封装等),想要继续怎么重构/维护,就全看自己的实际情况了。
kotlin
class MainActivity : BaseActivity() {
private var car: Car = Car()
private var computerFactory: CarFactory = ComputerFactoryHelper
private var engineFactory: CarFactory = EngineFactoryHelper(this)
private var shellFactory: CarFactory = ShellFactoryHelper(this)
private var wheelFactory: CarFactory = WheelFactoryHelper(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
refreshStatus()
findViewById<Button>(R.id.shell).setOnClickListener {
lifecycleScope.launchWhenResumed {
shellFactory.factory.invoke(car)
refreshStatus()
}
}
findViewById<Button>(R.id.engine).setOnClickListener {
lifecycleScope.launchWhenResumed {
engineFactory.factory.invoke(car)
refreshStatus()
}
}
findViewById<Button>(R.id.wheel).setOnClickListener {
lifecycleScope.launchWhenResumed {
wheelFactory.factory.invoke(car)
refreshStatus()
}
}
findViewById<Button>(R.id.computer).setOnClickListener {
lifecycleScope.launchWhenResumed {
Toast.makeText(this@MainActivity, "稍等一会儿", Toast.LENGTH_LONG).show()
computerFactory.factory.invoke(car)
Toast.makeText(this@MainActivity, "装好了!", Toast.LENGTH_LONG).show()
refreshStatus()
}
}
}
private fun refreshStatus() {
findViewById<TextView>(R.id.status).text = car.toString()
findViewById<Button>(R.id.save).run {
isEnabled = car.shell != null && car.engine != null && car.wheel != null && car.computer != null
setOnClickListener {
Toast.makeText(this@MainActivity, "提车咯!", Toast.LENGTH_SHORT).show()
}
}
}
}
总结
- 抽象是程序员保持优雅的最重要能力。
- 抽象不应局限在代码层面,而是要上升到业务,乃至技术思维上。
- 有意识地对代码PUA说:No!
- 学习新技术时,不应只学会调用,也不应迷失在技术原理上,更重要的是花哨的技术和朴实的编程思想之间的化学反应。