Kotlin E02:任务状态能共享到哪里
本篇从正在使用的 :domain 模块抽出状态模型,在 JVM 和 Node.js 上各跑一次相同的断言。实验并不涉及 Android、Native、浏览器或多端服务发布;“两端通过”的范围就是这两个运行目标。
先把真正纯粹的规则移出来
原来的 TaskState.kt 有两个不同职责。前半段定义状态、信号和转移;末尾的 acceptSubmission(store, id, text) 依赖 TaskStore,而 store 的具体实现位于 JVM 域模块。移动前半段到独立 Gradle 工程 shared-model/src/commonMain/kotlin/example/tasks/TaskState.kt,后半段留在原来的 :domain:
1 | |
这里的枚举、密封分支、字符串和值对象不调用平台 API;非法跳转抛 IllegalStateException。公共文件也保留 taskStage 对状态的分类:Queued 是等待、Running 是活动,三个终态统一归入 TERMINAL。类型和 when 的穷尽检查可由公共代码表达,数据库事务何时提交则不能靠这些类型代替。
原 JVM :domain 通过 kotlin.sourceSets.named("main") { kotlin.srcDir("../shared-model/src/commonMain/kotlin") } 编译同一份源文件。这样 00–39 的调用者不需要改 import,但必须清楚这不是主工程已经消费了一个发布到 Maven 的多平台二进制包;两个 Gradle 构建各自编译该源路径。另一方面,多平台子项目把该路径作为真正的 commonMain,分别编译 JVM 与 JS 目标。这个选择最小化了既有服务的改动,也让“源码复用”和“依赖一份跨平台产物”两个概念不能混为一谈。
commonMain 不等于所有库都能引用
多平台 Gradle 构建仅声明 jvm() 与 js(IR) { nodejs() },commonTest 依赖 kotlin("test")。相同的测试先执行 Queued + Start,再执行 Running + Success("done"),断言状态与阶段;另断言排队时取消得到 Cancelled,排队时抢先 Success 必须抛错。旧代码若把非法成功改成默许,两端测试都会失败;只断言 happy path 则检测不到这一类错误。
平台差异放在测试源集,而不是往领域模型里塞 JVM 判断:commonTest 声明 expect fun targetName(): String,jvmTest 返回 JVM,jsTest 返回 JS/Node。expect/actual 要求各参与目标提供对应声明,但不会使任意 JVM 类、JDBC 驱动或 Netty 自动获得 JS 实现。这里用它标识测试执行环境;生产领域规则本身不需要 expect/actual。多平台官方文档分别列出 commonMain/commonTest 源集与 expect/actual 的对应关系,不要反过来为了演示特性而给每个纯函数编造一对平台实现。
实际测试 XML 各报告 tests="1"、failures="0":
1 | |
这两行不是把 JVM 输出复制到 Node 一次;jvmTest 与 jsNodeTest 分别生成了 XML,目标名称来自各自编译的 actual。用 JVM 的单元测试先跑一遍、再换一个标签,不能替代 JavaScript 运行时真正执行转移函数。完整断言、原始测试 XML、环境版本见 writing-plans/kotlin/verification/E02/。
第一次失败发生在工具链,不在状态机
初次运行 ./gradlew -p shared-model jvmTest jsNodeTest 时,构建卡在 :kotlinYarnSetup:Gradle 要从 GitHub 取 Yarn 1.22.17,当前环境无法解析 github.com,退出非零。那次失败仍保留在验证目录。Kotlin 官方 JS 项目配置说明可以使用 npm,故只在独立多平台项目设置 kotlin.js.yarn=false 和 kotlin.js.nodejs.download=false,使用已安装的 Node 22.23.2 与 npm 重跑。第二次 BUILD SUCCESSFUL 才是前述两目标结论的来源,不能倒写成第一次已经成功。
主工程随后在原先的 JVM 工具链上执行 clean test loopbackTest packageTest,退出 0,证明挪动 TaskState.kt 没有让既有测试的类型引用、真实回环和发行包测试失效;它不意味着现有服务已能在 Node 上运行。JS 环境这次只验证同步状态机和非法转移,并未运行协程任务树、数据库访问、HTTP 或序列化适配,也未核对浏览器的导出形状。TaskStore 与提交幂等检查继续由原 JVM 代码承担;以后若要把持久化接口纳入公共 API,必须重新逐平台选依赖和执行真实测试,而不是把 java.sql 接口移到 commonMain 后宣布成功。
复跑与边界
素材中的源码归档保留 Wrapper、原主工程和独立 shared-model;两套构建分别执行:
1 | |
需要 JDK 21、Node 22.23.2 与已缓存的 Gradle、Kotlin、npm 依赖;没有缓存时先允许依赖获取并记录来源。Android、Native、浏览器与跨进程事务仍是各自的实验,不在两目标 XML 覆盖范围里。站点页面只能证明这些命令、素材和论述能够阅读;类型规则是否被两端执行,要回到测试输出。
练习与答案
练习一:能否把 TaskStore 直接放进 commonMain,让 JS 目标也使用它?答案:不能。当前 TaskStore 属于 JVM 域模块,后续 JDBC/文件实现都依赖平台 API;要共享只能先抽象出真正跨平台的接口,再为每个目标写实际实现和测试。
练习二:expect fun targetName() 是否说明公共状态机需要平台实现?答案:不说明。它只是测试用来标识实际运行目标;advanceTask 仍是纯公共代码。
练习三:JVM 与 Node 两目标通过后,Android 或 Native 是否自动通过?答案:不能自动通过。Android 生命周期、Native 指针和浏览器/Wasm 导出都有各自运行边界,必须分别构建并运行。






