状态的演进:从混沌单体到边界清晰的模块化格局

状态的演进:从混沌单体到边界清晰的模块化格局

核心观点 :状态管理的本质并非"置于何处",而是 "谁拥有写入权限、状态如何跨越边界流动、以及以何种契约触发变更" 。在此框架下,启动项(Composition Root) 是规则的声明者,DI 容器 是生命周期的执行者,而 业务边界 则决定了状态的封装粒度。

讨论边界界定:本文所指的"状态"特指运行时内存中的成员变量、静态字段或集合容器。它不等同于方法参数或数据库持久化记录,但包含持久化数据在内存中的变更快照(如对象关系映射框架中的变更追踪器)。


一、小型项目(单进程单体):主权在启动项,一切尽在眼前

在简单的控制台应用或微型 Web API 中,Program.cs 直接负责所有对象的创建(new)。此时没有容器,也无模块边界,状态管理直观而透明。

csharp 复制代码
class Program
{
    static void Main()
    {
        var config = new Config();           // 根状态
        var logger = new Logger(config);     // 依赖传递
        var service = new Service(logger);   // 链式传递
        service.Run();
    }
}

状态图谱(平坦结构)

text 复制代码
┌─────────────────────────────────────┐
│           启动项 (Program)           │
│  持有:Config、LoggerFactory        │
│  主权:声明权 + 创建权 + 销毁权     │
└──────────────┬──────────────────────┘
               │ 构造函数传递(显式参数)
               ▼
┌─────────────────────────────────────┐
│         业务类 (Service)            │
│  持有:仅局部临时变量               │
│  主权:0%(全部从外部传入)         │
└─────────────────────────────────────┘

规律与特征 :启动项是唯一的"状态工厂"。所有状态的生命周期与进程完全同步,无线程安全顾虑(单线程或简单锁机制)。此时,"治理"尚未成为问题,一切尽在掌控。状态更新机制为手动重新 new 或直接修改对象属性。


二、中型项目(分层架构):引入 DI 容器,职责分离

随着系统规模增长,我们引入分层架构(Controller → Service → Repository)并采用依赖注入(DI)容器来管理对象的创建与生命周期。此时,主权开始分化:

  • 声明主权:仍归启动项(负责注册类型映射与生命周期策略)。
  • 执行主权(创建、缓存、释放):移交给了 DI 容器。
csharp 复制代码
// 启动项 ------ 状态规则的"宪法"(框架无关的注册示例)
var container = new DiContainer();
container.Register<IConfiguration, Configuration>(Lifetime.Singleton);
container.Register<IRequestContext, RequestContext>(Lifetime.Scoped);
container.Register<IOrderService, OrderService>(Lifetime.Scoped);
container.Register<IOrderRepository, OrderRepository>(Lifetime.Scoped);

这里不依赖任何特定 DI 框架(如 ASP.NET Core 内置容器、Autofac、Unity 等),而是采用抽象的注册与生命周期概念。启动项只需声明"何种类型在何种生命周期下由容器提供",容器则负责按此规则创建、缓存和释放实例。

澄清关键概念

对象关系映射(ORM)框架中的变更追踪器 (如 EF Core 的 ChangeTracker)维护的是持久化数据在内存中的"工作单元"快照,保存了实体对象的原始值与当前值的差异。它充当内存状态与磁盘记录之间的双向映射缓冲,其本身并非数据库中的持久化记录。

状态图谱(单向依赖链)

text 复制代码
┌─────────────────────────────────────────────────────────┐
│              启动项 (Composition Root)                   │
│  声明:IConfiguration(Singleton)、各Service(Scoped)│
│  执行权:交由 DI 容器管理                               │
└────────────────────────┬────────────────────────────────┘
                         │ 容器依据注册表创建实例
                         ▼
┌─────────────────────────────────────────────────────────┐
│              DI 容器 (IServiceProvider)                  │
│  持有:Singleton/Scoped/Transient 对象池               │
│  行使:实例的创建、缓存、释放                           │
└─────────────────────────────────────────────────────────┘
          │                   │                    │
          ▼                   ▼                    ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│   Controller层   │ │    Service层     │ │   Repository层   │
│  不持有业务状态   │ │  持有业务逻辑    │ │  持有 ORM上下文  │
│  仅接收请求参数   │ │  依赖Repository  │ │  (变更追踪器)    │
└──────────────────┘ └──────────────────┘ └──────────────────┘

分层架构特征

  • 状态沿单向依赖链传递(Controller → Service → Repository)。
  • Scoped 状态的生命周期通常与一个请求或会话绑定(具体由容器策略定义)。
  • 配置变更可通过选项模式(如 IOptionsMonitor 的抽象等价物)实现动态刷新,无需重启容器。

适用判断:当业务域单一,修改订单逻辑不影响库存逻辑(或系统中根本没有库存域)时,无边界分层架构足以应对。所有状态由 DI 统一管理,分层传递即可,无需引入额外的模块边界。


三、引入模块边界的临界点:并非依赖数量,而是依赖归属域

一个常见的误区是将"Service 构造函数超过 5 个"视为模块化缺失的警报。真正的病因与解法应重新审视:

症状 真正的病因 正确的解法
单个 Service 依赖超过 5 个 Repository 类职责过重(违反单一职责原则) 使用外观模式(Facade) 聚合内部逻辑,或采用中介者模式(Mediator) 将命令/查询拆解到多个 Handler 中。这与模块化无关。
同时依赖 IInventoryServiceIUserServiceIPaymentService,且它们各自独立变更 业务边界不清------一个类横跨了多个业务域 引入模块化。将订单、库存、用户拆分为物理或逻辑隔离的模块。
多个 Service 之间相互循环调用 依赖图混乱------分层架构被打破 识别聚合根(Aggregate Root),将网状调用收敛为星型调用;若无法解决,则需要纵向切分模块。

临界点的真正标志 :不在于"依赖的数量",而在于 "依赖的归属域"。当修改一行业务逻辑,被迫同时修改订单、库存、用户等多个域的核心代码时,模块边界便势在必行。

一个更直接的检测信号:如果你发现修改一个业务域的代码,导致另一个业务域的测试大面积失败,这是耦合过重的明确信号------模块化已从"可选项"变为"刚需"。此时必须用模块边界将变更隔离,确保各业务域能够独立演进。


四、大型项目(模块化架构):纵向切分与横向分层共存

核心认知 :模块化并非分层的"替代品",而是在分层之上叠加了纵向的业务边界。分层是横向切分 (技术职责:表现/业务/数据),模块化是纵向切分(业务领域:订单/库存/用户)。在大型项目中,每个模块内部依然保持着严格的分层架构。

text 复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                    启动项 (Composition Root)                           │
│  声明:所有模块的入口服务、跨模块共享状态(全局配置、日志工厂)          │
└──────┬──────────────────────┬──────────────────────┬───────────────────┘
       │                      │                      │
       ▼                      ▼                      ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│   【订单模块】        │ │   【库存模块】        │ │   【用户模块】        │
│  ──────────────────  │ │  ──────────────────  │ │  ──────────────────  │
│  公开接口(契约)     │ │  公开接口(契约)     │ │  公开接口(契约)     │
│  IOrderFacade        │ │  IInventoryFacade    │ │  IUserFacade         │
│  ──────────────────  │ │  ──────────────────  │ │  ──────────────────  │
│  【内部横向分层】     │ │  【内部横向分层】     │ │  【内部横向分层】     │
│  Controller →        │ │  Controller →        │ │  Controller →        │
│  OrderService →      │ │  StockService →      │ │  ProfileService →    │
│  OrderRepository     │ │  StockRepository     │ │  UserRepository      │
│  ──────────────────  │ │  ──────────────────  │ │  ──────────────────  │
│  私有状态(封装)     │ │  私有状态(封装)     │ │  私有状态(封装)     │
│  OrderCache          │ │  StockBuffer         │ │  SessionStore        │
│  (写入权仅限本模块)   │ │  (写入权仅限本模块)   │ │  (写入权仅限本模块)   │
└──────────┬───────────┘ └──────────┬───────────┘ └──────────┬───────────┘
           │                         │                         │
           └───────────────┬─────────┴─────────┬───────────────┘
                           │                   │
                ┌──────────▼──────────┐ ┌──────▼──────┐
                │   同步命令(强一致)  │ │ 领域事件   │
                │   调用接口 + 锁      │ │(最终一致)│
                └─────────────────────┘ └─────────────┘

模块化架构核心特征

  • 主权实体:每个模块对外仅暴露接口,内部私有状态(缓存、队列、连接池)完全隐藏,外界无法直接读写。
  • 分层内聚:模块内部的 Service 和 Repository 遵循分层原则,但被模块边界包裹,不向外泄露。
  • 交互模式:跨模块状态变更并非必须走事件,而是取决于业务需要强一致性还是最终一致性。

适用判断

  • 强依赖场景 :当存在多个业务域,且彼此依赖极强(如财务总账与明细账,需要事务强一致)时,应采用模块化(逻辑边界)。在同一进程内划分程序集,模块间通过同步接口交互,保证事务的 ACID 特性。
  • 弱依赖场景 :当存在多个业务域,但彼此依赖弱(如订单与用户积分,允许秒级延迟)时,应采用模块化(物理或逻辑边界)。模块间优先走领域事件,各自维护私有缓存,允许最终一致性。

五、跨模块交互的两种契约模式

模式 A:同步命令 + 内存锁(强一致性)

场景:秒杀扣库存。订单创建必须与库存扣减同步成功或失败。

机制 :订单模块调用库存模块的公开接口 TryReserveStock。库存模块内部使用并发集合 + 信号量(SemaphoreSlim)进行原子扣减,并返回布尔值。

分析:两个模块共享了"库存数量"这一状态,但库存模块通过公开接口暴露原子操作,外界无法直接触碰其私有字段。封装性未被破坏,同时保证了强一致性。

csharp 复制代码
// 强一致场景:同步调用 + 模块内私有锁
public class OrderFacade {
    private readonly IInventoryFacade _inventory;
    public Result PlaceOrder(OrderDto dto) {
        if (!_inventory.TryReserveStock(dto.Sku, dto.Qty)) 
            return Result.Fail("库存不足");
        _orderRepo.Save(order); // 同一事务
        return Result.Ok();
    }
}

模式 B:异步事件 + 本地副本(最终一致性)

场景:订单支付成功后,为用户增加积分或刷新报表缓存。这些操作允许秒级延迟。

机制 :订单模块发布 OrderPaidEvent,用户模块和报表模块各自订阅。收到事件后,各模块自行更新本地的私有缓存。

分析:发布事件的一方无需知道谁在监听,也无需关心监听者内部状态如何变更,实现了高度解耦。

总结 :共享内存并非禁忌,禁忌是 "跨模块直接引用对方的内脏(私有字段)"。只要通过契约化的接口(同步或异步)进行交互,封装性就得以保持。


六、模块化架构的设计基石

基石一:模块的私有状态,连"看一眼"都不允许

csharp 复制代码
// ❌ 错误:跨模块直接访问私有字段
var buffer = inventoryModule._stockBuffer;  // 编译错误

// ✅ 正确:通过公开方法交互
var result = inventoryModule.TryReserveStock(productId, quantity);

基石二:模块间禁止共享"可变状态"的同一引用

csharp 复制代码
// ❌ 错误:两个模块持有同一个 Dictionary 引用,互相污染
public class OrderModule {
    public OrderModule(ConcurrentDictionary<Guid, Order> sharedCache) { ... }
}
public class InventoryModule {
    public InventoryModule(ConcurrentDictionary<Guid, Order> sharedCache) { ... }
}

// ✅ 正确:每个模块持有自己的副本,通过事件或同步回调同步变更

基石三:跨模块交互模式由"一致性"决定,不由教条决定

  • 需要 ACID 强一致 → 走同步命令调用(内部可加锁),业务失败则整体回滚。
  • 允许 最终一致(秒级延迟可接受) → 走异步领域事件,降低耦合。

七、一张图谱看清全局

text 复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                    第1层:启动根(规则声明者)                           │
│  职责:注册所有模块入口、声明全局配置、日志工厂等                       │
│  持有:仅注册表(类型映射与生命周期策略),不持有业务实例               │
└────────────────────────────────┬────────────────────────────────────┘
                                 │ 将执行权委托给容器
                                 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                    第2层:DI 容器(生命周期执行者)                     │
│  持有:Singleton/Scoped/Transient 对象池                               │
│  职责:依据注册表创建、缓存、释放状态实例                               │
└───────────┬─────────────────────────────────┬──────────────────────────┘
            │                                 │
            ▼                                 ▼
┌───────────────────────────────┐  ┌─────────────────────────────────────┐
│   第3a层:分层架构(横向切分) │  │   第3b层:模块边界(纵向切分)      │
│   Controller → Service → Repo │  │   OrderModule │ InventoryModule    │
│   适用于单模块内部或无边界     │  │   每个模块拥有私有状态封装           │
│   单体应用                     │  │   模块内部依然保持分层               │
└───────────────┬───────────────┘  └───────────────┬─────────────────────┘
                │                                  │
                └──────────────交互契约──────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                    第4层:跨模块交互契约层                               │
│  同步契约(接口调用 + 锁)   ←── 强一致性场景                          │
│  异步契约(领域事件)        ←── 最终一致性场景                        │
└─────────────────────────────────────────────────────────────────────────┘

架构决策的终极判断标准 :架构选择与团队人数无关,只与 业务逻辑的变更耦合度 有关。判断的核心指标是 "修改一处业务意图,需要被动修改多少个其他业务域的代码"。当这个数字持续大于 0 时,模块边界就该出现在架构蓝图上了。


结语

分层与模块化,并非阶梯上的前后级,而是网格中的横纵轴。横向分层 让代码职责清晰,纵向模块让变更风险隔离。优秀的架构师,应在这个二维网格中,找到最贴合业务本质的切割线,而非盲目追随某种模式的"演进"。

相关推荐
跨境数据猎手1 小时前
多平台电商比价系统从零搭建合规
开发语言·爬虫·架构
用户1040747270231 小时前
《架构特别篇三:DEVICES 层》
架构
捷配链1 小时前
新能源四层板叠层逻辑-最优架构适配电控高低压混合场景
架构·制造·pcb
AustinXu1 小时前
进化搜索:Harness Engineering 之后,Agent 怎么自己进化自己
架构·claude·敏捷开发
柒和远方2 小时前
Worker Readiness:为什么 `/health` 不能证明后台任务能跑
前端·后端·架构
xiaoshuai10242 小时前
夜里机器替你跑全量测试,挂了还自己修:我的夜间自动巡检系统
架构
星火燎原162 小时前
Android ViewModel 接收 Intent 参数
架构
OceanBase数据库官方博客2 小时前
基于 OceanBase 湖库一体架构,构建 AI 时代的数据基础设施
人工智能·架构·oceanbase