分层和解耦对我的诱惑太大了, 最近写业务有一些想法.
##
比方说一个服务有两种协议 http 和 grpc, 再加上一个 cli 吧, 算是三种完全不同的 UI 层了, 共享同一个 App 层的函数:
这个函数的参数已经够多了, 理论上来说, 我需要用一个 struct 来统筹一下, 比如叫 CreateOptions 的类型, 不过最核心的问题是, 这个 CreateOptions 类型在哪一层?
如果在下层 Model, 属于建模的一部分, 那么模型泄漏了, App 层知道得太多了; 如果放在当前 App 层, 那么下层又会依赖上层, 下层对上层的非接口耦合又让我感到不适, 依赖倒置原则说的可是接口.
当然直接使用领域模型的好处也是显然的, 简化参数就不说了, 首先我们有了更可靠的类型检查, 其次对于返回类型更加容易.
来说第二点, 看上面的代码里返回的是
第一种做法, 返回接口, 这种接口取决与更上层 UI 的使用; 比方说我们有两个 UI, 一个是 http 返回 JSON 一个是 grpc, 那么我们根据业务把 UI 层感兴趣的字段抽出来, 实现一个 interface:
这样这个 App 层的函数就变成了:
注意这时候虽然下层 App 依然要依赖上层 UI 定义的接口 InstanceProvider, 但是这是符合依赖倒置原则的, 没有任何的硬耦合.
然后 UI 层就可以放肆地调用接口定义的方法来填充自己的 Presentation Model, 而不用知道调用的真实的类型:
grpc UI 就不写了, 类似.
要注意 App interface 的定义也是在 UI, 而实现是在 App 层, 这是标准的依赖倒置, 就不说了.
另一种做法可能是传入一个接口对象, 比如叫 InstanceInterest:
然后传入 App 的函数:
在 App 层最后调用 InstanceInterest 的方法来通知上层; 此时的 App 的类型依然没有模型泄漏, 耦合也是下对上的接口, 很好.
我个人更倾向方案一, 方案二的这种
##
App 作为 Model 的客户端应该是很轻的一层, 但是我的主要问题是事务.
比方说具体的业务是要通过 Kubernetes SDK 去创建 Nginx, 同时管理好自己的元数据, 那么至少有两种流派:
第一种是先做事, 再创建元数据:
为了实现事务, 必须用 defer 处理"创建好容器但是写元数据失败"的情况; 当业务恶心的时候, App 层的实现简直不看入木.
或者, 第二种我们先写元数据:
这时候我们先建模, 上来先创建一个内存对象 nginx, 然后先存元数据再说, 最后再操作副作用.
如果我们再把 postCreateNginx 扔到单独的 goroutine 运行, 然后定义 nginx 对象的生命周期, 那就太美好了:
有限状态机可以搬出来了, 这时候事务已经被压缩到最小化, 你发现我们没有 defer 处理回滚, 因为事务只有一件事"写元数据", 而创建容器是单独的后台运行的, 运行后无论成功失败都记录到元数据里, 如果本身要做的事情特别多的话, 定义生命状态周期, 分步进行, 记录状态, 比一个巨大的事务可控得多.
而对上层和客户来说, 如果有一个 web ui, 这种异步模式也友好得多, 用户创建后能立刻看到自己创建的东西和状态, 而不是等浏览器响应等个两分钟, 都不知道是网络问题还是什么问题, 也不敢刷新怕重复创建.
DDD 里反复强调小聚合, 多做最终一致性, 这里也能有体现.
而且 App 层也会变得特别简单, "express user story", "simple invocations" 都能得以实现.
要是再 DDD 一点的话, 这里该上领域事件了, 或者 Event Bus, watever, remote 地处理的话, 那整个架构又可以 FaaS 化, 因为 event 驱动本身就很好 FaaS 化, 加上每个 event 的事务单一, 对于失败的创建, 无论是人工触发补偿或者回滚都没问题, 或者有有个 event center 做自动补偿回滚; 不过无论哪样, 我们的事务是能保证的, 也就是说不会出现 dangling container (容器在跑了但是没有记录下元数据) 或者 empty instance (元数据记录但是容器没有), 或者是一失败就 cascade delete 把啥都删了你都不知道怎么 debug; 精细化的生命周期管理, 小事务小聚合, 不管怎么做都好.
from https://gist.github.com/jschwinger23/38d8b5c7d35ba0b8bf58564357f8f448#file-layer-and-decouple-md
##
UI -> App比方说一个服务有两种协议 http 和 grpc, 再加上一个 cli 吧, 算是三种完全不同的 UI 层了, 共享同一个 App 层的函数:
type App interface {
func CreateInstance(name string, count, memory, cpu int, network, podname string, dns, env []string) *Instance
}
这个函数的参数已经够多了, 理论上来说, 我需要用一个 struct 来统筹一下, 比如叫 CreateOptions 的类型, 不过最核心的问题是, 这个 CreateOptions 类型在哪一层?
如果在下层 Model, 属于建模的一部分, 那么模型泄漏了, App 层知道得太多了; 如果放在当前 App 层, 那么下层又会依赖上层, 下层对上层的非接口耦合又让我感到不适, 依赖倒置原则说的可是接口.
当然直接使用领域模型的好处也是显然的, 简化参数就不说了, 首先我们有了更可靠的类型检查, 其次对于返回类型更加容易.
来说第二点, 看上面的代码里返回的是
*Instance, 其实就是模型泄漏了; 但是如果你真的要返回一个有信息量的东西而不仅仅是 ID 什么的, 也不是 mapstringinterface{} 这种不反射都不知道怎么遍历的东西, 那么工作量一下就大起来了.第一种做法, 返回接口, 这种接口取决与更上层 UI 的使用; 比方说我们有两个 UI, 一个是 http 返回 JSON 一个是 grpc, 那么我们根据业务把 UI 层感兴趣的字段抽出来, 实现一个 interface:
type InstanceProvider interface {
ProvideID() string
ProvideStatus() string
}
这样这个 App 层的函数就变成了:
type App interface {
func CreateInstance(name string, count, memory, cpu int, network, podname string, dns, env []string) InstanceProvider
}
注意这时候虽然下层 App 依然要依赖上层 UI 定义的接口 InstanceProvider, 但是这是符合依赖倒置原则的, 没有任何的硬耦合.
然后 UI 层就可以放肆地调用接口定义的方法来填充自己的 Presentation Model, 而不用知道调用的真实的类型:
func (ui *UI) CreateInstance(w http.ResponseWriter, r *http.Request) {
provider := ui.app.Create(r.FormValue("name"))
model := newHTTPPresentModel(provider)
fmt.Fprintf(w, model.toJSON())
}
grpc UI 就不写了, 类似.
要注意 App interface 的定义也是在 UI, 而实现是在 App 层, 这是标准的依赖倒置, 就不说了.
另一种做法可能是传入一个接口对象, 比如叫 InstanceInterest:
type InstanceInterest interface {
InformID(string)
InformStatus(string)
}
然后传入 App 的函数:
type App interface {
func CreateInstance(name string, count, memory, cpu int, network, podname string, dns, env []string, interest InstanceInterest) error
}
在 App 层最后调用 InstanceInterest 的方法来通知上层; 此时的 App 的类型依然没有模型泄漏, 耦合也是下对上的接口, 很好.
我个人更倾向方案一, 方案二的这种
value-result 模式太 C 了; 不过如果真的没那么介意模型泄漏的话, 也是可以忍受 App 层类型直接使用领域模型, 这里讨论的是"如果我一定不使用领域模型"怎么办.##
App -> ModelApp 作为 Model 的客户端应该是很轻的一层, 但是我的主要问题是事务.
比方说具体的业务是要通过 Kubernetes SDK 去创建 Nginx, 同时管理好自己的元数据, 那么至少有两种流派:
第一种是先做事, 再创建元数据:
func (a *App) CreateNginx(name string) NginxProvider {
virtualization, err := a.orchestrator.CreateVirtualization(...)
defer func() {
if err != nil {
a.orchestrator.RemoveVirtualization(virtualization)
}
}()
err = a.store.Save(virtualization)
return virtualization
}
为了实现事务, 必须用 defer 处理"创建好容器但是写元数据失败"的情况; 当业务恶心的时候, App 层的实现简直不看入木.
或者, 第二种我们先写元数据:
func (a *App) CreateNginx(name string) NginxProvider {
nginx := newNginx(name)
a.store.Save(nginx)
a.postCreateNginx(nginx)
return nginx
}
这时候我们先建模, 上来先创建一个内存对象 nginx, 然后先存元数据再说, 最后再操作副作用.
如果我们再把 postCreateNginx 扔到单独的 goroutine 运行, 然后定义 nginx 对象的生命周期, 那就太美好了:
func (a *App) postCreateNginx(nginx) {
if err := a.orchestrator.CreateVirtualization(nginx); err != nil {
nginx.UpdateStatus(Created)
} else {
nginx.UpdateStatus(CreateFailed)
}
}
有限状态机可以搬出来了, 这时候事务已经被压缩到最小化, 你发现我们没有 defer 处理回滚, 因为事务只有一件事"写元数据", 而创建容器是单独的后台运行的, 运行后无论成功失败都记录到元数据里, 如果本身要做的事情特别多的话, 定义生命状态周期, 分步进行, 记录状态, 比一个巨大的事务可控得多.
而对上层和客户来说, 如果有一个 web ui, 这种异步模式也友好得多, 用户创建后能立刻看到自己创建的东西和状态, 而不是等浏览器响应等个两分钟, 都不知道是网络问题还是什么问题, 也不敢刷新怕重复创建.
DDD 里反复强调小聚合, 多做最终一致性, 这里也能有体现.
而且 App 层也会变得特别简单, "express user story", "simple invocations" 都能得以实现.
要是再 DDD 一点的话, 这里该上领域事件了, 或者 Event Bus, watever, remote 地处理的话, 那整个架构又可以 FaaS 化, 因为 event 驱动本身就很好 FaaS 化, 加上每个 event 的事务单一, 对于失败的创建, 无论是人工触发补偿或者回滚都没问题, 或者有有个 event center 做自动补偿回滚; 不过无论哪样, 我们的事务是能保证的, 也就是说不会出现 dangling container (容器在跑了但是没有记录下元数据) 或者 empty instance (元数据记录但是容器没有), 或者是一失败就 cascade delete 把啥都删了你都不知道怎么 debug; 精细化的生命周期管理, 小事务小聚合, 不管怎么做都好.
from https://gist.github.com/jschwinger23/38d8b5c7d35ba0b8bf58564357f8f448#file-layer-and-decouple-md