框架与库的根本区别在于控制反转的方向。库被调用,框架调用你——它掌握主流程,你只在它预留的扩展点上填空。因此框架设计的核心不是“提供多少功能”,而是扩展点是否稳定、生命周期是否确定、运行时是否可诊断。以下从这三个维度给出可运行的骨架,代码用 C#,但原则适用于任何语言。
新手框架常让使用者继承一个庞大的 BaseService,后果是基类每加一个虚方法,所有实现都可能被意外影响。成熟做法是窄接口 + 组合:框架只依赖它真正调用的那几个方法,扩展者实现接口即可,不必绑定继承树。
public interface IPlugin
{
string Name { get; }
Task InitializeAsync(CancellationToken ct);
Task ExecuteAsync(PluginContext ctx, CancellationToken ct);
Task DisposeAsync();
}
public sealed record PluginContext(
IReadOnlyDictionary<string, object> State,
ILogger Logger,
CancellationToken Token);框架内部只认 IPlugin,插件作者可以自由组织代码。PluginContext 作为显式传递的依赖容器,避免插件通过静态单例偷偷耦合框架内部,这是框架可测试性的前提。
框架最容易被低估的复杂度是顺序。插件 A 依赖插件 B 的初始化结果,框架必须在 B 完成后才启动 A;停止时则要逆序释放,否则会出现“连接已关、回调还在跑”的竞态。用一个带状态检查的宿主来强制执行:
public enum HostState { Created, Starting, Running, Stopping, Stopped }
public sealed class PluginHost
{
private readonly List<IPlugin> _plugins = new();
private HostState _state = HostState.Created;
public void Register(IPlugin p)
{
if (_state != HostState.Created)
throw new InvalidOperationException("Cannot register after start");
_plugins.Add(p);
}
public async Task StartAsync(CancellationToken ct)
{
Transition(HostState.Starting);
var started = new Stack<IPlugin>();
try
{
foreach (var p in _plugins)
{
await p.InitializeAsync(ct);
started.Push(p); // 记录已启动,失败时逆序回滚
}
Transition(HostState.Running);
}
catch
{
foreach (var p in started) // 逆序释放,保证依赖关系正确
await SafeDispose(p);
Transition(HostState.Stopped);
throw;
}
}
public async Task StopAsync()
{
Transition(HostState.Stopping);
foreach (var p in _plugins.AsEnumerable().Reverse())
await SafeDispose(p);
Transition(HostState.Stopped);
}
private void Transition(HostState next)
{
// 状态机校验合法迁移,非法顺序直接抛异常而非静默容忍
_state = next;
}
}started 栈是关键:任一插件初始化失败,已启动的插件必须逆序回滚,否则框架会留下半初始化状态,比直接崩溃更难排查。Transition 看似简单,但它把“非法迁移”从隐性 bug 变成显式异常,这是框架可信度的来源。
框架承载多个插件,一个插件的异常不能拖垮整个宿主。执行入口必须做隔离,同时把耗时、结果、异常统一记录,让运行时行为可回溯:
public async Task<PluginResult> ExecuteSafelyAsync(
IPlugin plugin, PluginContext ctx)
{
var sw = Stopwatch.StartNew();
using var scope = ctx.Logger.BeginScope(new Dictionary<string, object>
{
["plugin"] = plugin.Name
});
try
{
await plugin.ExecuteAsync(ctx, ctx.Token);
ctx.Logger.LogInformation("{Plugin} ok in {Ms}ms",
plugin.Name, sw.ElapsedMilliseconds);
return PluginResult.Success(plugin.Name, sw.Elapsed);
}
catch (OperationCanceledException) when (ctx.Token.IsCancellationRequested)
{
return PluginResult.Cancelled(plugin.Name); // 取消不算故障
}
catch (Exception ex)
{
ctx.Logger.LogError(ex, "{Plugin} failed after {Ms}ms",
plugin.Name, sw.ElapsedMilliseconds);
return PluginResult.Failed(plugin.Name, ex); // 隔离,不向上抛
}
}三个细节决定框架的工程质量:取消与故障分离——OperationCanceledException 在 token 已取消时应视为正常终止,而非错误;异常在此终止——插件失败返回结果对象而不是向上抛,宿主才能继续调度其余插件;日志携带结构化字段——plugin 与耗时进入 scope,日志聚合才能按插件维度定位问题。可观测性不是框架的附加功能,而是执行入口的内建职责。
框架的长期价值不取决于功能列表,而取决于变更时的摩擦系数。窄接口让扩展者不被基类演进绑架;状态机化的生命周期让启停顺序成为可验证事实;隔离执行与结构化日志让运行时故障可定位、可归因。三者叠加,框架才从“能跑起来的脚手架”变成“可以交给别人在上面盖楼的底座”。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。