PAYATHON 2026

应用的启动和停止流程适合建模为 use-case 吗?

数据小陈

结论

可以把启动和停止流程封装成独立的编排对象。是否命名为 StartAppUseCase、StopAppUseCase,取决于它们管理的是采集系统的应用行为,还是进程和基础设施的初始化。

按照你的描述,抽取启动和停止流程是合理的。AppLifecycle、StartScrapingSystem 或 StartAppUseCase 都可以表达这个意图;类名本身不能决定架构是否合理。

“use-case 必须由外部系统发起,因此启动流程不能成为 use-case”这个判断过于绝对。可以从操作实现的目标、包含的应用规则和依赖的具体技术来判断。

先区分两种“启动”

原来的 start.ts 同时负责启动 scrapers 和 Telegram bot,后来又增加了两种启动通知。这里可能混合了两类职责。

职责例子常见放置位置
构建运行环境读取环境变量、创建具体客户端、组装依赖、注册进程退出处理启动入口、composition root
编排应用行为加载采集配置、启动采集任务、启用控制入口、发送运行通知应用服务、生命周期协调器,或 use-case

“启动 Telegram bot”也可能包含这两部分:创建 Telegram SDK 客户端属于具体技术的初始化;决定何时启用控制功能、失败后是否允许采集继续,则属于应用行为和策略。

如果“启动采集系统”在你的应用里意味着进入一个明确的工作状态,并且有前置条件、启动顺序和失败处理规则,把它建模为 use-case 是说得通的。

如果它只是依次创建几个对象、连接资源和启动进程内服务,命名为 AppLifecycle 或 Bootstrap 往往更直接。

外部触发并不能单独决定归属

use-case 通常表达参与者希望系统完成的目标。这里的参与者不必是另一个远程系统,触发方式也不必是 HTTP 请求。

CLI 命令、定时任务、消息处理器和启动入口都可以调用应用层操作。启动入口调用某个操作,不会自动使这个操作失去成为 use-case 的资格。

由外部系统触发的操作也不一定是业务 use-case,例如健康检查或底层连接维护。归属仍要看操作的目标和职责。

对这个 scraping system,可以尝试这样描述目标:

根据当前配置启动采集系统,使采集任务和控制入口可用,并发送启动通知。

如果这准确表达了应用需要提供的能力,就可以将其提取为应用层操作。

如何组织代码

可以让 start.ts 负责组装依赖,把执行顺序交给独立对象。下面是接口设计示例,接口名用于说明职责,并非某个库提供的 API。

interface ConfigProvider<C> {
  load(): Promise<C>;
}

interface Scrapers<C> {
  start(config: C): Promise<void>;
  stop(): Promise<void>;
}

interface Bot {
  start(): Promise<void>;
  stop(): Promise<void>;
}

interface Notifications {
  started(): Promise<void>;
  stopped(): Promise<void>;
}

class AppLifecycle<C> {
  constructor(
    private readonly config: ConfigProvider<C>,
    private readonly scrapers: Scrapers<C>,
    private readonly bot: Bot,
    private readonly notifications: Notifications,
  ) {}

  async start(): Promise<void> {
    const config = await this.config.load();
    await this.scrapers.start(config);
    await this.bot.start();
    await this.notifications.started();
  }

  async stop(): Promise<void> {
    const results = await Promise.allSettled([
      Promise.resolve().then(() => this.bot.stop()),
      Promise.resolve().then(() => this.scrapers.stop()),
    ]);

    if (results.some((result) => result.status === "rejected")) {
      throw new Error("Some components failed to stop");
    }

    await this.notifications.stopped();
  }
}

Notifications 的具体实现可以负责发送 Telegram 通知和邮件。协调器只依赖通知接口,无须直接调用这些服务的 SDK。

这个示例有两个前提:组件可以并行停止,启动方法在组件就绪后返回。如果实际组件存在停止顺序要求,或者其 start() 会一直等待运行循环结束,就需要调整实现。

如果团队习惯每个 use-case 只暴露一个 execute() 方法,也可以把这个类拆成 StartAppUseCase 和 StopAppUseCase。是否拆分可以根据职责和依赖关系来决定,无须仅为符合命名习惯而拆分。

启停流程需要明确哪些规则

示例展示了编排位置,还没有实现完整的生命周期管理。实际设计中,需要明确以下规则:

  • 何时算启动成功。scrapers 和 bot 都已就绪,还是只要采集任务就绪即可?启动通知应当符合这个定义。
  • 部分启动失败如何处理。如果 scrapers 已启动,而 bot 启动失败,是否需要停止已经启动的 scrapers?上面的简化示例没有实现这类回滚。
  • 通知失败是否影响运行。如果 Telegram 或邮件只是辅助通知,可以记录失败并继续运行;如果通知是运行要求,就需要把失败纳入操作结果。当前示例会向调用方传播通知异常,但不会因此自动停止组件。
  • 如何处理重复调用。连续两次启动、尚未启动就停止,以及启动过程中收到停止请求,都需要有明确行为。
  • 停止失败后如何继续清理。一个组件停止失败时,通常仍应尝试清理其他组件,并保留失败原因。示例使用 Promise.allSettled 等待两项清理完成,但只抛出了汇总错误,实际实现可进一步收集具体原因。

如果 bot 的在线状态会影响采集任务能否安全运行,启动和停止顺序也应体现这个约束。

对这个项目的建议

可以先把已有流程提取为 AppLifecycle,让 start.ts 保留组装依赖和调用启动操作的职责。进程退出信号也由入口接收,再转交给停止操作处理。

如果后续出现暂停采集、恢复采集、重新加载配置等能力,可以分别建模为应用层操作,启动流程再按需要调用它们。这样可以理清流程,避免所有功能最终都堆进一个启动类。

如果你的团队已经统一使用 *.use-case.ts,而“启动采集系统”确实代表明确的应用目标,采用 start-app.use-case.ts 和 stop-app.use-case.ts 完全可以。用例应负责流程和策略,具体客户端、进程 API 与资源创建留在外围。

备注:内容仅供参考。