首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >模块边界与数据流边界:模块化与反应式编程的工程化结合

模块边界与数据流边界:模块化与反应式编程的工程化结合

原创
作者头像
用户12339161
发布于 2026-10-26 09:52:57
发布于 2026-10-26 09:52:57
1230
举报

大型系统的耦合有两个来源:结构耦合——模块之间直接引用,改一处牵动全身;时序耦合——状态变化散落在回调与轮询里,数据流向不可追踪。模块化应对前者,反应式编程应对后者。二者不是可选项的叠加,而是分别守住系统的两个边界:模块化守住“谁能依赖谁”,反应式守住“变化如何传播”。缺任何一边,系统都会在规模增长后失控。

模块化:用契约隔离实现,用生命周期隔离初始化

模块化的核心不是“分文件夹”,而是依赖方向可控。抽象项目只含契约,实现模块只依赖契约,宿主只认识模块入口。以下是一个最小可用的模块契约与宿主:

代码语言:javascript
复制
public interface IModule
{
    string Name { get; }
    void Register(IServiceRegistry services);   // 只做注册,不执行
    Task InitializeAsync(IModuleContext ctx);   // 初始化与注册分离
}

public sealed class ModuleHost
{
    private readonly List<IModule> _modules = new();
    private readonly Stack<IModule> _started = new();

    public void Add(IModule m) => _modules.Add(m);

    public async Task StartAsync(IModuleContext ctx)
    {
        foreach (var m in _modules)
        {
            m.Register(ctx.Services);              // 阶段一:全部注册
        }
        foreach (var m in _modules)
        {
            await m.InitializeAsync(ctx);          // 阶段二:按序初始化
            _started.Push(m);                      // 记录,失败时逆序回滚
        }
    }
}

注册与初始化分成两个阶段是关键:所有模块的依赖先注册完毕,再统一初始化,避免“A 初始化时 B 还没注册”的时序陷阱。_started 栈保证任一模块初始化失败时,已启动的模块逆序释放,不留半初始化状态。模块之间禁止直接引用,跨模块通信只走事件总线——这就把结构耦合降到了契约层。

反应式:把状态变化建模为可组合的数据流

属性变化、命令可用性、异步结果,在传统写法里是三套机制;在反应式里是同一件事——IObservable<T>。ViewModel 继承 ReactiveObject,属性变化即数据流,命令的可执行条件直接由流派生:

代码语言:javascript
复制
public class OrderViewModel : ReactiveObject, IDisposable
{
    private readonly IEventBus _bus;
    private readonly CompositeDisposable _subs = new();
    private readonly IOrderService _service;

    [Reactive] public OrderStatus Status { get; private set; }
    [Reactive] public decimal Total { get; private set; }

    public ReactiveCommand<Unit, Unit> SubmitCommand { get; }

    public OrderViewModel(IOrderService service, IEventBus bus)
    {
        _service = service;
        _bus = bus;

        var canSubmit = this.WhenAnyValue(x => x.Status, x => x.Total)
            .Select(t => t.Item1 == OrderStatus.Draft && t.Item2 > 0);

        SubmitCommand = ReactiveCommand.CreateFromTask(SubmitAsync, canSubmit);

        // 订阅其他模块发布的领域事件,转换成本模块状态
        _bus.Listen<OrderPaid>()
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(e => Status = OrderStatus.Paid)
            .DisposeWith(_subs);
    }

    private async Task SubmitAsync()
    {
        var result = await _service.SubmitAsync();
        Status = result.Status;
        _bus.Publish(new OrderSubmitted(Status));   // 反向通知其他模块
    }

    public void Dispose() => _subs.Dispose();
}

三个要点:canSubmit 让命令可用性成为声明式派生,无需手写 CanExecute 与手动 RaiseCanExecuteChanged;ObserveOn(RxApp.MainThreadScheduler) 把后台事件切回 UI 线程,调度边界显式化;DisposeWith 把长订阅纳入生命周期管理——反应式系统最常见的生产事故是订阅泄漏,而不是逻辑错误。

协同:用事件流连接模块,用生命周期收口

模块化与反应式真正协同的地方,是模块间通信。假设新增“订单支付后自动开票”:把它做成独立的 InvoiceModule,订阅 OrderPaid 事件,内部用反应式算子做过滤与去重,再发布自己的事件:

代码语言:javascript
复制
public class InvoiceModule : IModule
{
    public string Name => "invoice";

    public void Register(IServiceRegistry s) =>
        s.AddSingleton<IInvoiceService, InvoiceService>();

    public Task InitializeAsync(IModuleContext ctx)
    {
        ctx.Bus.Listen<OrderPaid>()
            .Where(e => e.Amount > 0)
            .Distinct(e => e.OrderId)              // 幂等:同一订单只开一次票
            .Throttle(TimeSpan.FromMilliseconds(300))
            .SelectMany(e => Observable.FromAsync(
                ct => ctx.Get<IInvoiceService>().IssueAsync(e, ct)))
            .Subscribe(
                inv => ctx.Bus.Publish(new InvoiceIssued(inv.Id)),
                ex => ctx.Logger.LogError(ex, "invoice failed"))
            .DisposeWith(ctx.Lifetime);            // 随模块卸载自动释放

        return Task.CompletedTask;
    }
}

订单模块、支付模块、UI 模块一行都不用改。新增能力 = 新增模块 + 订阅已有事件流。ctx.Lifetime 把订阅绑定到模块生命周期,模块卸载时自动解除,避免“模块已停、回调还在跑”的竞态——这正是模块化与反应式结合时最容易被忽略的收口点。

结语

模块化让系统在结构上可扩展:新能力是新增模块,不是修改旧代码。反应式让系统在时序上可推理:状态变化是显式数据流,而不是散落各处的回调。两者结合时,必须补上第三件事——把订阅生命周期绑定到模块生命周期。前两者决定架构是否优雅,第三件决定它在生产环境能否长期存活。变更发生时,需要触碰的文件数,才是衡量这套组合是否成功的唯一标准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 模块化:用契约隔离实现,用生命周期隔离初始化
  • 反应式:把状态变化建模为可组合的数据流
  • 协同:用事件流连接模块,用生命周期收口
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档