☰
深入Hammer代码架构:Clean分层设计与Kotlin Multiplatform模块结构拆解
2026/10/27 1:27:39 网站建设 项目流程

【免费下载链接】hammer-editor

A simple tool for building stories.

项目地址:https://gitcode.com/gh_mirrors/ham/hammer-editor
点击查看免费下载

Hammer 是一款免费开源的跨平台故事编辑器(story editor),支持 Android、iOS、Windows、macOS 和 Linux。本文带你用最少代码、最快路线拆解它的 Clean 分层架构与 Kotlin Multiplatform 模块结构,帮你在第一次打开仓库时就能建立完整地图 🪨

一、先认识 Hammer:一个"数据在你手里"的跨平台故事编辑器

在拆架构之前,先花 30 秒理解这个产品要解决什么问题,架构设计才有落脚点:

  • 离线优先(Offline first):完全本地运行,不依赖网页技术,长时间离线写作不丢数据
  • 数据透明(Transparent Data):项目就是人类可读的普通文件与文件夹,不锁在云端的黑盒数据库里
  • 可选同步:可自部署同步服务器,也可用 Dropbox、Syncthing 等任意方式同步
  • 多端一致:手机、平板、桌面端共用同一份业务代码,界面按屏幕自适应

它的核心写作功能围绕四大模块组织:场景(Story)、百科(Encyclopedia,人物/地点/事件)、笔记(Notes)、时间线(Timeline),再加一个能穿透到任意内容的全局搜索。

理解了"数据透明 + 多端一致 + 离线优先"这三个产品目标,你会发现后文的每个架构决策几乎都能直接对应上。

二、Clean 架构总览:六层结构,依赖只允许向下

Hammer 采用的是一套严格约束的 Clean 风格分层。完整规范写在官方文档 docs/ARCHITECTURE.md 中,这是全仓库最重要的架构契约。层级从"最靠近数据"到"最靠近界面"排列为:

Data Sources → Foundation → Repositories → Services → Use Cases → ViewModels

其中强制主干只有三层:Data Source → Repository → ViewModel。Services 和 Use Cases 是可选层,只在真实需求出现时才插入,官方明确反对"预判式建层"。

层级有状态?生命周期必须?
Data Source 数据源无状态工厂创建,每次新实例✅
Foundation 基础原语有状态作用域单例固定集合
Repository 仓库有状态作用域单例✅
Service 服务有状态作用域单例❌ 可选
Use Case 用例无状态工厂创建❌ 可选
ViewModel(Component)—每屏幕一个✅

💡 本项目把最高层称为Component(Decompose 组件),等价于其他架构里的 ViewModel,两个词在文档中通用。

Foundation:一个"固定的三件套"

Foundation 层不是随便加东西的地方,而是全数据层公用的固定有状态基础设施,目前只有三个成员:

  • IdAllocator— 分配全项目单调递增的实体 ID:IdAllocator.kt
  • SyncJournal— 记录等待与服务器对账的脏数据(位于 data/sync/)
  • GlobalSettingsStore— 应用级与服务器设置:GlobalSettingsStore.kt

三者构成无向环的 DAG(GlobalSettingsStore ← SyncJournal ← IdAllocator),这是全仓库中唯一允许有状态组件横向引用同层的地方——正因为集合是手工保持无环的。

决策规则:该加哪一层?

官方文档给出了极简判断法,值得背下来:

  • 需要 Use Case?只有当存在无状态业务逻辑/组合时才加;如果只是把一次调用原样转发给 Repository,就别加
  • 需要 Service?只有当需要跨两个及以上 Repository协调且带共享状态时才加(Repository 之间禁止互相引用,协调逻辑只能放这里)
  • 需要 Foundation 原语?几乎永远不要。"两个 Repository 都需要它"不是理由——那是 Service 的职责

三、实战拆解:一次"场景编辑"如何穿过各层

纸上谈兵不如看真实案例。docs/DESIGN-PATTERNS.md 记录了一次教科书式的分层重构:场景编辑域曾经是约 1300 行、捆绑 12 项职责的SceneEditorRepository,最终被拆成"一个门面 + 三个仓库":

  • SceneEditorService— 组件层唯一入口,负责写操作编排(在每条命令周围挂上统计、引用索引、同步标记等横切副作用):SceneEditorService.kt
  • SceneRepository— 场景树、结构、路径计算、同步哈希,纯数据:SceneRepository.kt
  • SceneContentRepository— 编辑引擎:内存缓冲区、内容防抖管道、临时缓冲区自动保存:SceneContentRepository.kt
  • SceneMetadataRepository— 场景与项目元数据的持久化

注意一个关键设计:Component 层只能透过SceneEditorService这一个门访问场景编辑域,调用者永远不需要纠结"该注入 repo 还是 service"。而 Service、Use Case、同步层仍然可以直接消费下层 Repository——Service → Repository这条边是合法的,只有最高层被收口。

依赖注入由 Koin 完成,约定很简单:无状态组件(Data Source、Use Case)工厂创建;有状态组件(Repository、Service)作用域单例,且作用域尽量选窄(会话、导航图),别一股脑塞进应用级单例。入口模块见 dependencyinjection/。

四、Kotlin Multiplatform 模块结构:7 个 Gradle 模块各管一摊

在 settings.gradle.kts 中可以看到完整的模块清单。这是理解 KMP 结构的第一张地图:

模块职责关键内容
base/最底层共享原语ID 类型、markdown 工具、http、校验、diff
common/核心业务(约 350 个文件)数据层六层全家桶 + 组件层 + 同步协议
composeUi/跨平台 UI(约 250 个文件)Compose Multiplatform 界面组件
android/Android 宿主Activity、Widget、通知
desktop/桌面宿主窗口管理、系统快捷键、原生集成
server/独立同步服务器Ktor + SQLDelight + PostgreSQL
wear/手表端(非 F-Droid 构建)Wear Compose 配对界面
integrationTests/集成测试端到端场景验证

模块之间的依赖方向非常克制:base在最底,被所有模块依赖;common承载几乎全部业务逻辑;composeUi消费common的组件;各平台模块(android/desktop/wear)只是薄薄的一层"胶水",负责启动、权限和平台 API 接入。技术栈版本统一收敛在 gradle/libs.versions.toml 中:Kotlin 2.4、Compose Multiplatform、Decompose 3.5(组件框架)、Koin(DI)、Ktor、okio 文件系统抽象。

服务器模块是独立世界:它不复用客户端的 common 代码,而是按自己的轻量分层组织——Routes → Repository → Datasource(见 server/src/main/),数据库层用 SQLDelight 管理 schema 与迁移(server/src/main/sqldelight/)。

五、同一份代码跑五种端:源码集(Source Set)是怎么切分的

模块是"横向"切分,KMP 的源码集则是模块内部的"纵向"切分。以核心模块common为例,它的src/目录长这样:

  • commonMain— 编译到所有平台的共享代码(业务逻辑全在这里)
  • desktopMain/androidMain/iosMain/jvmMain— 各平台实现与胶水
  • commonTest/desktopTest/iosTest— 各端测试

一个有趣的工程细节:因为 mock 库 mockk 尚不支持 Kotlin/Native,绝大多数单元测试被放在desktopTest源码集里跑(gradlew desktopTest),这是 DEVELOPMENT.md 明确说明的取舍。

测试还承担了架构守卫的职责,例如 PersistedFormatOwnershipTest.kt 强制"每种磁盘文件格式只允许一个 Data Source 拥有"——历史上两个独立的project_data.toml写入方曾互相擦掉对方字段,这条规则就是事故教训固化成的自动化检查。

六、给读源码的你:核心依赖规则速查

官方把最容易踩的坑浓缩成了硬性约束,收藏 docs/ARCHITECTURE.md 即可:

  1. 只允许向下依赖:任何组件只能引用比它更低的层
  2. 禁止同层互引(No sibling references):这是保证依赖图天然无环的关键
  3. 唯一例外:无状态的 Use Case 可以引用其他 Use Case
  4. Use Case 之间用懒注入(Provider<T>或 lambda),无状态不足以防止构造循环
  5. ViewModel 永远不直接碰 Data Source,必须经过 Repository 或更上层
  6. Foundation 原语是下层共享件,不是兄弟,任何层引用它都算"向下"
  7. 一种持久化格式只有一个属主,由架构测试强制

七、源码导航清单:按这个顺序读效率最高

最后给出一份最小阅读路径,从产品到代码逐层下钻:

  • 📖 架构总纲:docs/ARCHITECTURE.md
  • 📖 设计模式目录(深链导航、场景编辑域 API 等范例):docs/DESIGN-PATTERNS.md
  • 🗺️ 模块清单与构建入口:settings.gradle.kts、gradle/libs.versions.toml
  • 🧩 组件层入口(屏幕栈与导航):components/projectroot/
  • 🗄️ 数据层主战场:data/(30+ 个领域子包)
  • 🔌 依赖注入装配:dependencyinjection/
  • 🖥️ 开发指南与测试哲学:DEVELOPMENT.md
  • 📦 构建元数据(版本、渠道、发布脚本):buildSrc/

小结:Hammer 的架构精髓可以浓缩为一句话——用"只向下依赖"的 Clean 分层保证业务代码与平台无关,再用 KMP 的源码集切分把这一份业务代码铺到五种操作系统上。读懂common模块里的六层数据流和base/composeUi的边界,你就掌握了这个代码库 90% 的脉络。

【免费下载链接】hammer-editor

A simple tool for building stories.

项目地址:https://gitcode.com/gh_mirrors/ham/hammer-editor
点击查看免费下载

相关推荐

上一篇:Gofile.io 文件下载工具使用指南
下一篇:Gofile.io文件下载工具使用指南:从基础操作到高级技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询