跳转到内容

Component 不是 Use Case

Component 是展示元素。

它渲染状态,接收用户交互,并向外发送 Intent。

听起来很简单,但实际项目里往往不是这样。

很多前端应用中的 Component 会在不知不觉中变成“带 Template 的小型 Application Service”:它们校验业务规则、转换数据、调用 API、决定导航、显示 Toast、刷新列表,同时还要维护本地状态。

到了这一步,它就不再只是 Component。

它成了穿着 Component 外衣的 Use Case。

Component 不是 Use Case。

Component 可以处理 UI 工作。

例如:

  • 展示数据
  • 消费 ViewModel
  • 持有局部 UI State
  • 接收用户交互
  • 发送简单的 UI Event
  • 根据状态调整展示

Component 可以知道 Dialog 是否打开,可以知道当前激活的是哪个 Tab,可以渲染表单,也可以在 ViewModel 表示某项操作不可用时禁用按钮。

但当用户点击按钮后,业务上到底应该发生什么,不应该由 Component 自己决定。

这才是关键。

通常会慢慢变成这样:

@Component(...)
export class OrderComponent {
constructor(
private http: HttpClient,
private router: Router,
private toast: ToastService,
) {}
async submitOrder(data: OrderData) {
const validated = this.validate(data); // Business-Logik
const enriched = this.enrich(validated); // Domänen-Transformation
await firstValueFrom(
this.http.post('/api/orders', enriched), // Infrastruktur
);
this.toast.success('Bestellung gespeichert'); // Seiteneffekt
this.router.navigate(['/confirmation']); // Navigation
}
private validate(data: OrderData) {
// fachliche Regeln
}
private enrich(data: OrderData) {
// Domänen-Transformation
}
}

乍看之下很高效。

所有东西都在一个地方,流程也很好跟踪:按钮点击,方法执行,结束。

问题只是:这个“一个地方”选错了。

此时 Component 已经知道:

  • 业务校验规则
  • 数据增强逻辑
  • HTTP 基础设施
  • Toast 行为
  • 导航
  • Use Case 的执行顺序

这已经不是展示元素。

这是一个挂着 HTML 的应用流程。

带 Template 的 Service。

问题不在于 Component “代码太多”。

代码多只是症状。

真正的问题是:它有太多不同的变化原因。

展示变化,Component 要改。 业务规则变化,Component 要改。 API 变化,Component 要改。 导航变化,Component 要改。 Toast 行为变化,Component 还是要改。

这就是不健康的耦合。

不是因为某张架构图会不高兴,而是因为变化不断流向成本最高的地方。

Component 通常更贴近 Framework 和 DOM,测试成本也更高。业务流程恰恰不应该藏在这里。

真正的问题不是:

Component 能不能有方法?

当然可以。

更好的问题是:

这个方法是否在做业务决策?

如果答案是“是”,它大概率就不属于 Component。

下面这种 Component 方法没有问题:

openDetails() {
this.detailsVisible.set(true);
}

这是 UI State。

而下面这种就值得警惕:

submitOrder(data: OrderData) {
if (data.items.length === 0) {
this.toast.error('Bestellung ohne Positionen ist nicht erlaubt');
return;
}
const payload = this.mapToPayload(data);
this.http.post('/api/orders', payload).subscribe(() => {
this.router.navigate(['/confirmation']);
});
}

这里同时出现了业务规则、Mapping、Infrastructure 和流程编排。

这已经不是 UI 行为了。

稳健的 Component 不负责描述完整流程。

它只表达意图。

@Component(...)
export class OrderComponent {
readonly vm = this.facade.vm;
constructor(private readonly facade: OrderFacade) {}
submit(data: OrderData) {
this.facade.submitOrder(data);
}
}

Component 只说:

用户想提交一笔订单。

它不需要知道接下来是不是 HTTP Call,不需要知道是否显示 Toast,也不需要知道是否导航或 Reload。

这正是边界的意义。

Intent 代替编排:Button click → submitOrder Intent → Facade/Use Case → Result → UI 渲染新的 ViewModel。Component 只有两个方向:ViewModel 进,Intent 出。

不是 Component。

根据具体架构,可以由 Facade、Store、Use Case Service 或 Application Layer 承担。

名称没有责任边界重要。

Component
-> sendet Intent
Facade / Store
-> koordiniert Zustand und UI-nahe Abläufe
Use Case / Application Service
-> führt fachlichen Ablauf aus
Infrastructure
-> spricht mit API, Storage, externen Systemen

或者更紧凑:

Component → Facade → Use Case → Infrastructure

Component 是系统的 UI 边缘。

它不是系统本身。

作为经验规则,我会尽量把以下内容留在 Component 之外:

  • HTTP 调用
  • Repository 访问
  • DTO Mapping
  • 业务校验
  • Domain Transformation
  • 复杂计算
  • Event 编排
  • Reload 链
  • 作为 Use Case 一部分的 Toast 和导航决策
  • 多数据源的手工同步
  • 超出简单显示判断的权限逻辑

单独出现一个并不意味着架构灾难。

但当其中几个同时出现时,Component 很可能已经越过自己的职责边界。

职责边界。

也不是什么都要抽出去。

否则只会变成架构表演。

以下内容完全可以留在 Component:

  • Dialog 的局部可见性
  • 当前激活的 Tab
  • Hover / Focus State
  • 简单 UI 交互
  • 转发一次按钮点击
  • 消费 ViewModel
  • 很简单的展示辅助逻辑

例如:

@Component(...)
export class OrderDialogComponent {
readonly dialogVisible = signal(false);
open() {
this.dialogVisible.set(true);
}
close() {
this.dialogVisible.set(false);
}
submit(formValue: OrderFormValue) {
this.submitted.emit(formValue);
}
}

这是 UI 行为。

Component 并不决定订单在业务上是否合法,也不决定 API Payload 长什么样,更不决定成功后导航到哪里。

如果 Use Case 藏在 Component 中,测试就必须穿过 UI Layer。

于是一个测试同时需要处理 DOM、Framework Lifecycle、Service Mock、Router、Toast 和业务规则。

这会很笨重。

如果 Component 只渲染 ViewModel 并发送 Intent,测试就会自然变小:

  • Component Test 验证展示
  • Use Case Test 验证业务流程
  • Mapper Test 验证转换
  • Store / Facade Test 验证状态与 Event Flow

每一层都测试自己真正负责的部分。

这不是形式主义。它会让测试更快、更稳定,也更容易读懂。

Component 可以触发一次交互。

但它不应该拥有完整的业务流程。

这就是下面两段代码的差别:

submit(data) {
this.facade.submitOrder(data);
}

和:

submit(data) {
const validated = this.validate(data);
const payload = this.map(validated);
this.http.post('/api/orders', payload).subscribe(() => {
this.toast.success('Gespeichert');
this.router.navigate(['/confirmation']);
this.reload();
});
}

第一种情况下,Component 表达意图。

第二种情况下,Component 拥有 Use Case。

Component 不是 Use Case。

它们是 UI 的边界。

它们渲染状态,接收交互,并发送 Intent。

但不应该编排业务逻辑、Infrastructure 或完整的业务流程。

好的 Component 往往有点无聊:

ViewModel 进。
Intent 出。
中间负责 Rendering。

这不是“少做架构”。

这恰恰就是架构的意义。