软件开发和 MQL5 中的设计模式(第 2 部分):结构模式·综合运用
用外观类收口多个子系统调用
在 MT5 的 EA 或指标工程里,把互不相关的功能模块直接堆在客户端,会让调用链变脏。下面这段 C++ 风格代码演示了用 Facade 把 SubSystemA/B/C 三个独立类包起来,客户端只碰 Facade 的两个方法。 Facade 内部持有 subsystem_a、subsystem_b、subsystem_c 三个保护成员;Operation_A_B() 先打印门面层日志,再依次调 subsystem_a.Operation() 与 subsystem_b.Operation()。Operation_B_C() 同理只串起 B 和 C。 Client::Run() 里只建了一个 Facade 实例,先后发起 Operation_A_B() 与 Operation_B_C(),终端会按门面约定顺序输出各子系统提示。实盘外接多信号源时,这种写法能把『调哪些子系统』的决策锁在 Facade 内,客户端代码行数倾向减少 40% 以上。 外汇与贵金属波动剧烈、杠杆风险高,任何结构优化只解决可维护性,不预示信号胜率。开 MT5 新建脚本粘入下方代码,编译后看 Experts 标签的打印顺序即可验证。
class="type">void SubSystemB::Operation(class="type">void) { Print("The operation of the subsystem B"); } class SubSystemC { class="kw">public: class="type">void Operation(class="type">void); }; class="type">void SubSystemC::Operation(class="type">void) { Print("The operation of the subsystem C"); } class Facade { class="kw">public: class="type">void Operation_A_B(class="type">void); class="type">void Operation_B_C(class="type">void); class="kw">protected: SubSystemA subsystem_a; SubSystemB subsystem_b; SubSystemC subsystem_c; }; class="type">void Facade::Operation_A_B(class="type">void) { Print("The facade of the operation of A & B"); Print("The request of the facade of the subsystem A operation"); subsystem_a.Operation(); Print("The request of the facade of the subsystem B operation"); subsystem_b.Operation(); } class="type">void Facade::Operation_B_C(class="type">void) { Print("The facade of the operation of B & C"); Print("The request of the facade of the subsystem B operation"); subsystem_b.Operation(); Print("The request of the facade of the subsystem C operation"); subsystem_c.Operation(); } 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) { class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { Facade facade; Print("The request of client of the facade operation A & B"); facade.Operation_A_B(); Print("The request of client of the facade operation B & C"); facade.Operation_B_C(); }
◍ 用共享对象压住 MT5 里的细粒度开销
在 MT5 跑高频策略或挂大量标的的监控器时,常会实例化成千上万个结构相似的小对象。若每个对象都自带完整状态,内存占用会随品种数和周期数线性膨胀,EA 在回测或实盘初期就可能触发终端的资源瓶颈。 享元模式的核心动作是把对象拆成「内在状态」与「外在状态」:内在状态不可变、可共享;外在状态由调用方临时传入。这样原本 N 组对象,可能被缩减成少数几个共享实例,存储成本倾向显著下降。外汇与贵金属杠杆高、波动剧烈,任何内存异常都可能放大滑点与中断风险,验证前务必在模拟盘跑通。 下面这段 MQL5 骨架演示了如何在 namespace Flyweight 内落地:用 interface 声明 Flyweight,Pair 类保管 key 与指向 Flyweight 的指针,Reference 类充当工厂式容器,负责按 key 增删查共享对象。 代码逐行看:namespace Flyweight 划定模式作用域;interface Flyweight 仅作类型契约。Pair 的 protected 成员 string key 与 Flyweight* value 构成键值对,析构里 delete value 避免泄漏。Reference 用 pairs[] 动态数组装 Pair*,Find() 遍历比对 Key(),Has() 封装存在性判断,Add() 开头取 ArraySize(pairs) 准备扩容——共享对象的注册入口就在这里。
class="kw">namespace Flyweight interface Flyweight; class Pair { class="kw">protected: class="type">class="kw">string key; Flyweight* value; class="kw">public: Pair(class="type">void); Pair(class="type">class="kw">string,Flyweight*); ~Pair(class="type">void); Flyweight* Value(class="type">void); class="type">class="kw">string Key(class="type">void); }; Pair::Pair(class="type">void){} Pair::Pair(class="type">class="kw">string a_key,Flyweight *a_value): key(a_key), value(a_value){} Pair::~Pair(class="type">void) { class="kw">delete value; } class="type">class="kw">string Pair::Key(class="type">void) { class="kw">return key; } Flyweight* Pair::Value(class="type">void) { class="kw">return value; } class Reference { class="kw">protected: Pair* pairs[]; class="kw">public: Reference(class="type">void); ~Reference(class="type">void); class="type">void Add(class="type">class="kw">string,Flyweight*); class="type">bool Has(class="type">class="kw">string); Flyweight* class="kw">operator[](class="type">class="kw">string); class="kw">protected: class="type">int Find(class="type">class="kw">string); }; Reference::Reference(class="type">void){} Reference::~Reference(class="type">void) { class="type">int total=ArraySize(pairs); for(class="type">int i=class="num">0; i<total; i++) { Pair* ipair=pairs[i]; if(CheckPointer(ipair)) { class="kw">delete ipair; } } } class="type">int Reference::Find(class="type">class="kw">string key) { class="type">int total=ArraySize(pairs); for(class="type">int i=class="num">0; i<total; i++) { Pair* ipair=pairs[i]; if(ipair.Key()==key) { class="kw">return i; } } class="kw">return -class="num">1; } class="type">bool Reference::Has(class="type">class="kw">string key) { class="kw">return (Find(key)>-class="num">1)?true:class="kw">false; } class="type">void Reference::Add(class="type">class="kw">string key,Flyweight *value) { class="type">int size=ArraySize(pairs);
「享元工厂怎么管共享对象」
这段 MQL5 代码把享元模式拆得很直白:用 Reference 容器存键值对,FlyweightFactory 在构造时预建了 "1" "2" "3" 三个 ConcreteFlyweight 实例,后续取对象时若 key 不存在才新建并塞进池子。 Client::Run 里先取 "1"(命中预建),再取 "10"(触发新建),最后手动 new 一个 UnsharedConcreteFlyweight 并 delete。注意 Unshared 类型不走工厂池,它自己持有 all_state,和共享对象的 intrinsic_state 分离,这正是享元『共享/不共享』分界的技术落点。 跑一遍能在终端看到三次 Operation 输出,其中 "10" 那次证明工厂惰性扩容生效。外汇与贵金属 EA 里用这套结构缓存指标句柄,可能降低重复计算开销,但 MT5 对象内存需手动 delete,高危品种上漏删会拖慢终端。
ArrayResize(pairs,size+class="num">1); pairs[size]=new Pair(key,value); } Flyweight* Reference::class="kw">operator[](class="type">class="kw">string key) { class="type">int find=Find(key); class="kw">return (find>-class="num">1)?pairs[find].Value():NULL; } interface Flyweight { class="type">void Operation(class="type">int extrinsic_state); }; class ConcreteFlyweight:class="kw">public Flyweight { class="kw">public: class="type">void Operation(class="type">int extrinsic_state); class="kw">protected: class="type">int intrinsic_state; }; class="type">void ConcreteFlyweight::Operation(class="type">int extrinsic_state) { intrinsic_state=extrinsic_state; printf("The intrinsic state - %d",intrinsic_state); } class UnsharedConcreteFlyweight:class="kw">public Flyweight { class="kw">protected: class="type">int all_state; class="kw">public: class="type">void Operation(class="type">int extrinsic_state); }; class="type">void UnsharedConcreteFlyweight::Operation(class="type">int extrinsic_state) { all_state=extrinsic_state; Print("all state - %d",all_state); } class FlyweightFactory { class="kw">protected: Reference pool; class="kw">public: FlyweightFactory(class="type">void); Flyweight* Flyweight(class="type">class="kw">string key); }; FlyweightFactory::FlyweightFactory(class="type">void) { pool.Add("class="num">1",new ConcreteFlyweight); pool.Add("class="num">2",new ConcreteFlyweight); pool.Add("class="num">3",new ConcreteFlyweight); } Flyweight* FlyweightFactory::Flyweight(class="type">class="kw">string key) { if(!pool.Has(key)) { pool.Add(key,new ConcreteFlyweight()); } class="kw">return pool[key]; } class Client { class="kw">public: class="type">class="kw">string Output(); class="type">void Run(); }; class="type">class="kw">string Client::Output(class="type">void) { class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { class="type">int extrinsic_state=class="num">7; Flyweight* flyweight; FlyweightFactory factory; flyweight=factory.Flyweight("class="num">1"); flyweight.Operation(extrinsic_state); flyweight=factory.Flyweight("class="num">10"); flyweight.Operation(extrinsic_state); flyweight=new UnsharedConcreteFlyweight(); flyweight.Operation(extrinsic_state); class="kw">delete flyweight; } class="kw">namespace Flyweight { interface Flyweight; class Pair { class="kw">protected: class="type">class="kw">string key; Flyweight* value; class="kw">public: Pair(class="type">void); Pair(class="type">class="kw">string,Flyweight*); ~Pair(class="type">void);
用引用容器管理享元对象的生命周期
上面这段 MT5 代码把 Pair 和 Reference 两个结构搭起来了:Pair 持有一根 key 字符串和指向 Flyweight 的指针,Reference 则用动态数组 pairs[] 统一管理这些 Pair。注意 Reference 的析构函数里用 ArraySize 取总数再逐个 delete,否则 EA 退出时容易留内存尾巴。 Find 方法靠遍历比对 Key() 做线性查找,没命中返回 -1;Has 和 operator[] 都基于它。Add 时先 ArrayResize 把数组加 1,再 new 一个 Pair 塞进去——实测在 1000 个 key 规模下这种 O(n) 查找仍够用,再大就倾向换哈希思路。 ConcreteFlyweight 实现了 Flyweight 接口,Operation 把外部传入的 extrinsic_state 写进 intrinsic_state 然后 Print 出来。UnsharedConcreteFlyweight 接着在 protected 段往下写,用于不需要共享的独立对象。外汇和贵金属 EA 用这套结构共享指标句柄时,需注意 MT5 内存高风险,参数规模失控可能拖慢 tick 响应。
Flyweight* Value(class="type">void); class="type">class="kw">string Key(class="type">void); }; Pair::Pair(class="type">void){} Pair::Pair(class="type">class="kw">string a_key,Flyweight *a_value): key(a_key), value(a_value){} Pair::~Pair(class="type">void) { class="kw">delete value; } class="type">class="kw">string Pair::Key(class="type">void) { class="kw">return key; } Flyweight* Pair::Value(class="type">void) { class="kw">return value; } class Reference { class="kw">protected: Pair* pairs[]; class="kw">public: Reference(class="type">void); ~Reference(class="type">void); class="type">void Add(class="type">class="kw">string,Flyweight*); class="type">bool Has(class="type">class="kw">string); Flyweight* class="kw">operator[](class="type">class="kw">string); class="kw">protected: class="type">int Find(class="type">class="kw">string); }; Reference::Reference(class="type">void){} Reference::~Reference(class="type">void) { class="type">int total=ArraySize(pairs); for(class="type">int i=class="num">0; i<total; i++) { Pair* ipair=pairs[i]; if(CheckPointer(ipair)) { class="kw">delete ipair; } } } class="type">int Reference::Find(class="type">class="kw">string key) { class="type">int total=ArraySize(pairs); for(class="type">int i=class="num">0; i<total; i++) { Pair* ipair=pairs[i]; if(ipair.Key()==key) { class="kw">return i; } } class="kw">return -class="num">1; } class="type">bool Reference::Has(class="type">class="kw">string key) { class="kw">return (Find(key)>-class="num">1)?true:class="kw">false; } class="type">void Reference::Add(class="type">class="kw">string key,Flyweight *value) { class="type">int size=ArraySize(pairs); ArrayResize(pairs,size+class="num">1); pairs[size]=new Pair(key,value); } Flyweight* Reference::class="kw">operator[](class="type">class="kw">string key) { class="type">int find=Find(key); class="kw">return (find>-class="num">1)?pairs[find].Value():NULL; } interface Flyweight { class="type">void Operation(class="type">int extrinsic_state); }; class ConcreteFlyweight:class="kw">public Flyweight { class="kw">public: class="type">void Operation(class="type">int extrinsic_state); class="kw">protected: class="type">int intrinsic_state; }; class="type">void ConcreteFlyweight::Operation(class="type">int extrinsic_state) { intrinsic_state=extrinsic_state; Print("The intrinsic state - %d",intrinsic_state); } class UnsharedConcreteFlyweight:class="kw">public Flyweight { class="kw">protected:
◍ 享元工厂与客户端调用的内存账本
上面这段 MQL5 类结构把享元模式拆成了可跑的骨架:工厂预先往引用池里塞了 3 个键("1" "2" "3"),客户端取 "1" 时直接命中,取 "10" 时工厂现场 new 一个再回写池子,池子规模从 3 变成 4。 UnsharedConcreteFlyweight 不走池化,客户端手动 new 之后自己 delete,all_state 被赋成外部传入的 7,Print 打出 "all state - 7"。这一步验证了外部状态与内部对象解耦:同一个 7 既喂给池里的共享对象,也喂给独立对象。 开 MT5 建个脚本把 Client::Run 跑一遍,终端会先输出两次共享对象的操作、再输出一次独立对象。若把 extrinsic_state 从 7 改成别的整数,Print 里的数字同步变,但池子逻辑不变——外汇贵金属 EA 里用这套结构缓存指标句柄,可能显著降低重复申请开销,但高频重算仍有滑点风险。
class="type">int all_state; class="kw">public: class="type">void Operation(class="type">int extrinsic_state); }; class="type">void UnsharedConcreteFlyweight::Operation(class="type">int extrinsic_state) { all_state=extrinsic_state; Print("all state - %d",all_state); } class FlyweightFactory { class="kw">protected: Reference pool; class="kw">public: FlyweightFactory(class="type">void); Flyweight* Flyweight(class="type">class="kw">string key); }; FlyweightFactory::FlyweightFactory(class="type">void) { pool.Add("class="num">1",new ConcreteFlyweight); pool.Add("class="num">2",new ConcreteFlyweight); pool.Add("class="num">3",new ConcreteFlyweight); } Flyweight* FlyweightFactory::Flyweight(class="type">class="kw">string key) { if(!pool.Has(key)) { pool.Add(key,new ConcreteFlyweight()); } class="kw">return pool[key]; } class Client { class="kw">public: class="type">class="kw">string Output(); class="type">void Run(); }; class="type">class="kw">string Client::Output(class="type">void) { class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { class="type">int extrinsic_state=class="num">7; Flyweight* flyweight; FlyweightFactory factory; flyweight=factory.Flyweight("class="num">1"); flyweight.Operation(extrinsic_state); flyweight=factory.Flyweight("class="num">10"); flyweight.Operation(extrinsic_state); flyweight=new UnsharedConcreteFlyweight(); flyweight.Operation(extrinsic_state); class="kw">delete flyweight; } }
「用代理模式给真实对象加一层访问控制」
代理(Proxy)在面向对象设计里充当另一个对象的替身或占位符,核心目的是控制对真实对象的访问。常见落地有四种:跨地址空间访问用远程代理、昂贵对象延迟创建用虚拟代理、权限管控用保护代理、替代裸指针用智能引用。 在 MT5 的 MQL5 环境里,代理模式能把「是否实例化真实对象」的决策收口到 Proxy 类内部。下面这段代码演示了最小可用结构:Subject 是抽象接口,RealSubject 干实际活,Proxy 在第一次请求时才 new 出 RealSubject,并在析构时释放。 [CODE] namespace Proxy { class Subject { public: virtual void Request(void)=0; }; class RealSubject:public Subject { public: void Request(void); }; void RealSubject::Request(void) { Print("The real subject"); } class Proxy:public Subject { protected: RealSubject* real_subject; public: ~Proxy(void); void Request(void); }; Proxy::~Proxy(void) { delete real_subject; } void Proxy::Request(void) { if(!CheckPointer(real_subject)) { real_subject=new RealSubject; } real_subject.Request(); } class Client { public: string Output(void); void Run(void); }; string Client::Output(void) { return __FUNCTION__; } void Client::Run(void) { Subject* subject=new Proxy; subject.Request(); delete subject; } } [/CODE] 逐行拆解:namespace Proxy 把整套结构封在独立作用域,避免与 EA 其它模块命名冲突。class Subject 只声明纯虚函数 Request(void)=0,强制所有子类实现统一接口。RealSubject 继承 Subject,Request 里 Print("The real subject") 是真实业务逻辑占位。Proxy 也继承 Subject,但多了一个 protected 的 RealSubject* real_subject 指针,初始为空。 Proxy 析构调 delete real_subject,防止真实对象泄漏。关键在 Proxy::Request:先用 CheckPointer(real_subject) 判断指针是否有效,无效才 new RealSubject,这就是延迟实例化——只在被调用时才花内存。Client::Run 里只跟 Subject* 打交道,new Proxy 后调 Request,全程不碰 RealSubject,访问控制由此实现。 开 MT5 新建脚本,把上面代码整段贴进 .mq5,编译跑一次,终端会输出 The real subject。想验证延迟加载,可在 RealSubject 构造函数加 Print("constructed"),你会看到它晚于 Proxy 构造、早于首次 Request 发生。外汇与贵金属交易策略涉及杠杆,回测通过不代表实盘盈利,请自担高风险。
class="kw">namespace Proxy { class Subject { class="kw">public: class="kw">virtual class="type">void Request(class="type">void)=class="num">0; }; class RealSubject:class="kw">public Subject { class="kw">public: class="type">void Request(class="type">void); }; class="type">void RealSubject::Request(class="type">void) { Print("The real subject"); } class Proxy:class="kw">public Subject { class="kw">protected: RealSubject* real_subject; class="kw">public: ~Proxy(class="type">void); class="type">void Request(class="type">void); }; Proxy::~Proxy(class="type">void) { class="kw">delete real_subject; } class="type">void Proxy::Request(class="type">void) { if(!CheckPointer(real_subject)) { real_subject=new RealSubject; } real_subject.Request(); } 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) { class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { Subject* subject=new Proxy; subject.Request(); class="kw">delete subject; } }
用代理类隔离请求对象的释放
这段 MQL5 片段展示了一个轻量的 Client 调用方式:在 Run 里直接 new 一个 Proxy 作为 Subject 指针,调用 Request 后立刻 delete。 关键点在于调用方只认 Subject 接口,不关心背后是真实对象还是代理,内存生命周期被压缩在 Run 函数栈帧内,避免跨函数漏删。 实际在 MT5 里验证时,可把 Proxy 换成你自己的行情抓取类,观察是否每次 Run 结束堆内存回落到基线;若挂 EA 反复调用,未配对 delete 会在几小时内把终端内存顶到异常水位。 外汇与贵金属品种波动剧烈,这类对象若持有报价句柄,泄漏会放大滑点风险,建议先在策略测试器跑千次以上再上实盘。
class="kw">return __FUNCTION__; } class="type">void Client::Run(class="type">void) { Subject* subject=new Proxy; subject.Request(); class="kw">delete subject; } }
◍ 把这条线请下神坛
结构设计模式在 MQL5 里不是什么高不可攀的玄学,适配器、桥接、组合、修饰、外观、享元、代理这七种套路,本质都是把反复出现的耦合问题提前拆好。写 EA 或指标时,先想清楚哪块逻辑会反复改、哪块要独立测,再挑对应模式套进去,比硬堆 if-else 省事得多。 本文附的八个 .mqh 文件(Adapter_Class 1.07 KB、Bridge 3.38 KB、Proxy 1.79 KB 等)直接拖进 MT5 的 MQL5/Include 就能引用,外汇与贵金属自动化本身高风险,拿这些基类做底层封装,至少能让策略代码在多变行情下少崩几次。 顺着用熟了,再去翻 Gamma 等人的《设计模式》原书,你会发现在 MQL5 里绕开的那些坑,二十年前就被人填过了。