软件开发和 MQL5 中的设计范式(第 4 部分):行为范式 2·进阶篇
(2/3)· 接上篇未竟的行为范式,本篇收口 Memento 到 Visitor,给交易策略代码解耦
观察者解耦订阅与状态推送的实现细节
在 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 同步刷新,就能验证解耦是否生效。外汇与贵金属杠杆高,这类结构只解决代码耦合,不替代风控。
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 编译与阅读效率。外汇与贵金属杠杆高,任何结构重构都先在策略测试器跑历史样本再上实盘。
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 数据验证切换频率。
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 的入参。
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 个月。
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 比在各子类散写更稳,但任何结构都不能消除行情反转风险,该止损仍要止损。
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) { }