模块化设计原则:单一职责、接口隔离、依赖倒置在回测系统中的应用
做回测系统,最怕什么?
我最怕的是——写了一个月代码,突然发现策略逻辑和数据处理搅在一起,改一个参数要翻遍十几个文件。嗯,这种痛,经历过的人都懂。
今天我们就聊聊模块化设计的三个核心原则。说白了,就是怎么让你的回测系统像乐高积木一样,拆得开、装得上、换得掉。
1. 单一职责原则:一个类,只干一件事
先问个问题:你的策略类里,是不是既算指标、又管下单、还负责记录日志?
如果是,那你已经踩坑了。
单一职责原则讲的是:一个模块只应该有一个被修改的理由。我见过太多回测系统,一个类里塞了七八个功能,最后改都不敢改。
核心观点:策略逻辑、数据管理、执行引擎、绩效分析——这些应该各归各的类。
举个例子。我之前接手过一个回测系统,那个 Strategy 类里既有移动平均线计算,又有滑点模拟,还管着订单簿。你想想看,改个滑点模型,结果把策略信号搞崩了。这种耦合,就是灾难。
正确的做法是这样:
// 不好的做法:一个类干所有事
class BadStrategy {
void calculateMA() { /* ... */ }
void placeOrder() { /* ... */ }
void logTrade() { /* ... */ }
}
// 好的做法:各司其职
class MovingAverageStrategy {
Signal generateSignal(DataFeed data) { /* ... */ }
}
class OrderExecutor {
void execute(Signal signal) { /* ... */ }
}
class TradeLogger {
void record(Trade trade) { /* ... */ }
}
我个人习惯是:每个类的名字就能说清楚它干什么。如果名字里出现“And”,那大概率职责过重了。
避坑指南:我曾经把一个数据清洗逻辑塞进了策略类里,结果换数据源时改了三天。从那以后,我坚持每个类只做一件事。
2. 接口隔离原则:别让客户端依赖它不需要的东西
这个原则听起来有点绕。说白了就是:你给别人的接口,要尽量小、尽量专。
为什么?因为大接口意味着高耦合。你改一个方法,所有实现类都得跟着改。
在回测系统里,我经常看到这样的接口:
// 不好的做法:臃肿的接口
interface IStrategy {
void initialize();
void onTick(Tick tick);
void onBar(Bar bar);
void onOrder(Order order);
void onTrade(Trade trade);
void onPosition(Position pos);
void onAccount(Account acc);
void onError(Error err);
void onStart();
void onStop();
}
你想想看,一个简单的均线策略,需要处理订单、账户、错误吗?不需要。但因为它实现了这个接口,就不得不写一堆空方法。
更好的做法是拆分成多个小接口:
// 好的做法:接口隔离
interface ITickHandler {
void onTick(Tick tick);
}
interface IBarHandler {
void onBar(Bar bar);
}
interface IOrderHandler {
void onOrder(Order order);
}
// 策略只需要实现它关心的接口
class MovingAverageStrategy implements ITickHandler, IBarHandler {
// 只处理 tick 和 bar
}
我记得有一次重构,把一个大接口拆成五个小接口后,代码量减少了40%。不是因为代码少了,而是因为不再需要写那些“为了编译通过”的空方法了。
注意:接口隔离不是越细越好。太细了会导致接口爆炸。我的经验是:一个接口里3-5个方法比较合适。
3. 依赖倒置原则:依赖抽象,不要依赖具体
这个原则是模块化设计的灵魂。它说的是:高层模块不应该依赖低层模块,两者都应该依赖抽象。
翻译成人话就是:你的策略代码,不应该直接依赖某个具体的数据源或券商接口。
来看一个反面教材:
// 不好的做法:直接依赖具体实现
class BacktestEngine {
private CsvDataFeed dataFeed; // 直接依赖 CSV
private SimBroker broker; // 直接依赖模拟券商
void run() {
dataFeed.load("data.csv");
broker.connect();
// ...
}
}
如果有一天你想换数据库,或者换真实券商,这个类就得大改。这就是依赖具体实现的代价。
正确的做法是依赖抽象:
// 好的做法:依赖抽象
interface IDataFeed {
Bar getNextBar();
}
interface IBroker {
void placeOrder(Order order);
}
class BacktestEngine {
private IDataFeed dataFeed; // 依赖接口
private IBroker broker; // 依赖接口
void run() {
Bar bar = dataFeed.getNextBar();
broker.placeOrder(order);
// ...
}
}
这样,你想换数据源?写个新的 IDataFeed 实现就行。想换券商?换个 IBroker 实现。引擎代码一行都不用改。
我的经验:在回测系统里,数据源、执行引擎、策略、风控——这些核心模块之间,都应该用接口来通信。这是系统能活过三年以上的关键。
三个原则的关系
这三个原则不是孤立的。它们互相配合,共同构建一个可维护的回测系统。
我画了一张图,帮你理清它们的关系:
从这张图可以看出:
- 单一职责让每个模块边界清晰
- 接口隔离让模块间的通信轻量化
- 依赖倒置让模块可以独立替换
三者缺一不可。少了任何一个,系统都会变得脆弱。
实战中的取舍
理论说完了,聊聊实战中的取舍。毕竟,现实世界不是教科书。
什么时候可以适当放宽?
- 原型阶段:快速验证想法时,可以暂时忽略一些原则。但记得回头重构。
- 性能瓶颈处:比如高频回测中,过多的接口调用可能影响性能。这时可以适当内联。
- 团队规模小:如果就你一个人写代码,可以灵活一些。但别养成坏习惯。
什么时候必须严格遵循?
- 多人协作的项目:接口不清晰,沟通成本会指数级上升。
- 长期维护的系统:半年后你自己都可能忘了当初为什么这么写。
- 需要对接外部系统:比如对接不同券商、不同数据源时,依赖倒置是必须的。
我的建议:刚开始做回测系统时,先按原则来。等你有经验了,自然知道哪里可以“偷懒”。但新手阶段,老老实实遵守原则,能少踩很多坑。
好了,模块化设计的三个核心原则就聊到这里。记住:好的架构不是设计出来的,是不断重构出来的。这三个原则,就是你重构时的指路明灯。
公众号:蓝海数据掘金营,微信deep3321