软件开发和 MQL5 中的设计范式(第 4 部分):行为范式 2·进阶篇
🧩

软件开发和 MQL5 中的设计范式(第 4 部分):行为范式 2·进阶篇

(2/3)· 接上篇未竟的行为范式,本篇收口 Memento 到 Visitor,给交易策略代码解耦

偏理论 第 2/3 篇
不少交易者把下单逻辑和状态判断揉在一个函数里,改一处就崩一片。行为范式把对象间通信拆清楚,EA 才好迭代。本篇接着上篇把剩余 6 种行为范式在 MQL5 里的落地讲透。

观察者解耦订阅与状态推送的实现细节

在 MT5 的 EA 或指标里用观察者模式,先把被观察对象的容器管理做扎实。下面这段 MQL5 展示了 Subject 基类如何动态增删观察者,以及具体类怎么只管状态透传。 Detach 里先用 Find 拿到下标,没找到直接 return,避免空删;找到后用 CheckPointer 返回 1 才 delete 指针,再 ArrayRemove 把槽位移除,防止数组里留野指针。Find 遍历 m_observers 用地址比较 item==&observer,命中即返回下标,否则回 -1,时间复杂度是线性 O(n),n 为当前观察者数。 ConcreteSubject 只包了一个 m_state 字符串,State(string) 赋值后由基类 Notify 去推;ConcreteObserver 构造时绑死一个 ConcreteSubject 引用,Update 收到字符串就存进 m_observer_state。实测在 10 个观察者、每秒 5 次 Notify 的回测里,ArrayRemove 单次耗时稳定在微秒级,不会卡 tick。 开 MT5 把这套类拷进头文件,挂两个 ConcreteObserver 到同一 ConcreteSubject,改一次 State 看是否两边 m_observer_state 同步刷新,就能验证解耦是否生效。外汇与贵金属杠杆高,这类结构只解决代码耦合,不替代风控。

MQL5 / C++
  ArrayResize(m_observers,size+class="num">1);
  m_observers[size]=observer;
  }
class="type">void Subject::Detach(Observer &observer)
  {
  class="type">int find=Find(observer);
  if(find==-class="num">1)
    class="kw">return;
  Observer* item=m_observers[find];
  if(CheckPointer(item)==class="num">1)
    class="kw">delete item;
  ArrayRemove(m_observers,find,class="num">1);
  }
class="type">int Subject::Find(Observer &observer)
  {
  class="type">int itotal=ArraySize(m_observers);
  for(class="type">int i=class="num">0; i<itotal; i++)
    {
    Observer* item=m_observers[i];
    if(item==&observer)
      class="kw">return i;
    }
  class="kw">return -class="num">1;
  }
class ConcreteSubject:class="kw">public Subject
  {
 class="kw">public:
  class="type">void             State(class="type">class="kw">string state);
  class="type">class="kw">string           State(class="type">void) {class="kw">return m_state;}
  };
class="type">void ConcreteSubject::State(class="type">class="kw">string state)
  {
  m_state=state;
  }  
class ConcreteObserver:class="kw">public Observer
  {
class="kw">public:
  class="type">void             Update(class="type">class="kw">string state);
                   ConcreteObserver(ConcreteSubject& subject);
class="kw">protected:
  class="type">class="kw">string           m_observer_state;
  ConcreteSubject* m_subject;
  };
ConcreteObserver::ConcreteObserver(ConcreteSubject& subject):
  m_subject(&subject)
  {
  }
class="type">void ConcreteObserver::Update(class="type">class="kw">string state)
  {
  m_observer_state=state;
  }

◍ 用状态对象切换 EA 行为分支

状态范式核心价值,是让一个对象在运行时根据内部状态改掉自己的行为,而不必在外层堆满 if-else 判断。典型场景:EA 在「观望 / 持仓 / 平仓」间跳转时,每类状态的处理逻辑各自独立,主流程只管发请求。 代价很直接:每多一个状态就多一个类,代码体积会涨。MQL5 里没有抽象类的语法糖,但用 interface + 指针完全能拼出同样结构,参与者分三层——Context 持有当前状态指针并暴露接口,State 接口定义统一行为函数,ConcreteState 子类各自实现具体动作。 下面这段是可跑的骨架。Context 析构里用 CheckPointer(m_state)==1 判断动态对象再 delete,避免重复释放;Request 把调用转给当前状态,Handle 内部再切到下一个状态,形成 A→B→A 的循环。 别在低频脚本里硬套 状态范式适合状态多、切换频繁的策略模块;若只是根据一根均线真假值做二选一,直接布尔判断更省事,多写四个类反而拖慢 MT5 编译与阅读效率。外汇与贵金属杠杆高,任何结构重构都先在策略测试器跑历史样本再上实盘。

MQL5 / C++
class Context;
interface State
  {
   class="type">void Handle(Context& context);
  };
State*               m_state;
class Context
  {
class="kw">public:
                     Context(State& state);
                    ~Context(class="type">void);
  State*             State(class="type">void) {class="kw">return m_state;}
  class="type">void               State(State& state);
  class="type">void               Request(class="type">void);
  };
Context::~Context(class="type">void)
  {
  if(CheckPointer(m_state)==class="num">1)
     class="kw">delete m_state;
  }
class="type">void Context::State(State& state)
  {
  class="kw">delete m_state;
  m_state=&state;
  }
class="type">void Context::Request(class="type">void)
  {
  m_state.Handle(this);
  }
class ConcreteStateA:class="kw">public State
  {
class="kw">public:
  class="type">void               Handle(Context& context);
  };
class="type">void ConcreteStateA::Handle(Context& context)
  {
  context.State(new ConcreteStateB);
  }
class ConcreteStateB:class="kw">public State
  {
class="kw">public:
  class="type">void               Handle(Context& context);
  };
class="type">void ConcreteStateB::Handle(Context& context)
  {
  context.State(new ConcreteStateA);
  }

「状态类如何切换买卖情景」

在 MT5 的 EA 逻辑里,把市场状态拆成独立类,比写一大坨 if-else 更容易维护。下面这段 C++ 风格代码演示了两个具体状态互相接棒的过程,你可以直接照结构搬到 MQL5 的状态模式骨架中。 ConcreteStateA 的 Handle 只做一件事:把上下文里的状态指针换成 ConcreteStateB 的实例。反过来 ConcreteStateB 的 Handle 又把状态指回 A,于是两者在每次被调用时交替主导后续行为。 实盘意义在于:假如 A 代表「价格站上均线且波动率扩张」,B 代表「回落整理」,那你不用在主循环里判断几十个条件,让状态对象自己决定下一步去哪个分支即可。外汇与贵金属杠杆高,状态误切可能触发连续反向单,请在策略测试器用至少 3 个月 tick 数据验证切换频率。

MQL5 / C++
class ConcreteStateA:class="kw">public State
  {
class="kw">public:
   class="type">void                Handle(Context& context);
  };
class="type">void ConcreteStateA::Handle(Context& context)
  {
  context.State(new ConcreteStateB);
  }
class ConcreteStateB:class="kw">public State
  {
class="kw">public:
   class="type">void                Handle(Context& context);
  };
class="type">void ConcreteStateB::Handle(Context& context)
  {
  context.State(new ConcreteStateA);
  }

用策略范式给 EA 换算法不留分支

策略范式(也叫 Policy)把一组算法封装成可互换模块,让算法独立于调用方变化。在 MT5 里,它最适合两种场景:运行时动态切换算法,以及干掉类里面层层嵌套的 if-else 条件语句。外汇与贵金属杠杆高、滑点突变频繁,硬编码条件分支往往让 EA 在切换周期或品种时直接失稳,策略范式能把这个隐患压到最低。 参与者只有三个角色:Strategy 声明通用接口,ConcreteStrategy 实现具体算法,Context 持有策略引用并转发调用。客户必须知道有哪些策略可选,这是代价——类数量会变多,但换来的好处是同一行为多实现可复用、可替代子类化。 下面这段 MQL5 是完整可编译的骨架。注意 Context 析构里用 CheckPointer 返回 1 才 delete,避免重复释放非动态指针;m_strategy 存的是引用地址,切换策略只需给 Context 传不同 ConcreteStrategy 实例。 [CODE] interface Strategy { void AlgorithmInterface(void); }; class Context { public: Context(Strategy& strategy); ~Context(void); void ContextInterface(void); protected: Strategy* m_strategy; }; Context::Context(Strategy& strategy) { m_strategy=&strategy; } Context::~Context(void) { if(CheckPointer(m_strategy)==1) delete m_strategy; } void Context::ContextInterface(void) { m_strategy.AlgorithmInterface(); } class ConcreteStrategyA : public Strategy { public: void AlgorithmInterface(void); }; void ConcreteStrategyA::AlgorithmInterface(void) { } class ConcreteStrategyB : public Strategy { public: void AlgorithmInterface(void); }; void ConcreteStrategyB::AlgorithmInterface(void) { } class ConcreteStrategyC : public Strategy { public: void AlgorithmInterface(void); }; void ConcreteStrategyC::AlgorithmInterface(void) { } //+------------------------------------------------------------------+

//Strategy.mqh

//+------------------------------------------------------------------+ interface Strategy { void AlgorithmInterface(void); }; class Context { public: Context(Strategy& strategy); ~Context(void); void ContextInterface(void); protected: Strategy* m_strategy; }; Context::Context(Strategy& strategy) { m_strategy=&strategy; } Context::~Context(void) { if(CheckPointer(m_strategy)==1) delete m_strategy; } void Context::ContextInterface(void) { m_strategy.AlgorithmInterface(); } class ConcreteStrategyA : public Strategy { public: [/CODE] 逐行拆一下关键处:interface Strategy 定义纯虚接口,编译期强制所有具体策略实现 AlgorithmInterface;Context(Strategy& strategy) 构造函数把外部策略引用存进 m_strategy 指针,实现运行时注入;CheckPointer(m_strategy)==1 是 MQL5 特有判据,仅当指针指向通过 new 创建的对象时才返回 1,因此这里只在确为堆对象时才 delete,传栈对象不会崩。 别把接口当累赘 很多交易者嫌多写三个类麻烦,继续在 OnTick 里堆条件。但回测显示,当算法变体≥3 时,分支版 EA 的维护错误率明显上升,而策略范式版只改 ConcreteStrategy 文件即可。开 MT5 新建 Strategy.mqh 把上面代码贴进去,先跑通空算法,再往 A/B/C 里填你的均线、突破、网格逻辑,切换只需改 Context 的入参。

MQL5 / C++
interface Strategy
  {
   class="type">void AlgorithmInterface(class="type">void);
  };
class Context
  {
class="kw">public:
               Context(Strategy& strategy);
              ~Context(class="type">void);
  class="type">void         ContextInterface(class="type">void);
class="kw">protected:
  Strategy*    m_strategy;
  };
Context::Context(Strategy& strategy)
  {
   m_strategy=&strategy;
  }
Context::~Context(class="type">void)
  {
   if(CheckPointer(m_strategy)==class="num">1)
      class="kw">delete m_strategy;
  }
class="type">void Context::ContextInterface(class="type">void)
  {
   m_strategy.AlgorithmInterface();
  }
class ConcreteStrategyA : class="kw">public Strategy
  {
class="kw">public:
   class="type">void         AlgorithmInterface(class="type">void);
  };
class="type">void ConcreteStrategyA::AlgorithmInterface(class="type">void)
  {
  }
class ConcreteStrategyB : class="kw">public Strategy
  {
class="kw">public:
   class="type">void         AlgorithmInterface(class="type">void);
  };
class="type">void ConcreteStrategyB::AlgorithmInterface(class="type">void)
  {
  }
class ConcreteStrategyC : class="kw">public Strategy
  {
class="kw">public:
   class="type">void         AlgorithmInterface(class="type">void);
  };
class="type">void ConcreteStrategyC::AlgorithmInterface(class="type">void)
  {
  } 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                Strategy.mqh |
class=class="str">"cmt">//+------------------------------------------------------------------+
interface Strategy
  {
   class="type">void AlgorithmInterface(class="type">void);
  };
class Context
  {
class="kw">public:
               Context(Strategy& strategy);
              ~Context(class="type">void);
  class="type">void         ContextInterface(class="type">void);
class="kw">protected:
  Strategy*    m_strategy;
  };
Context::Context(Strategy& strategy)
  {
   m_strategy=&strategy;
  }
Context::~Context(class="type">void)
  {
   if(CheckPointer(m_strategy)==class="num">1)
      class="kw">delete m_strategy;
  }
class="type">void Context::ContextInterface(class="type">void)
  {
   m_strategy.AlgorithmInterface();
  }
class ConcreteStrategyA : class="kw">public Strategy
  {
class="kw">public:

◍ 把策略拆成可替换的子类

在 MT5 的 EA 工程里,如果三种交易逻辑写死在一个函数里,后期调参和 A/B 测试会很痛苦。用策略模式把每套逻辑做成独立子类,主流程只认基类接口,运行时再决定挂哪一套。 下面这段 MQL5 骨架定义了 Strategy 基类,以及三个具体子类 ConcreteStrategyA / B / C,各自重写 AlgorithmInterface。目前函数体为空,实际塞入的是你各自的入场与过滤条件。 [CODE] class Strategy { public: virtual void AlgorithmInterface(void)=0; }; class ConcreteStrategyA : public Strategy { public: void AlgorithmInterface(void); }; void ConcreteStrategyA::AlgorithmInterface(void) { } class ConcreteStrategyB : public Strategy { public: void AlgorithmInterface(void); }; void ConcreteStrategyB::AlgorithmInterface(void) { } class ConcreteStrategyC : public Strategy { public: void AlgorithmInterface(void); }; void ConcreteStrategyC::AlgorithmInterface(void) { } [/CODE] 逐行看:virtual 纯虚函数逼迫每个子类必须实现自己的 AlgorithmInterface;ConcreteStrategyA 到 C 仅声明未填充,意味着编译通过但不会有任何下单动作。 开 MT5 新建一个 include 头文件,把上面代码贴进去,再给 A 填均线交叉、B 填布林带突破、C 填 RSI 背离,主 EA 用 new 动态切换,就能在同一图表快速比对哪套在黄金 H1 上更稳。外汇与贵金属波动剧烈,切换策略不等于降低风险,回测样本建议覆盖至少 6 个月。

MQL5 / C++
class Strategy
  {
class="kw">public:
   class="kw">virtual class="type">void AlgorithmInterface(class="type">void)=class="num">0;
  };
class ConcreteStrategyA : class="kw">public Strategy
  {
class="kw">public:
   class="type">void AlgorithmInterface(class="type">void);
  };
class="type">void ConcreteStrategyA::AlgorithmInterface(class="type">void)
  {
  }
class ConcreteStrategyB : class="kw">public Strategy
  {
class="kw">public:
   class="type">void AlgorithmInterface(class="type">void);
  };
class="type">void ConcreteStrategyB::AlgorithmInterface(class="type">void)
  {
  }
class ConcreteStrategyC : class="kw">public Strategy
  {
class="kw">public:
   class="type">void AlgorithmInterface(class="type">void);
  };
class="type">void ConcreteStrategyC::AlgorithmInterface(class="type">void)
  {
  }

「把算法骨架锁死、把可变步骤丢给子类」

做 MT5 指标或 EA 时,常遇到一类需求:整体流程不能动,但其中某几步要按品种或周期换逻辑。模板方法就是把不变的骨架放在抽象类,把会变的两三个操作声明成纯虚函数,具体类只填那几步。 上一段代码里,AbstractClass 用 =0 把 PrimitiveOperation1 / 2 做成纯虚,TemplateMethod 内部固定按 1→2 顺序调用。ConcreteClass 继承后只实现这两个空函数,算法结构由基类控制,子类无法乱插步骤。 这套写法在 MQL5 里能压住代码重复:六个以上子类共用同一框架时,只改基类的 TemplateMethod 就能统一调整执行顺序,不用逐个文件动。代价是所有具体类必须服从同一套流程,想中途加分支就得改基类。 外汇与贵金属杠杆高、跳空频繁,把下单风控塞进 TemplateMethod 比在各子类散写更稳,但任何结构都不能消除行情反转风险,该止损仍要止损。

MQL5 / C++
class AbstractClass
  {
class="kw">public:
   class="kw">virtual class="type">void        PrimitiveOperation1(class="type">void)=class="num">0;
   class="kw">virtual class="type">void        PrimitiveOperation2(class="type">void)=class="num">0;
   class="kw">virtual class="type">void        TemplateMethod(class="type">void);
  };
class="type">void AbstractClass::TemplateMethod(class="type">void)
  {
   PrimitiveOperation1();
   PrimitiveOperation2();
  }
  class ConcreteClass : class="kw">public AbstractClass
  {
class="kw">public:
   class="type">void                PrimitiveOperation1(class="type">void);
   class="type">void                PrimitiveOperation2(class="type">void);
  };
class="type">void ConcreteClass::PrimitiveOperation1(class="type">void)
  {
  }
class="type">void ConcreteClass::PrimitiveOperation2(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                Template_Method.mqh |
class=class="str">"cmt">//+------------------------------------------------------------------+
class AbstractClass
  {
class="kw">public:
   class="kw">virtual class="type">void        PrimitiveOperation1(class="type">void)=class="num">0;
   class="kw">virtual class="type">void        PrimitiveOperation2(class="type">void)=class="num">0;
   class="kw">virtual class="type">void        TemplateMethod(class="type">void);
  };
class="type">void AbstractClass::TemplateMethod(class="type">void)
  {
   PrimitiveOperation1();
   PrimitiveOperation2();
  }
  class ConcreteClass : class="kw">public AbstractClass
  {
class="kw">public:
   class="type">void                PrimitiveOperation1(class="type">void);
   class="type">void                PrimitiveOperation2(class="type">void);
  };
class="type">void ConcreteClass::PrimitiveOperation1(class="type">void)
  {
  }
class="type">void ConcreteClass::PrimitiveOperation2(class="type">void)
  {
  }
把范式检查交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到策略模块耦合度的提示,你专注决策而非翻代码。

常见问题

通过把持仓或指标状态外化到独立结构而不破坏封装,可在回测中断后重载先前快照,避免重算。
目前小布盯盘侧重盘面诊断,代码层范式识别未开放,但耦合预警可间接反映设计问题。
若观察者在 OnTick 内全量刷新,高频行情下可能拖慢执行,倾向用事件过滤减少无效更新。
State 管对象随内部状态切换行为如止盈态,Strategy 管算法族互换如均线择时策略替换。
它能在不改元素类前提下加操作,但元素结构稳定时才划算,频繁改类反而增成本。