软件开发和 MQL5 中的设计模式(第 2 部分):结构模式(基础篇)
📘

软件开发和 MQL5 中的设计模式(第 2 部分):结构模式(基础篇)

第 1/3 篇

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 的具体请求,说明桥接生效。外汇与贵金属杠杆高,这类结构重构只解决代码耦合,不暗示任何行情结论。

MQL5 / C++
<span class="keyword">class="kw">namespace</span> AdapterClass
<span class="keyword">interface</span> Target
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">void</span> Request();
&nbsp;&nbsp;};
<span class="keyword">class</span> Adaptee
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SpecificRequest();
&nbsp;&nbsp;};
<span class="keyword">class="type">void</span> Adaptee::SpecificRequest(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"A specific request is executing by the Adaptee"</span>);
&nbsp;&nbsp;}
<span class="keyword">class</span> Adapter;
<span class="keyword">class</span> AdapterAsTarget:<span class="keyword">class="kw">public</span> Target
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; Adapter*&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;asAdaptee;
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Request();
&nbsp;&nbsp;};
<span class="keyword">class="type">void</span> AdapterAsTarget::Request()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">printf</span>(<span class="class="type">class="kw">string">"The Adapter requested Operation"</span>);
&nbsp;&nbsp; asAdaptee.SpecificRequest();
&nbsp;&nbsp;}
<span class="keyword">class</span> Adapter:<span class="keyword">class="kw">public</span> Adaptee
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; AdapterAsTarget*&nbsp;&nbsp;asTarget;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Adapter();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~Adapter();
&nbsp;&nbsp;};
<span class="keyword">class="type">void</span> Adapter::Adapter(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; asTarget=<span class="keyword">new</span> AdapterAsTarget;
&nbsp;&nbsp; asTarget.asAdaptee=&amp;<span class="keyword">this</span>;
&nbsp;&nbsp;}
<span class="keyword">class="type">void</span> Adapter::~Adapter(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="kw">delete</span> asTarget;
&nbsp;&nbsp;}
<span class="keyword">class</span> Client
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Output();
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Run();
&nbsp;&nbsp;};
<span class="keyword">class="type">class="kw">string</span> Client::Output()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="kw">return</span> <span class="keyword">__FUNCTION__</span>;
&nbsp;&nbsp;}
<span class="keyword">class="type">void</span> Client::Run()
&nbsp;&nbsp;{
&nbsp;&nbsp; Adapter adapter;
&nbsp;&nbsp; Target* target=adapter.asTarget;
&nbsp;&nbsp; target.Request();
&nbsp;&nbsp;}
<span class="keyword">class="kw">namespace</span> AdapterClass
{
<span class="keyword">interface</span> Target
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">void</span> Request();
&nbsp;&nbsp;};
<span class="keyword">class</span> Adaptee
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SpecificRequest();
&nbsp;&nbsp;};
<span class="keyword">class="type">void</span> Adaptee::SpecificRequest(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"A specific request is executing by the Adaptee"</span>);
&nbsp;&nbsp;}
<span class="keyword">class</span> Adapter;
<span class="keyword">class</span> AdapterAsTarget:<span class="keyword">class="kw">public</span> Target
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; Adapter*&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;asAdaptee;
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Request();
&nbsp;&nbsp;};
<span class="keyword">class="type">void</span> AdapterAsTarget::Request()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="functions">printf</span>(<span class="class="type">class="kw">string">"The Adapter requested Operation"</span>);
&nbsp;&nbsp; asAdaptee.SpecificRequest();
&nbsp;&nbsp;}
<span class="keyword">class</span> Adapter:<span class="keyword">class="kw">public</span> Adaptee
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; AdapterAsTarget*&nbsp;&nbsp;asTarget;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Adapter();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~Adapter();
&nbsp;&nbsp;};
<span class="keyword">class="type">void</span> Adapter::Adapter(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{

◍ 对象适配器把旧接口塞进新调用链

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 线确认无异常。

MQL5 / C++
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 在切换品种时直接异常,先在策略测试器里空跑验证再上真仓。

MQL5 / C++
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;
  }
}

常见问题

用结构型模式把类和对象组合成更大结构,比如用组合代替继承,把订单管理、信号判断拆成独立类再拼起来。
写个适配器套在旧接口外,新调用链只认新接口,旧逻辑原样跑,不用动老代码。
小布可以读你的策略逻辑,标出哪些类该拆、哪里该加适配器,给你结构优化建议。
把抽象层和实现层分成两半,抽象管调用规则,实现管具体下单或算信号,改一边不影响另一边。
先搞懂适配器和组合,一个套旧接口一个拼新功能,覆盖大部分ea重构场景。