软件开发和 MQL5 中的设计范式(第 4 部分):行为范式 2·综合运用
(3/3)· 从备忘录到访问者,把最后几种行为范式在 MQL5 里跑通,代码不再越改越乱
接上篇,我们继续深挖行为范式里剩下的几块拼图。很多交易者写 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 里大型指标工程会少很多耦合坑。外汇与贵金属杠杆高,回测访客得出的历史表现不代表未来,仅作概率参考。
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 线元素上热插拔不同分析视角。外汇与贵金属波动剧烈,这类结构仅降低代码耦合,不预示任何方向,策略胜率仍取决于参数与行情匹配度。
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 里动态加载输入参数仍做不到,参数对象得在编译期定好。 别把范式当银弹 外汇与贵金属杠杆高、滑点跳空频繁,范式只解决代码可维护性,不替你过滤爆仓风险。先拿策略模式跑通历史回测,再谈实盘概率优势。
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 线,再决定要不要在策略层加滑点补偿。
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; }