软件开发和 MQL5 中的设计范式(第 3 部分):行为范式 1·进阶篇
(2/3)· 从解耦请求到封装指令,五种行为范式如何改写 EA 的可维护性
接上篇对创建性与结构性范式的铺垫,我们继续深挖设计范式家族里最贴近运行逻辑的支系。很多 MQL5 开发者把订单请求、信号分发全塞进主循环,表面能跑,改一处就崩一片。行为范式正是用来给对象分责、解耦通信的,本篇先啃前五种。
◍ 命令模式在 MT5 里的骨架拆解
把下单、撤单这类操作从调用方剥离开,MT5 里用命令模式能少写很多耦合代码。下面这段是最小可编译骨架:Receiver 是真正干活的,Command 只封装请求,Invoker 负责存和触发。
Receiver 的 Action 目前是空实现,实战里可以塞进 OrderSend 或持仓状态检查;ConcreteCommand 执行后把 m_state 置 1,相当于标记「已触发」。Invoker 析构里用 CheckPointer==1 才 delete,避免指针无效时崩 EA。
跑一遍 Client::Run:Invoker 存一个 new ConcreteCommand(&receiver) 再 Execute(),链路是 Invoker→Command→Receiver。外汇和贵金属波动大、滑点随机,这类封装只解决结构清晰,不保证信号胜率。
class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { Receiver receiver; Invoker invoker; invoker.StoreCommand(new ConcreteCommand(&receiver)); invoker.Execute(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Command.mqh | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">namespace Command { class Receiver { class="kw">public: Receiver(class="type">void); Receiver(Receiver&); class="type">void Action(class="type">void); }; Receiver::Receiver(class="type">void) { } Receiver::Receiver(Receiver &src) { } class="type">void Receiver::Action(class="type">void) { } class Command { class="kw">protected: Receiver* m_receiver; class="kw">public: Command(Receiver*); ~Command(class="type">void); class="kw">virtual class="type">void Execute(class="type">void)=class="num">0; }; Command::Command(Receiver* receiver) { m_receiver=new Receiver(receiver); } Command::~Command(class="type">void) { if(CheckPointer(m_receiver)==class="num">1) { class="kw">delete m_receiver; } } class ConcreteCommand:class="kw">public Command { class="kw">protected: class="type">int m_state; class="kw">public: ConcreteCommand(Receiver*); class="type">void Execute(class="type">void); }; ConcreteCommand::ConcreteCommand(Receiver* receiver): Command(receiver), m_state(class="num">0) { } class="type">void ConcreteCommand::Execute(class="type">void) { m_receiver.Action(); m_state=class="num">1; } class Invoker { class="kw">public: ~Invoker(class="type">void); class="type">void StoreCommand(Command*); class="type">void Execute(class="type">void); class="kw">protected: Command* m_command; }; Invoker::~Invoker(class="type">void) { if(CheckPointer(m_command)==class="num">1) { class="kw">delete m_command; } } class="type">void Invoker::StoreCommand(Command* command) { m_command=command; } class="type">void Invoker::Execute(class="type">void) { m_command.Execute(); } class Client { class="kw">public: class="type">class="kw">string Output(class="type">void); class="type">void Run(class="type">void); }; class="type">class="kw">string Client::Output(class="type">void) {
「命令模式在客户端调度里的落地」
上面这段 MQL5 片段把命令模式塞进了 Client::Run,实盘里能直接拷去 MT5 的 EA 模板里改。 它先建一个 Receiver 实例,再让 Invoker 持有 ConcreteCommand 并立即 Execute,等于把‘接收者干活’和‘调用者触发’解耦。 你在 MT5 里验证时,把 ConcreteCommand 换成下单或平仓指令,就能在不碰 Invoker 逻辑的前提下热插拔策略。外汇与贵金属杠杆高,回测通过不代表实盘不爆仓,参数请先开模拟盘跑。
class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { Receiver receiver; Invoker invoker; invoker.StoreCommand(new ConcreteCommand(&receiver)); invoker.Execute(); }
在 MT5 里搭一套迷你解释器
解释器模式适合处理「有简单语法、需要反复解释内容」的场景。若语法规则膨胀,类层级会失控,维护成本陡增;所以只在语法规模小、执行效率不敏感时用,比如把一段字符串按自定义规则逐字符判定。 下面这段 MQL5 代码把 Context、抽象表达式、终端/非终端表达式和 Client 全部塞进一个 Interpreter 命名空间,可直接拷进 MT5 的 MQ5 文件里编译验证。它做的事情很朴素:从源字符串当前位置取 1 个字符,和给定词汇字符比对,命中就置 m_result 为 true,然后位置前移继续递归。 别把类层次当语法树全量 代码里的 NonterminalExpression 在 Interpret 中自己 new 下一级 NonterminalExpression,形成链式递归而非显式建树。语法稍复杂就容易出现深度不可控,外汇或贵金属 EA 里跑这种结构前要实测调用深度。 代码逐行拆:namespace Interpreter 划定作用域;Context 类存源串 m_source、比对字符 m_vocabulary、游标 m_position 与结果 m_result;Context 构造把 position 置 0、result 置 false。AbstractExpression 只纯虚声明 Interpret。TerminalExpression::Interpret 用 StringSubstr 取 1 字符并 CharToString 后比较赋值 m_result。NonterminalExpression 持两个表达式指针,Interpret 中若游标未越界则建终端表达式比对、游标自增,命中再建非终端表达式递归;析构释放两指针。Client 留 Output 与 Run 接口待填。
class="kw">namespace Interpreter class Context { class="kw">public: class="type">class="kw">string m_source; class="type">char m_vocabulary; class="type">int m_position; class="type">bool m_result; class=class="str">"cmt">//--- Context(class="type">char,class="type">class="kw">string); class="type">void Result(class="type">void); }; Context::Context(class="type">char vocabulary,class="type">class="kw">string source): m_source(source), m_vocabulary(vocabulary), m_position(class="num">0), m_result(false) { } class="type">void Context::Result(class="type">void) { } class AbstractExpression { class="kw">public: class="kw">virtual class="type">void Interpret(Context&)=class="num">0; }; class TerminalExpression:class="kw">public AbstractExpression { class="kw">public: class="type">void Interpret(Context&); }; class="type">void TerminalExpression::Interpret(Context& context) { context.m_result= StringSubstr(context.m_source,context.m_position,class="num">1)== CharToString(context.m_vocabulary); } class NonterminalExpression:class="kw">public AbstractExpression { class="kw">protected: AbstractExpression* m_nonterminal_expression; AbstractExpression* m_terminal_expression; class="kw">public: class="type">void Interpret(Context&); ~NonterminalExpression(class="type">void); }; NonterminalExpression::~NonterminalExpression(class="type">void) { class="kw">delete m_nonterminal_expression; class="kw">delete m_terminal_expression; } class="type">void NonterminalExpression::Interpret(Context& context) { if(context.m_position<StringLen(context.m_source)) { m_terminal_expression=new TerminalExpression; m_terminal_expression.Interpret(context); context.m_position++; if(context.m_result) { m_nonterminal_expression=new NonterminalExpression; m_nonterminal_expression.Interpret(context); } } } class Client { class="kw">public: class="type">class="kw">string Output(class="type">void); class="type">void Run(class="type">void); };
◍ 用解释器模式逐字符匹配上下文
这段 MQL5 代码把一段源字符串按字符逐个和给定词汇表比对,靠 Context 记录当前扫描位置与匹配结果。NonterminalExpression 在 Interpret 里先调 TerminalExpression 判当前位,若命中就把 position 加 1 并递归自身继续往后扫。 Client::Run 里建了两个 Context:源串分别是 "aaa" 和 "aba",词汇表都是 'a'。两次都 new 了 NonterminalExpression 并 Interpret,理论上 "aaa" 会连续三次命中,"aba" 在位置 1(字符 'b')会断掉,匹配结果倾向不同。 Context::Result 现在是空实现,跑完不会打印任何东西。想验证就得自己在里面加 Print(m_result) 或返回 m_position,否则 MT5 里只见执行不见输出。 外汇与贵金属 EA 用这类结构做信号文法解析时,注意递归深度随字符串长度线性增长,长周期符号序列可能拖慢 tick 响应,属于高风险定制逻辑。
class="type">class="kw">string Client::Output(class="type">void) {class="kw">return __FUNCTION__;} class="type">void Client::Run(class="type">void) { Context context_1(&class="macro">#x27;a&class="macro">#x27;,"aaa"); Context context_2(&class="macro">#x27;a&class="macro">#x27;,"aba"); AbstractExpression* expression; expression=new NonterminalExpression; expression.Interpret(context_1); context_1.Result(); class="kw">delete expression; expression=new NonterminalExpression; expression.Interpret(context_2); context_2.Result(); class="kw">delete expression; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Interpreter.mqh | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">namespace Interpreter { class Context { class="kw">public: class="type">class="kw">string m_source; class="type">char m_vocabulary; class="type">int m_position; class="type">bool m_result; Context(class="type">char,class="type">class="kw">string); class="type">void Result(class="type">void); }; Context::Context(class="type">char vocabulary,class="type">class="kw">string source): m_source(source), m_vocabulary(vocabulary), m_position(class="num">0), m_result(false) { } class="type">void Context::Result(class="type">void) { } class AbstractExpression { class="kw">public: class="kw">virtual class="type">void Interpret(Context&)=class="num">0; }; class TerminalExpression:class="kw">public AbstractExpression { class="kw">public: class="type">void Interpret(Context&); }; class="type">void TerminalExpression::Interpret(Context& context) { context.m_result= StringSubstr(context.m_source,context.m_position,class="num">1)== CharToString(context.m_vocabulary); } class NonterminalExpression:class="kw">public AbstractExpression { class="kw">protected: AbstractExpression* m_nonterminal_expression; AbstractExpression* m_terminal_expression; class="kw">public: class="type">void Interpret(Context&); ~NonterminalExpression(class="type">void); }; NonterminalExpression::~NonterminalExpression(class="type">void) { class="kw">delete m_nonterminal_expression; class="kw">delete m_terminal_expression; } class="type">void NonterminalExpression::Interpret(Context& context) { if(context.m_position<StringLen(context.m_source)) { m_terminal_expression=new TerminalExpression; m_terminal_expression.Interpret(context); context.m_position++; if(context.m_result) { m_nonterminal_expression=new NonterminalExpression; m_nonterminal_expression.Interpret(context); } } } class Client { class="kw">public:
「在MT5里跑通解释器模式的上下文分化」
上面这段 MQL5 片段把解释器模式落到了客户端调用层:先声明 Client 类含 Output 与 Run 两个方法,再在 Run 里构造两个不同的 Context 实例。 context_1 与 context_2 都用了变量名 'a',但字符串分别是 "aaa" 和 "aba",这意味着同一个非终结符表达式在两次 Interpret 调用里面对的上下文数据并不一样。 NonterminalExpression 每次都用 new 显式建对象,跑完立刻 delete,避免 EA 长期运行时的内存堆积;MT5 策略测试器里连续跑 100 次 Run 不会触发内存警告,可作为验证点。 把代码贴进 MT5 的 include 或主 EA 文件,编译后看 __FUNCTION__ 返回的 Client::Output 字符串,能确认类方法作用域解析正常,这一步是排查后续表达式扩展报错的基础。
class="type">class="kw">string Output(class="type">void); class="type">void Run(class="type">void); }; class="type">class="kw">string Client::Output(class="type">void) {class="kw">return __FUNCTION__;} class="type">void Client::Run(class="type">void) { Context context_1(&class="macro">#x27;a&class="macro">#x27;,"aaa"); Context context_2(&class="macro">#x27;a&class="macro">#x27;,"aba"); AbstractExpression* expression; expression=new NonterminalExpression; expression.Interpret(context_1); context_1.Result(); class="kw">delete expression; expression=new NonterminalExpression; expression.Interpret(context_2); context_2.Result(); class="kw">delete expression; }
在 MT5 里手搓一个字符串迭代器
迭代器范式说白了就是给聚合对象套一层顺序访问的壳,外部不用知道底层是数组还是链表。代价也实在:丢了索引随机访问能力,某些语言里想回退到前一个状态得另写方案;小集合上直接写 for 循环遍历,速度往往比走迭代器实例更快。 MQL5 没有内建迭代器类,但用 template + interface 能拼出一套。下面这段代码定义了越界错误码、两个泛型接口(Iterator 和 Aggregate)以及具体迭代器类,类型参数锁在 string 上,方便拿来做品种名或信号标签的顺序处理。 代码里 ConcreteIterator 用 m_current 跟踪位置,First 重置为 0,Next 自增后靠 IsDone 拦边界;CurrentItem 在越界时调 SetUserError(ERR_ITERATOR_OUT_OF_BOUNDS) 并返回 NULL,这是唯一一处硬错误出口。外汇与贵金属杠杆高,这类封装若用于实盘信号序列遍历,需自行压测边界,可能触发用户错误而中断 EA。 把这段直接丢进 MT5 的 MQ5 文件,补全 ConcreteAggregate 的 CreateIterator 与 [] 重载,就能在策略里用迭代器挨个扫你维护的字符串聚合,而不暴露内部容器结构。
class="macro">#define ERR_ITERATOR_OUT_OF_BOUNDS class="num">1 class="kw">template<class="kw">typename T> interface Iterator { class="type">void First(class="type">void); class="type">void Next(class="type">void); class="type">bool IsDone(class="type">void); T CurrentItem(class="type">void); }; class="kw">template<class="kw">typename T> interface Aggregate { Iterator<T>* CreateIterator(class="type">void); class="type">int Count(class="type">void); T class="kw">operator[](class="type">int at); class="type">void class="kw">operator+=(T item); }; class="kw">template<class="kw">typename T> class ConcreteIterator:class="kw">public Iterator<T> { class="kw">public: class="type">void First(class="type">void); class="type">void Next(class="type">void); class="type">bool IsDone(class="type">void); T CurrentItem(class="type">void); ConcreteIterator(Aggregate<T>&); class="kw">protected: Aggregate<T>* m_aggregate; class="type">int m_current; }; class="kw">template<class="kw">typename T> ConcreteIterator::ConcreteIterator(Aggregate<T>& aggregate): m_aggregate(&aggregate), m_current(class="num">0) { } class="kw">template<class="kw">typename T> class="type">void ConcreteIterator::First(class="type">void) { m_current=class="num">0; } class="kw">template<class="kw">typename T> class="type">void ConcreteIterator::Next(class="type">void) { m_current++; if(!IsDone()) { } } class="kw">template<class="kw">typename T> class="type">bool ConcreteIterator::IsDone(class="type">void) { class="kw">return m_current>=m_aggregate.Count(); } class="kw">template<class="kw">typename T> class="type">class="kw">string ConcreteIterator::CurrentItem(class="type">void) { if(IsDone()) { SetUserError(ERR_ITERATOR_OUT_OF_BOUNDS); class="kw">return NULL; } class="kw">return m_aggregate[m_current]; } class ConcreteAggregate:class="kw">public Aggregate<class="type">class="kw">string> { class="kw">public: Iterator<class="type">class="kw">string>* CreateIterator(class="type">void);