
大型系统的耦合有两个来源:结构耦合——模块之间直接引用,改一处牵动全身;时序耦合——状态变化散落在回调与轮询里,数据流向不可追踪。模块化应对前者,反应式编程应对后者。二者不是可选项的叠加,而是分别守住系统的两个边界:模块化守住“谁能依赖谁”,反应式守住“变化如何传播”。缺任何一边,系统都会在规模增长后失控。
模块化的核心不是“分文件夹”,而是依赖方向可控。抽象项目只含契约,实现模块只依赖契约,宿主只认识模块入口。以下是一个最小可用的模块契约与宿主:
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,属性变化即数据流,命令的可执行条件直接由流派生:
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 事件,内部用反应式算子做过滤与去重,再发布自己的事件:
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 删除。