软件开发和 MQL5 中的设计范式(第 4 部分):行为范式 2·综合运用
🧩

软件开发和 MQL5 中的设计范式(第 4 部分):行为范式 2·综合运用

(3/3)· 从备忘录到访问者,把最后几种行为范式在 MQL5 里跑通,代码不再越改越乱

新手友好 第 3/3 篇

接上篇,我们继续深挖行为范式里剩下的几块拼图。很多交易者写 EA 写到后期,加一个平仓逻辑就要动半套代码,本质是没把对象间的通信和责任分配理清楚。本文把本系列收尾,带你把 Memento、Observer、State、Strategy、Template、Visitor 在 MQL5 中真正用起来。

「把新操作塞进旧对象结构而不污染类」

访客范式适合这种场景:你的 EA 里有一堆不同接口的对象(比如行情元素、信号元素、风控元素),类结构基本不动,但经常要往上加新算法——算回测、导出日志、做压力测试。直接把方法写进类里会越改越脏,访客把操作抽出来,类只留一个 Accept(Visitor*) 的口子。 它的参与者很清晰:Visitor 接口给每个具体元素类声明一个 Visit 方法;ConcreteVisitor 实现这些 Visit,并在遍历结构时累积自己的局部状态;Element 和 ConcreteElement 只负责接住访客并把自己交出去;ObjectStructure 负责枚举元素,可能是个数组或集合。 实际好处有两点可验证:往结构上加新操作只需新增一个 ConcreteVisitor 类,不用碰任何 Element 子类;相关操作(如多种统计)能收进同一个访客,不相关的(如导出和风控检查)分开放,类职责不串味。 下面这段 MQL5 是完整骨架。Element 基类的 Switch 用 CheckPointer(m_visitor)==1 判断指针有效才 delete,避免重复释放;ConcreteElementA::Accept 里先 Switch(visitor) 再调用 visitor.VisitElementA(&this),把自身类型暴露给访客做具体分发。 别在元素类里写死算法 Accept 只做转发,OperationA/B 目前是空壳,真逻辑放在 ConcreteVisitor1/2 的 Visit 实现里。这样你换统计口径时,只改访客、不重新编译元素类,MT5 里大型指标工程会少很多耦合坑。外汇与贵金属杠杆高,回测访客得出的历史表现不代表未来,仅作概率参考。

MQL5 / C++
interface Visitor;
class Element
  {
class="kw">protected:
   Visitor*            m_visitor;
class="kw">public:
                     ~Element(class="type">void);
   class="kw">virtual class="type">void       Accept(Visitor* visitor)=class="num">0;
class="kw">protected:
   class="type">void               Switch(Visitor* visitor);
  };
Element::~Element(class="type">void)
  {
   if(CheckPointer(m_visitor)==class="num">1)
      class="kw">delete m_visitor;
  }
class="type">void Element::Switch(Visitor *visitor)
  {
   if(CheckPointer(m_visitor)==class="num">1)
      class="kw">delete m_visitor;
   m_visitor=visitor;
  }
class ConcreteElementA : class="kw">public Element
  {
class="kw">public:
   class="type">void                Accept(Visitor*);
   class="type">void                OperationA(class="type">void);
  };
class="type">void ConcreteElementA::OperationA(class="type">void)
  {
  }
class="type">void ConcreteElementA::Accept(Visitor *visitor)
  {
   Switch(visitor);
   visitor.VisitElementA(&this);
  }
class ConcreteElementB : class="kw">public Element
  {
class="kw">public:
   class="type">void                Accept(Visitor* visitor);
   class="type">void                OperationB(class="type">void);
  };
class="type">void ConcreteElementB::OperationB(class="type">void)
  {
  }
class="type">void ConcreteElementB::Accept(Visitor *visitor)
  {
   Switch(visitor);
   visitor.VisitElementB(&this);
  }
interface Visitor
{
   class="type">void VisitElementA(ConcreteElementA*);
   class="type">void VisitElementB(ConcreteElementB*);
};
class ConcreteVisitor1 : class="kw">public Visitor
  {
class="kw">public:
   class="type">void                VisitElementA(ConcreteElementA* visitor);
   class="type">void                VisitElementB(ConcreteElementB* visitor);
  };
class="type">void ConcreteVisitor1::VisitElementA(ConcreteElementA* visitor)
  {
   visitor.OperationA();
  }
class="type">void ConcreteVisitor1::VisitElementB(ConcreteElementB* visitor)
  {
   visitor.OperationB();
  }
class ConcreteVisitor2 : class="kw">public Visitor
  {
class="kw">public:
   class="type">void                VisitElementA(ConcreteElementA*);
   class="type">void                VisitElementB(ConcreteElementB*);
  };
class="type">void ConcreteVisitor2::VisitElementA(ConcreteElementA* visitor)
  {
   visitor.OperationA();
  }
class="type">void ConcreteVisitor2::VisitElementB(ConcreteElementB* visitor)
  {
   visitor.OperationB();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

用访问者模式拆开指标元素与算法

在 MT5 的 EA 或指标工程里,若把具体计算逻辑直接写进价格元素类,后续增删统计口径会牵一发动全身。下面这套访问者结构把 Element 与 Visitor 解耦:元素只管接收访问者,具体动作推迟到 Visitor 子类实现。 基类 Element 持有一个 Visitor 指针 m_visitor,析构时通过 CheckPointer(m_visitor)==1 判断有效性再 delete,避免重复释放或野指针。Switch() 负责替换访问者前先清理旧实例,保证同一元素串行挂载不同算法时不漏内存。 ConcreteElementA / B 各自实现 Accept(),内部先 Switch(visitor) 再回调 visitor.VisitElementA/B(&this)。这意味着你新增一种遍历方式(比如从「算均线斜率」换成「算波动率分位」),只需写新的 ConcreteVisitor 类,不用改任何 Element 派生类。 两个具体访问者 ConcreteVisitor1 与 ConcreteVisitor2 目前都只调了 OperationA/B 的空壳,但接口已固化:VisitElementA 接收 ConcreteElementA*、VisitElementB 接收 ConcreteElementB*。实盘接数据时,把 OperationA 填成获取某周期收盘价序列、OperationB 填成计算价差分布,就能在同一批 K 线元素上热插拔不同分析视角。外汇与贵金属波动剧烈,这类结构仅降低代码耦合,不预示任何方向,策略胜率仍取决于参数与行情匹配度。

MQL5 / C++
class=class="str">"cmt">//|                                                                   Visitor.mqh |
class=class="str">"cmt">//+------------------------------------------------------------------+
interface Visitor;
class Element
  {
class="kw">protected:
   Visitor*            m_visitor;
class="kw">public:
                    ~Element(class="type">void);
   class="kw">virtual class="type">void      Accept(Visitor* visitor)=class="num">0;
class="kw">protected:
   class="type">void              Switch(Visitor* visitor);
  };
Element::~Element(class="type">void)
  {
   if(CheckPointer(m_visitor)==class="num">1)
      class="kw">delete m_visitor;
  }
class="type">void Element::Switch(Visitor *visitor)
  {
   if(CheckPointer(m_visitor)==class="num">1)
      class="kw">delete m_visitor;
   m_visitor=visitor;
  }
class ConcreteElementA : class="kw">public Element
  {
class="kw">public:
   class="type">void              Accept(Visitor*);
   class="type">void              OperationA(class="type">void);
  };
class="type">void ConcreteElementA::OperationA(class="type">void)
  {
  }
class="type">void ConcreteElementA::Accept(Visitor *visitor)
  {
   Switch(visitor);
   visitor.VisitElementA(&this);
  }
class ConcreteElementB : class="kw">public Element
  {
class="kw">public:
   class="type">void              Accept(Visitor* visitor);
   class="type">void              OperationB(class="type">void);
  };
class="type">void ConcreteElementB::OperationB(class="type">void)
  {
  }
class="type">void ConcreteElementB::Accept(Visitor *visitor)
  {
   Switch(visitor);
   visitor.VisitElementB(&this);
  }
interface Visitor
  {
   class="type">void VisitElementA(ConcreteElementA*);
   class="type">void VisitElementB(ConcreteElementB*);
  };
class ConcreteVisitor1 : class="kw">public Visitor
  {
class="kw">public:
   class="type">void              VisitElementA(ConcreteElementA* visitor);
   class="type">void              VisitElementB(ConcreteElementB* visitor);
  };
class="type">void ConcreteVisitor1::VisitElementA(ConcreteElementA* visitor)
  {
   visitor.OperationA();
  }
class="type">void ConcreteVisitor1::VisitElementB(ConcreteElementB* visitor)
  {
   visitor.OperationB();
  }
class ConcreteVisitor2 : class="kw">public Visitor
  {
class="kw">public:
   class="type">void              VisitElementA(ConcreteElementA*);
   class="type">void              VisitElementB(ConcreteElementB*);
  };
class="type">void ConcreteVisitor2::VisitElementA(ConcreteElementA* visitor)
  {
   visitor.OperationA();
  }
class="type">void ConcreteVisitor2::VisitElementB(ConcreteElementB* visitor)
  {
   visitor.OperationB();
  }

◍ 把范式清单变成可跑的 MT5 骨架

走到这一步,创建、结构、行为三类范式你已经能在 MQL5 里认全:单例管全局状态,外观收口复杂接口,观察者解耦信号与执行,状态机切换持仓逻辑。附件里那 6 个 .mqh(Memento 1.34 KB、Observer 2.5 KB、State 1.19 KB、Strategy 1.24 KB、Template_Method 0.77 KB、Visitor 2.14 KB)直接拖进 MT5 的 Include 目录就能 include 复用。 有读者在回测里用策略模式把 RSI 和 MA 塞进同一个 EA:OnInit 里用 switch 根据枚举 new 出具体策略,OnTick 只调通用方法,一套专家就能优化多策略。下面这段就是他贴的上下文切换核心,注意 MT5 里动态加载输入参数仍做不到,参数对象得在编译期定好。 别把范式当银弹 外汇与贵金属杠杆高、滑点跳空频繁,范式只解决代码可维护性,不替你过滤爆仓风险。先拿策略模式跑通历史回测,再谈实盘概率优势。

MQL5 / C++
class="kw">switch(strategyName)
    {
     case STRATEGY_RSI :
         m_strategy = new Strategy_RSI();
         break;
     case STRATEGY_MA :
         m_strategy = new Strategy_MA();
         break;
     class="kw">default:
         break;
}
[...]
class=class="str">"cmt">// 通过专家方法将信号参数添加到策略中
Expert.AddParameterObject(signal_RSI_Params); class=class="str">"cmt">// signal_RSI_Params 是参数对象,其中包含输入参数
class=class="str">"cmt">//AddParameterObject 方法的细节(简化):
class="type">bool CExpert::AddParameterObject(CParameter & parameter)
  {
     if(!m_strategy.AddParameterObject(parameter))
       {
        class="kw">return(false);
       }
   }
  class="kw">return true;
  }
class=class="str">"cmt">// 添加通用专家参数。
Expert.InitStrategyParameters(expert_1_Params);
class=class="str">"cmt">//方法的细节(简化) :
class="type">bool CExpert::InitStrategyParameters(CParameter & expertParameters)
  {
class=class="str">"cmt">// 
   if(!m_strategy.InitStrategyParameters(expertParameters))
    {
    [...]

「别急着下结论」

上面这段 MQL5 片段把开多单的判定收口在 CExpert::CheckOpenLong 里,外部只靠 m_strategy.CheckOpenLong(...) 返回 true 才放行,等于把策略信号和订单执行做了硬性解耦。 Memento::SetState 与 Originator::State 都只是把传入字符串写进 m_state,说明这套状态存档没有做校验,实盘里若状态串被并发改写,回测和直播会出现偏差。 真要验证,把 CheckOpenLong 的返回分支打日志跑一周 EURUSD M15,看信号触发和实际成交的时差是否稳定小于 2 根 K 线,再决定要不要在策略层加滑点补偿。

MQL5 / C++
    class="kw">return false;
    }
  class="kw">return true;
}
class="type">bool CExpert::CheckOpenLong(class="type">void)
  {
[...]
    if(true == m_strategy.CheckOpenLong([...]))
      {
[...]
}
[...]
}
class="type">void Memento::SetState(class="type">class="kw">string state)
  {
   m_state=state;
  }


class="type">void Originator::State(class="type">class="kw">string state)
  {
   m_state=state;
  }
把范式检查交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 结构是否出现职责耦合过重。把重复劳动交给小布,你专注决策。

常见问题

倾向适合。一对多依赖可让一个状态对象变更时通知多个图表上的 EA 实例,但需注意跨品种通讯的吞吐与延迟,外汇贵金属波动快、高风险,实际表现需回测验证。
把核心步骤设为 final 或私有,只暴露特定虚函数给子类重写,这样算法骨架不变、仅局部可替换,降低出错概率。
Strategy 封装可互换算法供客户端选;State 是对象内部状态变了自己换行为。简单说一个你主动换,一个它自己换。
目前小布内置的是结构诊断与盘口辅助,不直接解析源码范式,但你能参照本文梳理职责后,用它的 AIGC 笔记功能记录自己的 EA 架构。
可能略有增加,因多了一层接受者分派。若元素类稳定、操作常新增,收益大于成本;高频 tick 环境需实测。