模块化设计原则:单一职责、接口隔离、依赖倒置在回测系统中的应用

做回测系统,最怕什么?

我最怕的是——写了一个月代码,突然发现策略逻辑和数据处理搅在一起,改一个参数要翻遍十几个文件。嗯,这种痛,经历过的人都懂。

今天我们就聊聊模块化设计的三个核心原则。说白了,就是怎么让你的回测系统像乐高积木一样,拆得开、装得上、换得掉。

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 实现。引擎代码一行都不用改。

我的经验:在回测系统里,数据源、执行引擎、策略、风控——这些核心模块之间,都应该用接口来通信。这是系统能活过三年以上的关键。

三个原则的关系

这三个原则不是孤立的。它们互相配合,共同构建一个可维护的回测系统。

我画了一张图,帮你理清它们的关系:

模块化设计三大原则在回测系统中的应用 单一职责原则 一个类只做一件事 策略类 → 只生成信号 执行类 → 只负责下单 日志类 → 只记录交易 避免“万能类” 接口隔离原则 接口要小、要专 ITickHandler → 只处理Tick IBarHandler → 只处理Bar IOrderHandler → 只处理订单 避免“胖接口” 依赖倒置原则 依赖抽象,不依赖具体 引擎依赖 IDataFeed 引擎依赖 IBroker 策略依赖 IDataFeed 换实现不换逻辑 三者协同:构建可维护的回测系统 单一职责 → 每个模块职责清晰,易于理解和修改 接口隔离 → 模块间通信接口最小化,降低耦合 依赖倒置 → 高层模块不依赖低层,系统灵活可扩展 最终效果:改一个模块,不影响其他模块

从这张图可以看出:

  • 单一职责让每个模块边界清晰
  • 接口隔离让模块间的通信轻量化
  • 依赖倒置让模块可以独立替换

三者缺一不可。少了任何一个,系统都会变得脆弱。

实战中的取舍

理论说完了,聊聊实战中的取舍。毕竟,现实世界不是教科书。

什么时候可以适当放宽?

  • 原型阶段:快速验证想法时,可以暂时忽略一些原则。但记得回头重构。
  • 性能瓶颈处:比如高频回测中,过多的接口调用可能影响性能。这时可以适当内联。
  • 团队规模小:如果就你一个人写代码,可以灵活一些。但别养成坏习惯。

什么时候必须严格遵循?

  • 多人协作的项目:接口不清晰,沟通成本会指数级上升。
  • 长期维护的系统:半年后你自己都可能忘了当初为什么这么写。
  • 需要对接外部系统:比如对接不同券商、不同数据源时,依赖倒置是必须的。

我的建议:刚开始做回测系统时,先按原则来。等你有经验了,自然知道哪里可以“偷懒”。但新手阶段,老老实实遵守原则,能少踩很多坑。

好了,模块化设计的三个核心原则就聊到这里。记住:好的架构不是设计出来的,是不断重构出来的。这三个原则,就是你重构时的指路明灯。


公众号:蓝海数据掘金营,微信deep3321