第 3 章 · 03 Composer 反射装配与 MEF 自注册


文档摘要

第 3 章 · 03 Composer 反射装配与 MEF 自注册 本节摘要:前两节讲了 Handler 接口与双实现,本节收口——这些实现是怎么从 config.json 里的字符串变成内存对象的?答案是 。它基于 .NET 的 MEF(Managed Extensibility Framework),做两件事:① 启动时扫描所有 ,把带 的接口的实现类登记进 ;② 提供 方法,通过反射+类型名匹配把字符串实例化成接口对象。两套 handler 容器( 系统级 + 算法级)都靠 Composer 从 config 装配。这套机制是"依赖反转原则"在 Lean 的落地——引擎主体依赖接口,Composer 负责把字符串映射成实现,引擎对具体实现类一无所知。

第 3 章 · 03 Composer 反射装配与 MEF 自注册

本节摘要:前两节讲了 Handler 接口与双实现,本节收口——这些实现是怎么从 config.json 里的字符串变成内存对象的?答案是 Composer。它基于 .NET 的 MEF(Managed Extensibility Framework),做两件事:① 启动时扫描所有 QuantConnect.*.dll,把带 [InheritedExport] 的接口的实现类登记进 CompositionContainer;② 提供 GetExportedValueByTypeName<T>(typeName) 方法,通过反射+类型名匹配把字符串实例化成接口对象。两套 handler 容器(LeanEngineSystemHandlers 系统级 + LeanEngineAlgorithmHandlers 算法级)都靠 Composer 从 config 装配。这套机制是"依赖反转原则"在 Lean 的落地——引擎主体依赖接口,Composer 负责把字符串映射成实现,引擎对具体实现类一无所知。本节还总结了 Lean 用到的五种设计模式。

内容来源:原项目源码 Common/Util/Composer.csEngine/LeanEngineSystemHandlers.csEngine/LeanEngineAlgorithmHandlers.csEngine/Initializer.cs,精读并套用体系化模板。

⚠️ 注意:本节假设你已读完前两节。[InheritedExport] 的"接口级自注册"语义在第 01 节讲过,五大 Handler 接口的 [InheritedExport(typeof(IXxx))] 标记是本节的前提。本节重点在 Composer 的两阶段工作流:启动扫描 + 按名实例化,以及它如何支撑"改 config 字符串就换实现"的插件化。

学习目标

阅读完本节,你应当能够:

  1. 说清 [InheritedExport(typeof(IXxx))] 写在接口上的"继承式导出"语义。
  2. 读懂 Composer 构造函数的 DLL 目录定位逻辑。
  3. 逐行读懂 GetExportedValueByTypeName<T>(typeName) 的三级匹配流程。
  4. 区分 LeanEngineSystemHandlersLeanEngineAlgorithmHandlers 两套容器各装什么。
  5. 说清 Initializer.GetSystemHandlers/GetAlgorithmHandlers 的装配入口。
  6. 总结 Lean 用到的五种设计模式及各自落点。

一、MEF 自注册:[InheritedExport] 的语义

第 01 节我们看到了所有 Handler 接口都带这个特性:

// Engine/DataFeeds/IDataFeed.cs:28 [InheritedExport(typeof(IDataFeed))] public interface IDataFeed
// Common/Interfaces/IHistoryProvider.cs:27 [InheritedExport(typeof(IHistoryProvider))] public interface IHistoryProvider : IDataProviderEvents

[InheritedExport(typeof(IDataFeed))] 是 MEF 的"继承式导出"特性,语义是:任何实现 IDataFeed 的非抽象类,都会被自动导出为 IDataFeed 契约的实现。注意它写在接口上,不是实现类上。

对比传统写法——每个实现类自己声明:

// 传统写法(Lean 没用) [Export(typeof(IDataFeed))] public class FileSystemDataFeed : IDataFeed { ... }

Lean 的 [InheritedExport] 写在接口上的好处:实现类不用加任何特性。新增一个 public class MyCustomDataFeed : IDataFeed,编译进 DLL 放到程序目录,Composer 启动扫描时自动发现它,不用改注册表。这就是"自注册"——实现类通过继承接口隐式注册,而非显式标注。

💡 钻取要点:[InheritedExport] 让"新增 handler"的成本降到最低——写个类继承接口就行。这也是 Lean 生态能容纳几十种券商(IB/FXCM/Oanda/Alpaca/Binance/...)、几十种数据源的关键——每个券商只要写一个类实现 IBrokerage + IBrokerageFactory,扔进 DLL,用户改 config 就能用。

二、Composer 构造:DLL 目录定位与安全加载

Composer 是单例(Common/Util/Composer.cs:38-410),通过 Composer.Instance 访问。构造函数(L60-97)做两件事:定位 DLL 目录 + 安全加载所有程序集。

DLL 目录定位(L62-82)

62 var dllDirectoryString = Config.Get("composer-dll-directory"); 63 if (string.IsNullOrWhiteSpace(dllDirectoryString)) 64 { 65 // Check our appdomain directory for QC Dll's 66 if (!string.IsNullOrEmpty(AppDomain.CurrentDomain.BaseDirectory) 67 && Directory.EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory, "QuantConnect.*.dll").Any()) 68 { 69 dllDirectoryString = AppDomain.CurrentDomain.BaseDirectory; 70 } 71 else 72 { 73 var currentDirectory = Directory.GetCurrentDirectory(); 74 var parentDirectory = Directory.GetParent(currentDirectory)?.FullName ?? currentDirectory; 76 dllDirectoryString = Directory.EnumerateFiles(parentDirectory, "QuantConnect.*.dll").Any() 77 ? parentDirectory : currentDirectory; 78 } 79 }

三级回退:① config 显式指定 composer-dll-directory → ② AppDomain.BaseDirectory(标准 .NET 程序目录,Launcher 启动时通常命中)→ ③ 父目录或当前工作目录(给 Jupyter 研究环境用的兜底)。判断依据都是"目录里有没有 QuantConnect.*.dll"。

安全加载 LoadPartsSafely(L349-409)

355 Parallel.ForEach(files, file => 356 { 358 try 359 { 361 Assembly assembly; 362 try { assembly = Assembly.Load(AssemblyName.GetAssemblyName(file)); } 363 catch { assembly = Assembly.LoadFrom(file); } 364 373 if (Path.GetFileName(file).StartsWith($"{nameof(QuantConnect)}.")) 374 { 375 foreach (var type in assembly.ExportedTypes 376 .Where(t => !t.IsAbstract && !t.IsInterface && !t.IsEnum)) 377 { 378 exportedTypes.Add(type); 379 } 380 } 389 catch (Exception ex) { /* 跳过坏 DLL,不致命 */ } 396 _exportedTypes = new List<Type>(exportedTypes); 398 _compositionContainer = new CompositionContainer(aggregate);

四个要点:

  1. 并行加载:Parallel.ForEach 并行处理所有 DLL,快。
  2. 两级 Load:Assembly.Load(标准方式,走 probing path)失败则退到 Assembly.LoadFrom(直接路径)。后者处理"依赖不在 probing path 但 DLL 已加载"的情况。
  3. 只收 QuantConnect.*.dll 的导出类型(L373):第三方库(如 Newtonsoft.Json)的类型不进 exportedTypes,避免污染。
  4. 容错:单个 DLL 加载失败不影响整体(L388-393 只记日志不抛)。

💡 钻取要点:Composer 同时维护两份数据——_exportedTypes(纯 Type 列表,供反射查询)和 _compositionContainer(MEF 容器,供标准导出查询)。GetExportedValueByTypeName 优先用 _exportedTypes(L220),fallback 到 _composableParts(L241)。两份数据服务于同一个目标——按类型名找实现。

三、GetExportedValueByTypeName:三级匹配的核心方法

这是 Composer 最关键的方法(Common/Util/Composer.cs:207-306),把 config 里的类名字符串变成接口实例。核心是三级匹配 + 反射实例化 + 缓存

207 public T GetExportedValueByTypeName<T>(string typeName, bool forceTypeNameOnExisting = true) 208 where T : class 209 { 213 // 第一级:缓存命中 214 var instance = GetParts<T>().FirstOrDefault(x => !forceTypeNameOnExisting || x.GetType().MatchesTypeName(typeName)); 215 if (instance != null) { return instance; } 219 var type = typeof(T); 220 // 第二级:从 _exportedTypes 反射查 221 var typeT = _exportedTypes.Where(type1 => 223 type.IsAssignableFrom(type1) && type1.MatchesTypeName(typeName)) 230 .FirstOrDefault(); 233 if (typeT != null) 234 { 235 instance = (T)Activator.CreateInstance(typeT); // 反射 new 236 } 238 if (instance == null) 239 { 240 // 第三级:从 MEF _composableParts 查 241 var selectedPart = _composableParts.Where(...).FirstOrDefault(); 257 if (selectedPart == null) 258 { 259 throw new ArgumentException($"Unable to locate any exports matching the requested typeName: {typeName}..."); 260 } 263 instance = (T)selectedPart.CreatePart().GetExportedValue(exportDefinition); 266 } 269 // 把新实例缓存到各接口的 List 270 var exportedParts = instance.GetType().GetInterfaces() 271 .Where(i => i.GetCustomAttribute<InheritedExportAttribute>() != null); 272 lock (_exportedValuesLockObject) 273 { 274 foreach (var export in exportedParts) { ...缓存... } 291 } 292 }

三级匹配流程:

级别 数据源 方式 适用场景
第一级 _exportedValues 缓存 已实例化的对象按类型名匹配 同一类型多次请求(如多次 GetExportedValueByTypeName<IResultHandler>)
第二级 _exportedTypes 反射 Activator.CreateInstance new 一个 标准情况,实现类在 QuantConnect.*.dll 里
第三级 MEF _composableParts CreatePart().GetExportedValue 兜底,处理 MEF 特殊导出

MatchesTypeName(typeName) 是扩展方法,依次尝试 AssemblyQualifiedNameFullNameName 三种匹配。所以 config 里写全限定名(QuantConnect.Lean.Engine.DataFeeds.FileSystemDataFeed)或短名(FileSystemDataFeed)都能命中。

💡 钻取要点:forceTypeNameOnExisting 参数(L204)默认 true。当 false 时,如果接口已有一个实例,就直接返回它(不校验类型名)。这对全局单例有用——如 IDataAggregator,不管 config 写什么类型名,只要已有一个实例就用它。Engine.cs:228Composer.Instance.AddPart(historyProvider) 正是配合这个——手动塞一个实例进缓存,后续请求优先用它。

四、两套 Handler 容器

Lean 把 Handler 分成两组:LeanEngineSystemHandlers(系统级)与 LeanEngineAlgorithmHandlers(算法级)。

LeanEngineSystemHandlers(系统级,跨算法复用)

Engine/LeanEngineSystemHandlers.cs:30-139,装四个系统级 handler:

字段 接口 职责
Api IApi 与云端 API 通信(状态/限流)
Notify IMessagingHandler 消息推送(结果包/错误)
JobQueue IJobQueueHandler 任务队列(取 job/ack)
LeanManager ILeanManager 宿主环境增强

装配方法 FromConfiguration(L107-114):

107 public static LeanEngineSystemHandlers FromConfiguration(Composer composer) 108 { 109 return new LeanEngineSystemHandlers( 110 composer.GetExportedValueByTypeName<IJobQueueHandler>(Config.Get("job-queue-handler", "QuantConnect.Queues.JobQueue")), 111 composer.GetExportedValueByTypeName<IApi>(Config.Get("api-handler", "QuantConnect.Api.Api")), 112 composer.GetExportedValueByTypeName<IMessagingHandler>(Config.Get("messaging-handler", "QuantConnect.Messaging.Messaging")), 113 composer.GetExportedValueByTypeName<ILeanManager>(Config.Get("lean-manager-type", "LocalLeanManager"))); 114 }

四行 Config.Get(key, default) 读 config,缺省值是本地实现(JobQueue/Api/Messaging/LocalLeanManager)。云端运行时 config 会覆盖成云端实现。

LeanEngineAlgorithmHandlers(算法级,每个 job 一套)

Engine/LeanEngineAlgorithmHandlers.cs:37-298,装五个核心 Handler + 数据辅助 provider:

字段 接口 职责
Results IResultHandler 结果输出
Setup ISetupHandler 装配算法+券商
DataFeed IDataFeed 数据流
Transactions ITransactionHandler 订单处理
RealTime IRealTimeHandler 定时事件
MapFileProvider IMapFileProvider 标的符号映射(代码变更)
FactorFileProvider IFactorFileProvider 复权因子
DataProvider IDataProvider 数据文件获取
ObjectStore IObjectStore 持久化存储
DataPermissionsManager IDataPermissionManager 数据权限
DataMonitor IDataMonitor 数据缺失监控

装配方法 FromConfiguration(L235-276)核心段:

238 var setupHandlerTypeName = Config.Get("setup-handler", "ConsoleSetupHandler"); 239 var transactionHandlerTypeName = Config.Get("transaction-handler", "BacktestingTransactionHandler"); 240 var realTimeHandlerTypeName = Config.Get("real-time-handler", "BacktestingRealTimeHandler"); 241 var dataFeedHandlerTypeName = Config.Get("data-feed-handler", "FileSystemDataFeed"); 242 var resultHandlerTypeName = Config.Get("result-handler", "BacktestingResultHandler"); 247 var result = new LeanEngineAlgorithmHandlers( 248 composer.GetExportedValueByTypeName<IResultHandler>(resultHandlerTypeName), 249 composer.GetExportedValueByTypeName<ISetupHandler>(setupHandlerTypeName), 250 composer.GetExportedValueByTypeName<IDataFeed>(dataFeedHandlerTypeName), 251 composer.GetExportedValueByTypeName<ITransactionHandler>(transactionHandlerTypeName), 252 composer.GetExportedValueByTypeName<IRealTimeHandler>(realTimeHandlerTypeName), 253 mapFileProvider, factorFileProvider, dataProvider, 254 composer.GetExportedValueByTypeName<IObjectStore>(objectStoreTypeName), 255 composer.GetExportedValueByTypeName<IDataPermissionManager>(dataPermissionManager), 258 Globals.LiveMode, researchMode, 260 composer.GetExportedValueByTypeName<IDataMonitor>(dataMonitor));

注意 L238-242 的缺省值——全是 Backtesting*/FileSystem*。这是"缺省即回测"的设计:config 不写任何 environment,跑的就是回测。

💡 钻取要点:注意 L258 传了 Globals.LiveMode——它决定 ZipDataCacheProvider 是否临时缓存(L187 isDataEphemeral: liveMode)、是否挂 DataMonitor 钩子(L190-194 只在非 live 非 research 时挂)。这个布尔从哪来?config.json 顶层或 environment 块里的 live-mode 字段,被 Globals 静态读取。所以模式开关的源头就是 config 的 live-mode + environment 两个字段。

五、Initializer:装配入口

Engine/Initializer.cs 是装配流程的入口,Program.cs 调用它:

63 public static LeanEngineSystemHandlers GetSystemHandlers() 64 { 65 var systemHandlers = LeanEngineSystemHandlers.FromConfiguration(Composer.Instance); 68 systemHandlers.Initialize(); // Notify/JobQueue 初始化 70 return systemHandlers; 71 } 76 public static LeanEngineAlgorithmHandlers GetAlgorithmHandlers(bool researchMode = false) 77 { 78 return LeanEngineAlgorithmHandlers.FromConfiguration(Composer.Instance, researchMode); 79 }

两个方法都是"从 Composer + config 装配 → 返回容器"。Program.cs 的典型流程:

Initializer.Start() // 日志/全局 var system = Initializer.GetSystemHandlers() // 系统级 handler var algorithm = Initializer.GetAlgorithmHandlers() // 算法级 handler var engine = new Engine(system, algorithm, liveMode) // new 引擎 engine.Run(job, manager, assemblyPath, workerThread) // 跑

整条链路里,Engine 拿到的是接口容器,不知道具体实现类。Engine.cs:311-319JOB HANDLERS 日志会在运行时打印实际装配的类型,用户能从日志看到"我这次跑用的是 FileSystemDataFeed + BacktestingTransactionHandler"。

六、依赖反转原则与设计模式总结

依赖反转原则(DIP)

传统分层架构是"高层依赖低层"——引擎直接 new 数据源、new 券商。Lean 反过来:高层(引擎)定义接口,低层(实现)依赖接口。配合 Composer,依赖关系变成:

引擎主体 ──依赖──> IDataFeed 接口 <──实现── FileSystemDataFeed <──实现── LiveTradingDataFeed <──实现── (你的自定义数据源)

引擎主体不知道、也不需要知道运行时是哪个实现。Composer 是"依赖注入容器",负责把字符串映射成实例。这就是 DIP 在 Lean 的落地。

五种设计模式

模式 落点 作用
依赖注入(字符串+反射) Composer.GetExportedValueByTypeName config 字符串 → 接口实例,解耦配置与代码
MEF 自注册([InheritedExport]) 各 Handler 接口 新增实现零登记,自动发现
策略模式 五大 Handler 接口 每个接口可替换实现(回测/实盘/自定义)
门面模式(Facade) Engine 编排所有 Handler,对外暴露单一 Run 入口
模板方法 AlgorithmManager.Run 固定主循环步骤(收数据→调 OnData→处理订单),子步骤可覆写

💡 钻取要点(收束):这五种模式协同工作——MEF 自注册让实现类零成本接入,Composer 用反射把字符串变实例,策略模式让每接口可换,Engine 作门面编排,AlgorithmManager 作模板方法固定流程。合起来就是"回测实盘统一"的工程全貌:改 config 一个字符串,整套 handler 套件切换,引擎主体与用户代码零修改。这是 Lean 作为"C# 量化引擎典范"最值得学习的设计。

本节要点回顾

  1. [InheritedExport(typeof(IXxx))]:写在接口上,实现类自动被 MEF 登记为该契约的导出,新增实现零成本。
  2. Composer 构造:三级回退定位 DLL 目录 → Parallel.ForEach 安全加载所有 QuantConnect.*.dll → 维护 _exportedTypes_compositionContainer 两份数据。
  3. GetExportedValueByTypeName<T>(typeName):三级匹配(缓存 → 反射 _exportedTypes → MEF _composableParts),MatchesTypeName 支持 AssemblyQualifiedName/FullName/短名。
  4. 两套容器:SystemHandlers(Api/Notify/JobQueue/LeanManager)系统级复用;AlgorithmHandlers(五大核心 + Map/Factor/Data Provider/ObjectStore/DataPermission/DataMonitor)每 job 一套。
  5. 缺省即回测:AlgorithmHandlers 的 config 缺省值全是 Backtesting*/FileSystem*,不写 environment 就跑回测。
  6. Initializer 入口:GetSystemHandlers/GetAlgorithmHandlers 各调一次 FromConfiguration(Composer.Instance)
  7. 依赖反转:引擎依赖接口不依赖实现,Composer 是依赖注入容器。
  8. 五种模式:依赖注入(字符串+反射)/MEF 自注册/策略模式/门面 Engine/模板方法 AlgorithmManager。

第 3 章结束。你已经理解了"回测实盘统一"的完整设计——七大接口、双实现、三个分叉点、Composer 装配。下一章(第 4 章)钻进用户 API 层 QCAlgorithm:它是被 SetupHandler 反射加载的用户代码基类,封装了所有 Handler 能力的友好 API。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U