跳转到内容

Application——面向 View 的 Domain API

+State 文章 已经为业务逻辑提供了明确归属:业务相关 State、Business Rule、派生、Selection、State Transition 和业务 Intent 都在那里。

但一个具体 View 通常只需要其中很小的一部分:

customers
loading
selectCustomer()
createCustomer()

Application 从这里开始。

如果这一层变得复杂,通常意味着某些原本属于它下面或上面的职责滑了进来。

在经典 DDD Layering 中,Application 层常常与 Application Service、Use Case 以及它们的协调联系在一起。它会控制一个 Use Case 的执行,但不拥有真正的 Business Rule。

这里的模型把边界切得更窄。业务决策、State Transition 和 Projection 已经存在于 +State,因此在它上面不会再建立第二层 Use-Case Orchestration。

所以 Application 刻意比很多经典 DDD 架构更薄。理想情况下,这一层几乎只由 Facade 组成。

这不是偷懒,而是有意识的前端解释:View 需要一个稳定入口访问 Domain,但不需要再多一个地方重新产生业务逻辑。

Presentation 不应该需要知道 Domain 内部也许由 CustomerStateOrderStatePermissionStateSelectionState 组成。

没有 Facade 时,一个 View 可能直接访问多个内部区域:

customerState.customers;
customerState.selectedCustomer;
orderState.canOrder;
permissionState.canEdit;

这样一来,Domain 的内部切分就变成了 Presentation 必须知道的知识。

有了 Facade,View 只需要看到:

facade.customers;
facade.selectedCustomer;
facade.canOrder;
facade.canEdit;

Facade 了解下面的结构。View 只了解自己的 Contract。

Customer List View 只通过 CustomerListFacade 访问 Domain 内多个 State 区域。

这个基本思想来自经典 Facade Pattern:Facade 为一个更复杂的 Subsystem 提供更简化的入口(Design Patterns: Elements of Reusable Object-Oriented Software,Gamma/Helm/Johnson/Vlissides,1994)。这里把同一原则应用到 Presentation 与 Domain 之间的边界。

如果熟悉经典 MVC,可以大致把这个位置理解为靠近 View 的 Controller:UI 与 Model 之间有一个明确入口。但这里 Facade 的职责更窄——它不会发展出自己的业务逻辑或 Application Logic。

Facade 可以收集 +State 中已有的值和数据流,并通过合适的 Contract 暴露出去:

export class CustomerDetailFacade {
readonly customer = this.customerState.selectedCustomer;
readonly canDelete = this.customerState.canDelete;
readonly loading = this.customerState.loading;
deleteCustomer(): void {
this.customerState.deleteSelected();
}
}

这是一个好的 Facade。它负责暴露、委托,并隐藏内部结构。它没有产生新的业务含义。

同一个 Contract 中可以并列暴露多个已经存在的 Stream 或 Reactive Value。Facade 不应该做的是:在这里才第一次计算出新的业务结论。

readonly canOrder = combineLatest([
this.customerState.customer$,
this.orderState.orders$,
this.permissionState.permissions$,
]).pipe(
map(([customer, orders, permissions]) => {
// 业务决策在这里才产生
}),
);

如果多个数据源共同形成一条业务结论,这条结论就应该已经存在于 +State

Application 只暴露结果:

readonly canOrder = this.orderState.canOrder;

Facade 可以收集多个数据流。

它不负责在业务上把这些流解释成新的含义。

Facade 不做决策。

Business Rule 不属于这里:

save(): void {
if (this.customerState.customer().status === CustomerStatus.Active) {
// ...
}
}

业务派生也不属于这里:

readonly visibleCustomers = computed(() =>
this.customerState.customers()
.filter(...)
.sort(...),
);

前提是 Filter 与 Sort 属于 State 或具有业务意义的 Projection。

UI 职责同样不属于这里:

delete(): void {
if (confirm('Really delete?')) {
// ...
}
}

Facade 可以做的是简单委托:

deleteCustomer(): void {
this.customerState.deleteSelected();
}

或者暴露已有 State:

readonly canDelete = this.customerState.canDelete;

这些方法看起来“太简单”是刻意的。如果 Facade 开始变得有趣,就值得检查:是不是有别的 Layer 的职责正在流进来。

如果 Facade 只是暴露和委托,Presentation 理论上也可以直接访问 +State

代价是:它开始了解 Domain 的内部结构。

今天可能只是:

CustomerListComponent
CustomerState

新的需求一来,依赖就开始增加:

CustomerListComponent
├── CustomerState
├── PermissionState
├── SelectionState
└── OrderState

这样,Domain 内部如何拆分就变成 View Contract 的一部分。

有 Facade 时,View 始终只有一个入口:

CustomerListView
CustomerListFacade
Domain 内部结构

Domain 下面怎么重新切分,Presentation 不需要跟着理解。

这就是这层看似“没什么代码”的结构真正价值:降低结构性耦合。

这个模型里的规则非常简单:

每个 View 都有且只有一个对应 Facade。

CustomerListView ↔ CustomerListFacade
CustomerDetailView ↔ CustomerDetailFacade
CustomerEditView ↔ CustomerEditFacade

一开始,一个通用 CustomerFacade 服务所有 Customer View,看起来很诱人:

CustomerList
CustomerDetail
CustomerEditor
CustomerFacade

但随着 View 增加,它的 Contract 也会一起膨胀:

CustomerList
CustomerDetail
CustomerEditor
CustomerHistory
CustomerSearch
CustomerAdmin
CustomerExport
CustomerFacade

最初只有 customersselectedCustomer,后来会不断加入 historyorderspermissionssearchfiltersortcreateupdatedeletearchiveexport 等能力,而每个 Consumer 往往只需要其中一小部分。

Facade 最终变成 God Service。

真正的问题是缺少自然边界。三个、五个还是十个 View,都不是有意义的架构阈值。某个“共享 Property 百分比”也不可能提供稳定规则。

1:1 关系刻意把这件事变得非常简单:

Facade 的职责边界,就是它所服务 View 的职责边界。

不需要 Size Heuristic,也不需要争论 Property 最多允许多少个。View 本身就是边界。

一个服务多个 View 的巨大 CustomerFacade,与每个 View 都拥有自己小型 Facade 的结构形成对比。

1:1 规则必然会产生一些相似、甚至完全一样的 Facade:

export class CustomerListFacade {
readonly customers = this.customerState.customers;
}
export class CustomerSearchFacade {
readonly customers = this.customerState.customers;
}

这是故意的。

两个 Contract 可以独立演进。今天它们恰好暴露同一个 Projection,不意味着应该把两项职责绑定到一起。

重复委托很便宜。耦合职责很昂贵。

这不是反 DRY。真正要看的仍然是 Change Reason。CustomerListFacade 在 Customer List Contract 变化时修改,CustomerSearchFacade 在 Search View Contract 变化时修改。

因此 Facade 回答的不是:

我们的应用对 Customer 一共能做什么?

而是:

这个具体 View 需要从 Domain 得到什么?

CustomerListFacadeCustomerDetailFacadeCustomerEditFacade 这样的命名会把这项职责直接显示出来。Facade 是一份面向 View 的 Domain API,只暴露这个 View 被允许消费的内容。

Application 可以做什么,不该做什么

Section titled “Application 可以做什么,不该做什么”

Application / Facade 可以:

  • 消费 +State
  • 暴露已有 State 和 Projection
  • 收集已有 Stream
  • 把 Intent 和 Command 委托给 +State
  • 提供面向具体 View 的 Contract
  • 向 Presentation 隐藏 Domain 内部结构
  • 把多个内部 Domain Source 封装在一个 View Contract 后面
  • 暴露已经生成的 ViewModel 或 Projection

Application / Facade 不应该:

  • 实现 Business Rule
  • 做业务决策
  • 生成新的业务 Derived State
  • 在这里才从业务上组合 Stream
  • 建模自己的 State Transition
  • Parse DTO
  • 调用 HTTP 或外部 SDK
  • 解释技术 Contract
  • 控制 Router
  • 打开 Dialog、Toast 或 Snackbar
  • 了解 DOM 或 UI Framework 细节
  • 做 Material、Ionic 或 Bootstrap 专属决策
  • 为了复用,把多个 View 塞进一个共同的 God Facade

一个 View 只拥有一个进入 Domain 的入口。它不需要知道单个值来自哪里,也不需要知道 Contract 后面有几个 Store 或 Projection。Domain 可以内部重构,而不迫使每个表现层一起理解这次变化。

Change Reason 也因此保持局部。Customer Editor 新增能力,不会自动扩大 Customer List 的 Contract。每个 Facade 只需要和它对应的 View 一样大。

这会在一些地方造成 Delegation 重复。但那是独立 Contract 的成本,而且成本很低。

所以 Application 层真正的收益,不来自里面有多少“聪明代码”。

而来自这些代码让边界变得可见。

一个现实中的 Application Layer 可能几乎只包含这样的类:

export class CustomerListFacade {
readonly customers = this.customerState.customers;
readonly loading = this.customerState.loading;
selectCustomer(id: CustomerId): void {
this.customerState.select(id);
}
createCustomer(): void {
this.customerState.create();
}
}

这并不说明这一层“没有必要”。价值在 Contract 和解耦结构,不在代码数量。

好的 Facade 很无聊。它不会重新发明业务逻辑,只会让已有业务能力以适合一个具体 View 的方式被访问。

+State 拥有业务逻辑。Application 为一个具体 View 提供合适 Contract。Domain 不知道 View。Facade 了解两边,但不会创造新的业务含义。

Contract 的另一侧,就是 Presentation。