拒绝代码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!
  • 学习新技术时,不应只学会调用,也不应迷失在技术原理上,更重要的是花哨的技术和朴实的编程思想之间的化学反应。
相关推荐
HerayChen1 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11231 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件1 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252032 小时前
group_concat配置影响程序出bug
android·bug
周全全2 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -3 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨3 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸3 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android