BLOG · #Engineering

跨平台的边界:共享那些不允许分叉的部分

“选哪个跨平台框架”不是第一个该问的问题。真正该问的是:哪些层在两个平台上必须完全一致(因为在那里分叉就是缺陷),哪些层本就应该不同(因为在那里分叉才是正确的)。

English version: The cross-platform boundary: share what must not diverge。本文是交易类 App 架构系列第四篇。

跨平台的讨论通常被当成框架对比来进行,这也是它从来吵不出结果的原因。真正决定大局的是前一个问题:你的应用里,哪些部分在 iOS 和 Android 上必须一模一样,哪些部分不应该一样?

回答了它,框架选择就收敛到两三个合理选项。跳过它,再多的基准测试也没用,因为你是在需求还没定义的时候争论工具。

判据:分叉是缺陷,还是特性

拿你应用里的任意一层,问一句:如果两个平台的行为不同,会发生什么?

对于订单校验、费用计算、期权定价规则、状态机、协议编码:分叉是缺陷。同一条金融规则的两份独立实现一定会漂移,不是“可能”。它们先在边界情况上漂移,而在这个领域里,边界情况同时也是昂贵的情况。几个月内没人发现,然后缺陷以“某位用户的数字对不上”的形式送到你面前。

对于导航转场、手势手感、日期选择器、权限流程、无障碍、分享面板:分叉是正确的。用户有平台预期,抹平它们的代码会做出一个在两端都隐隐别扭的应用。花在统一这一层上的力气,买到的是负价值。

规则就这一条:共享那些“不同即缺陷”的层,把那些“不同才是重点”的层留给原生。

请注意这是一个正确性论证,不是效率论证。DRY 通常被解释成“只写一遍”,于是很容易被“有时候复制更便宜”反驳回去。这个反驳对 UI 成立,对领域规则则完全不相干,因为在那里,复制的代价不是打字,而是关于钱的两份互相分叉的真相。

共享真正的代价

共享不是免费的,而且代价是结构性的,不那么显眼:

多出一条边界。 共享代码和平台代码必须对话。这个接口需要双方都理解的类型、序列化、以及错误映射。它从此成为一个需要设计和版本管理的东西。

多出一个运行时。 第二个运行时意味着内存、启动开销,以及一个再也看不到全貌的调试器。当问题恰好发生在边界上时,你调试的是两个系统外加它们之间的接缝。

团队被切开。 必须有人精通共享层和它的工具链。在小规模下这个人只有一个,那不是方案,那是巴士系数。

所以问题不是“共享有没有好处”,而是“这一层的分叉风险,是否超过边界成本”。对领域逻辑,通常是;对一个设置页面,通常否。

三种站位

完全原生,写两遍。 平台质感最高,边界成本为零,每条规则写两次。当共享逻辑很薄、或者平台特性本身就是产品主体时,这是合理的。

共享逻辑,保留原生 UI。 领域、模型、校验放在一份代码里,各平台各自渲染。它精确地命中了“分叉即缺陷”的那一层,而放过了不是的那一层。代价就是那条边界和它的工具链。

连 UI 一起共享。 一份代码画两个平台。这是单个团队做出功能完整应用最快的路径,也是大多数小团队应该从这里起步的原因。代价会晚一点到来,而且来的方向可以预测:平台能力。每一个相机、生物识别、后台、通知特性,最终都需要一个插件,而插件就是原生代码加上额外的步骤。从一开始就为它留预算,而不是等它来吓你一跳。

混合模式,以及它真实的成本

大型应用常常最终是混合的:一个原生外壳,其中若干页面嵌入跨平台 UI 框架。这通常不是犹豫不决,而是一次迁移的诚实形态;当你无法为了重写而停止交付时,它就是正确的模式(下一篇会展开)。

在选择它之前,有几项真实成本值得点名:

  • 运行时内存。 每个嵌入的引擎都有内存占用,多个同时存活会成倍放大;而缓解办法(缓存并复用引擎)本身又是一个你从此要负责的生命周期问题。
  • 两个导航栈。 原生栈和框架的路由必须被统一,否则返回键行为会成为一个长期的缺陷来源。
  • 两侧都有状态。 会话、主题、语言从此存在两份,而且必须保持一致。每一份被复制的状态,都是一次未来的失步。
  • 跨运行时调试。 调用栈会在边界处中断。崩溃和性能工具必须被配置成能看见两侧,否则你有一半的线上问题只能靠猜。

把边界做粗

无论你共享什么,共享核心与平台之间的接口都应该是粗粒度的。

跨越边界的成本(序列化、封送、运行时切换)是按次调用付费的,不是按字节。一个碎嘴的接口,几十次小的属性读取,可能比一次返回完整对象的调用慢得多,尽管它搬运的数据更少。这和第一篇里“在原生边界上批量提交更新”是同一条教训,而且可以推广:把边界设计成少数几个有意义的操作,而不是远程字段访问。

这样做还有设计上的好处。粗接口表达的是意图(“下这一单”“给我这个页面的数据”),它在底层实现变化时保持稳定;细接口暴露的是结构,而结构一直在变。

诚实地做选择

两个变量决定结论:你的产品里有多少是不允许分叉的领域逻辑,以及你有多少人。

  • 一个小团队,产品主要是接口之上的界面:全部共享。一份代码,平台要求时再写原生模块。
  • 小团队,但有实质领域逻辑:仍然全部共享,但把那部分逻辑放进独立的一层,不依赖任何 UI,这样以后可以抽出来而不必重写。
  • 多个平台团队,且领域逻辑可观:共享逻辑,UI 保持原生。在这里,边界成本被明确地赚了回来。
  • 只有一个平台:这个问题不存在。不要为一种你没有证据会用到的可选性付费。

代价最大的错误,是按一个你还没有的团队规模去选共享策略。为一个你也许会建立的组织提前设计架构,正是小团队最终以小团队的产能,去维护一个大团队的开销的原因。

参考


上一篇:本地优先 · 下一篇:模块化与迁移