拒绝代码PUA,优雅地迭代业务代码

最初的美好

没有历史包袱,就没有压力,就是美好的。

假设项目启动了这样一个业务------造车:生产一辆小汽车(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
    }
}

即使是初学者也能看出来,业务实现起来很简单,通过ActivitystartActivityForResult就能跳转到相应的零件车间,安装好零件回到提车车间就完事了。

开始迭代

往往业务的第一个版本就是这么简单,感觉也没什么好重构的。

但是业务难免会进行迭代。比如业务迭代到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看着差不多,但在实际项目中很可能会开枝散叶,各自迭代出不同的业务细节。到那时,项目里就是各种BaseXxxActivityXxxV1ActivityXxxV2Activity...

那什么又是业务的抽象?直接上代码:

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不放呢?

甚至,很多人即使学了ComposeFlutter,仍然对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

说实话,我以前都没用过这玩意儿,但是我这时好像抓到了救命稻草。

随便搜了个教程并谢谢他,参考这篇博客,我们可以把startActivityForResultonActivityResult这套流程,封装成一次异步调用。

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!
  • 学习新技术时,不应只学会调用,也不应迷失在技术原理上,更重要的是花哨的技术和朴实的编程思想之间的化学反应。
相关推荐
阿巴斯甜9 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker10 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952711 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android