软件开发和 MQL5 中的设计模式(第 2 部分):结构模式(基础篇)
MQL5 里的结构型设计模式到底解决什么
在 MT5 的 EA 与指标开发中,结构型设计模式关注的是「对象之间怎么搭」,而不是单个类怎么写。它把已有模块按某种固定关系组装,让行情处理、信号生成和订单执行之间的耦合降下来。 官方社区在 2024 年 7 月 3 日发布的该系列第 2 部分,单篇浏览量约 1056 次,说明这类偏底层的工程化内容在量化交易者里需求稳定但不算大众。 对做实盘自动化的人而言,直接价值是:当你把策略逻辑拆成可替换的「接收行情—计算信号—下发指令」三层结构后,换一个经纪商接口或加一个过滤条件,不必重写整段 EA。外汇与贵金属杠杆高,任何结构改动都建议先在策略测试器用历史数据跑通再上模拟盘。
◍ 结构型模式在 MQL5 里的位置
设计模式分创建型、结构型、行为型几大类。上一篇聊了创建型,这一节把重心放到结构型:它解决的是如何用已有的类拼出更大的运行结构,而不是从零造对象。 对写 MT5 指标或 EA 的人来说,结构型模式的价值在于把行情获取、信号计算、订单管理这些零散类黏合成一个可维护的系统。本文会逐一拆解适配器、桥接、组合、修饰、外观、享元、代理这 7 种结构型模式,并给出在 MQL5 中的落地方式。 若你此前没接触过 MQL5 的 OOP,建议先补一下面向对象基础再回来读,否则类之间怎么委托、怎么继承会看得很吃力。外汇与贵金属交易本身高风险,用这类框架写出的程序也只是工具,不替代仓位与风控判断。
「把类与对象拼成更大结构的几种办法」
结构模式解决的是「已有类怎么组合成更大组件」的问题,核心机制是继承:子类持有并复用父类的属性与实现,这样多个开发完的类可以各自独立、又能协同工作。 在 MQL5 里常见的结构模式有七种:适配器把类接口转换成客户期望的另一套接口;桥接把抽象和实现解耦,两边可独立演变;组合用树形结构表达部分—整体,且对单个对象和组合一视同仁;修饰在不改类的前提下动态加功能,替代继承式扩展;外观给子系统包一层高级接口;享元靠共享支撑大量细粒度对象;代理用占位符控制对原对象的访问。 落到实做,判断该用哪种只需要三个问题:这个模式是干什么的、它解决什么具体麻烦、在 MQL5 里怎么写。带着这三个问题去翻 MT5 的 Standard Library 源码,你能直接看到 CObject 派生体系里组合与修饰的实际用法。
给老接口套一层壳:MT5 里的适配器写法
适配器模式的核心就一个词——适应性。你手上有一段旧类的接口,市场逻辑一变,调用方期望的接口对不上了,直接改旧代码风险高,就用一个中间层把旧接口转成新接口,让两边能协同跑起来。在 MQL5 里它常被叫作 Wrapper,本质就是给不兼容的接口包一层。 这套结构里有四个角色:Target 是客户期待的新接口域,Client 只认 Target 干活,Adaptee 是那个需要被改造的旧接口,Adapter 负责把 Adaptee 桥接成 Target。MQL5 支持多重继承,所以能分出类适配器(继承实现)和对象适配器(组合实现)两条路。 下面这段代码是类适配器的完整骨架。Adaptee::SpecificRequest 打印 "A specific request is executing by the Adaptee",证明旧逻辑确实被调到了;AdapterAsTarget 继承 Target,内部挂一个 Adapter* 回头引向被适配者;Adapter 继承 Adaptee 并 new 出 asTarget,形成双向引用。Client::Run 里只碰 Target*,完全不感知 Adaptee 的存在。 [CODE] 段里值得注意的是 Adapter 构造时把 asTarget.asAdaptee = &this,析构里 delete asTarget,这种自引用在 EA 退出时要防二次释放。你在 MT5 里贴这段代码,Run 一次会在日志看到两行输出:先 Adapter requested Operation,再 Adaptee 的具体请求,说明桥接生效。外汇与贵金属杠杆高,这类结构重构只解决代码耦合,不暗示任何行情结论。
<span class="keyword">class="kw">namespace</span> AdapterClass <span class="keyword">interface</span> Target { <span class="keyword">class="type">void</span> Request(); }; <span class="keyword">class</span> Adaptee { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">void</span> SpecificRequest(); }; <span class="keyword">class="type">void</span> Adaptee::SpecificRequest(<span class="keyword">class="type">void</span>) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"A specific request is executing by the Adaptee"</span>); } <span class="keyword">class</span> Adapter; <span class="keyword">class</span> AdapterAsTarget:<span class="keyword">class="kw">public</span> Target { <span class="keyword">class="kw">public</span>: Adapter* asAdaptee; <span class="keyword">class="type">void</span> Request(); }; <span class="keyword">class="type">void</span> AdapterAsTarget::Request() { <span class="functions">printf</span>(<span class="class="type">class="kw">string">"The Adapter requested Operation"</span>); asAdaptee.SpecificRequest(); } <span class="keyword">class</span> Adapter:<span class="keyword">class="kw">public</span> Adaptee { <span class="keyword">class="kw">public</span>: AdapterAsTarget* asTarget; Adapter(); ~Adapter(); }; <span class="keyword">class="type">void</span> Adapter::Adapter(<span class="keyword">class="type">void</span>) { asTarget=<span class="keyword">new</span> AdapterAsTarget; asTarget.asAdaptee=&<span class="keyword">this</span>; } <span class="keyword">class="type">void</span> Adapter::~Adapter(<span class="keyword">class="type">void</span>) { <span class="keyword">class="kw">delete</span> asTarget; } <span class="keyword">class</span> Client { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">string</span> Output(); <span class="keyword">class="type">void</span> Run(); }; <span class="keyword">class="type">class="kw">string</span> Client::Output() { <span class="keyword">class="kw">return</span> <span class="keyword">__FUNCTION__</span>; } <span class="keyword">class="type">void</span> Client::Run() { Adapter adapter; Target* target=adapter.asTarget; target.Request(); } <span class="keyword">class="kw">namespace</span> AdapterClass { <span class="keyword">interface</span> Target { <span class="keyword">class="type">void</span> Request(); }; <span class="keyword">class</span> Adaptee { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">void</span> SpecificRequest(); }; <span class="keyword">class="type">void</span> Adaptee::SpecificRequest(<span class="keyword">class="type">void</span>) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"A specific request is executing by the Adaptee"</span>); } <span class="keyword">class</span> Adapter; <span class="keyword">class</span> AdapterAsTarget:<span class="keyword">class="kw">public</span> Target { <span class="keyword">class="kw">public</span>: Adapter* asAdaptee; <span class="keyword">class="type">void</span> Request(); }; <span class="keyword">class="type">void</span> AdapterAsTarget::Request() { <span class="functions">printf</span>(<span class="class="type">class="kw">string">"The Adapter requested Operation"</span>); asAdaptee.SpecificRequest(); } <span class="keyword">class</span> Adapter:<span class="keyword">class="kw">public</span> Adaptee { <span class="keyword">class="kw">public</span>: AdapterAsTarget* asTarget; Adapter(); ~Adapter(); }; <span class="keyword">class="type">void</span> Adapter::Adapter(<span class="keyword">class="type">void</span>) {
◍ 对象适配器把旧接口塞进新调用链
MQL5 里写 EA 或指标时,常遇到已有类只暴露 SpecificRequest(),但调用方约定走 Target 接口的 Request()。对象适配器做法是在 Adapter 内持有一个 Adaptee 实例,而非继承它,这样运行时耦合更软。 下面这段是对象适配器的核心骨架:Adapter 继承 Target,成员里放一个 Adaptee 对象;Request() 先打印调度信息,再转调 adaptee.SpecificRequest()。日志会先后输出 "The request of Operation requested" 和 "The specific Request" 两行,验证转发是否生效。 Client::Run() 里用 new Adapter 拿到 Target 指针,调一次 Request() 后立刻 delete,避免 MT5 策略测试器里堆出内存泄漏。实盘外接行情 DLL 时,这种写法能让你不改原 Adaptee 就接入统一接口,但外汇与贵金属杠杆高,接入第三方模块前先在策略测试器跑 200 根以上历史 K 线确认无异常。
class="kw">namespace AdapterObject { interface Target { class="type">void Request(); }; class Adaptee { class="kw">public: class="type">void SpecificRequest(); }; class="type">void Adaptee::SpecificRequest(class="type">void) { Print("The specific Request"); } class Adapter:class="kw">public Target { class="kw">public: class="type">void Request(); class="kw">protected: Adaptee adaptee; }; class="type">void Adapter::Request(class="type">void) { Print("The request of Operation requested"); adaptee.SpecificRequest(); } class Client { class="kw">public: class="type">class="kw">string Output(); class="type">void Run(); }; class="type">class="kw">string Client::Output() { class="kw">return __FUNCTION__; } class="type">void Client::Run() { Target* target=new Adapter; target.Request(); class="kw">delete target; } }
「在 MT5 里把抽象和实现拆成两半」
写 EA 或指标时,常遇到一种尴尬:一个交易逻辑抽象(比如「信号源」)背后有好几种实现(均线穿越、RSI 背离、订单流),用继承硬绑在一起,哪天改其中一种实现就容易把上层搞崩。桥接模式的核心就是把这个绑定切断——抽象只持有一个实现接口的引用,具体怎么算信号由另一边独立扩展。 MQL5 没有多继承,但 interface 关键字足够搭出这套结构。下面这段代码把 Implementor 定为信号实现接口,Abstraction 只管调度,RefinedAbstraction 做具体抽象,ConcreteImplementorA/B 是两套实现。Client 里先挂 A 跑一次 Operation,再挂 B 跑一次,终端会先后打印「The implementor A」「The implementor B」——这就是解耦后的直观证据。 [CODE] 区块里的 Abstraction 析构直接 delete implementor,意味着实现对象生命周期归抽象层接管;你在 MT5 里复用时若想共享实现实例,得把这块改成引用计数或外部管理,否则多抽象共用一个实现会双重释放崩掉。外汇与贵金属杠杆高,这类底层结构错误可能让实盘 EA 在切换品种时直接异常,先在策略测试器里空跑验证再上真仓。
class="kw">namespace Bridge { interface Implementor { class="type">void OperationImp(); }; class Abstraction { class="kw">public: class="kw">virtual class="type">void Operation(); Abstraction(Implementor*); Abstraction(); ~Abstraction(); class="kw">protected: Implementor* implementor; }; class="type">void Abstraction::Abstraction(class="type">void) {} class="type">void Abstraction::Abstraction(Implementor*i):implementor(i) {} class="type">void Abstraction::~Abstraction() { class="kw">delete implementor; } class="type">void Abstraction::Operation() { implementor.OperationImp(); } class RefinedAbstraction:class="kw">public Abstraction { class="kw">public: RefinedAbstraction(Implementor*); class="type">void Operation(); }; class="type">void RefinedAbstraction::RefinedAbstraction(Implementor*i):Abstraction(i) {} class="type">void RefinedAbstraction::Operation() { Abstraction::Operation(); } class ConcreteImplementorA:class="kw">public Implementor { class="kw">public: class="type">void OperationImp(); }; class="type">void ConcreteImplementorA::OperationImp(class="type">void) { Print("The implementor A"); } class ConcreteImplementorB:class="kw">public Implementor { class="kw">public: class="type">void OperationImp(); }; class="type">void ConcreteImplementorB::OperationImp(class="type">void) { Print("The implementor B"); } class Client { class="kw">public: class="type">class="kw">string Output(); class="type">void Run(); }; class="type">class="kw">string Client::Output(class="type">void) { class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { Abstraction* abstraction; abstraction=new RefinedAbstraction(new ConcreteImplementorA); abstraction.Operation(); class="kw">delete abstraction; abstraction=new RefinedAbstraction(new ConcreteImplementorB); abstraction.Operation(); class="kw">delete abstraction; } }