恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C++过滤器模式实战:从“开卷有IF”到组合过滤重构烂代码

  • 首页
  • 资讯中心
  • /
  • C++过滤器模式实战:从“开卷有IF”到组合过滤重构烂代码

相关资讯

气动搅拌机定制厂家怎么选?盐城策途精密制造厂家省心可靠 2026/10/11 9:02:30
镀锌桥架采购常见问题解答 新明电气 大厂直供 降低采购成本 2026/10/11 9:02:30
我开始用 AI 辅助测试后,真正改变的不是写用例 2026/10/11 8:57:29

最新资讯

Vibe Coding 实战:把 Claude Code 的 settings 改到 TaoToken 免费体验 AI 编程
架构深潜:无后端纯前端刷题系统 IELTS Atlas 的五层设计全解析
Claude Code实战:AI编程Agent如何像资深工程师一样完成复杂代码重构
微信数据库解析工具全解:定位、解密、导出与挖掘
红外航拍人车识别数据集构建与模型适配指南
别再把 OS 当作 AI 的计算机了!解构下一代声明式 Agent Infra

今日推荐

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

C++过滤器模式实战:从“开卷有IF”到组合过滤重构烂代码

发布时间:2026/10/11 9:02:30
C++过滤器模式实战:从“开卷有IF”到组合过滤重构烂代码 从开卷有IF到组合过滤我如何用C过滤器模式重构了一堆烂代码如果你写过一段时间C大概率会经历这么一幕某个核心模块里一个函数动辄几百行里面全是if嵌套每加一条业务规则就往里塞一个if改到后来没人敢动那个文件。我前两年接手一个日志分析项目时就是这么个状况几十个过滤条件全堆在一个循环里看着能跑但加需求、做复用的成本高得吓人。后来我把这套逻辑整体拆成C的过滤器模式才真正体会到组合优于继承这句话在手写代码里的分量。这篇文章就聊聊我在实际项目里是怎么分析和落地过滤器模式的。不聊虚的会直接给你能编译运行的代码示例讲清楚设计取舍、性能代价还有我踩过的几个坑。如果你在准备C面试或者正在做数据清洗、日志过滤、游戏实体筛选这类需求这篇的内容应该能直接参考。1. 过滤器模式到底解决什么问题1.1 一个真实存在的灾难代码先还原一下没重构之前的样子。当时的需求是过滤一批日志记录大概有这些条件日志级别至少要达到INFO、消息里不能带密码字段、来源模块黑名单要排除、时间窗口要落在最近半小时内、消息长度不能超过200字节。刚开始写很直接for循环套if一条条往上加for (const auto log : rawLogs) { if (log.level LogLevel::INFO) continue; if (log.message.find(password) ! std::string::npos) continue; if (blockedModules.count(log.module) 0) continue; if (log.timestamp startTime || log.timestamp endTime) continue; if (log.message.size() 200) continue; // 每个if后面都可能跟着新的业务逻辑 result.push_back(log); }这代码第一眼还行简单直接。但三个月后需求一变就露馅了产品说日志级别要区分DEBUG和INFO两套逻辑安全组说要增加敏感词列表测试那边让把过滤过程做成可配置的。于是这个循环越写越肥if里套if还出现了一些临时开关变量来跳过某些条件。最难受的是这些逻辑完全没法单独测试你没办法只测关键字过滤而不用把整个循环跑一遍。当时我把这种代码命名为开卷有IF意思是打开这个循环就像翻开一本无限续写的烂小说。1.2 过滤器模式的定义和适用场景业界定义过滤器模式Filter Pattern时说的是让过滤逻辑能够以标准化的接口形式定义然后将多个过滤器串联成链数据依次经过链上的每个节点被任意节点拦截就丢弃全部通过则保留。这个定义听起来简单但真正有价值的点在于它把判断条件从业务循环里抽离成了独立对象。什么时候该用我经验里主要有三类场景。第一类是类似上述日志系统的多重过滤条件彼此独立、经常增删需要可配置可组合。第二类是数据校验管线比如一个输入数据要过非空校验、格式校验、长度校验、规则校验每步校验失败要给出不同的错误码。第三类是游戏里的实体筛选比如要找出敌对阵营、生命值低于阈值、在视野范围内且不是召唤物的单位这种多条件组合在游戏逻辑里极其常见热搜词里那个c游戏其实经常踩这种需求。不过这模式也不是银弹。如果你的过滤条件只有两三个、且永远不会变直接写在一处反而更清晰。过滤器模式的价值在变化和复用上不在省代码上。这一点很多初学者会理解偏一上来就套一堆类和接口反而得不偿失。1.3 和相近模式的边界划分面试里经常有人把过滤器模式、责任链模式、策略模式搞混这里我按自己的理解给个干净的分野。责任链模式Chain of Responsibility的核心是每个处理器决定是否继续传递它允许某个节点处理完后终止链条或者把请求交给下一个节点。它和过滤链在结构上很像但语义侧重不同责任链里通常有一个最终处理者重点是谁能处理这件事过滤器模式里没有处理者只有是否放行重点是哪些数据能通过。策略模式Strategy则是针对算法的替换比如排序策略、压缩策略。它和过滤器模式最根本的区别是策略模式最终要产出一个结果过滤器模式只是判断过还是不过。你可以把过滤器理解为返回布尔值的策略但整体设计意图和组合方式都不一样。装饰器模式Decorator也常被拿来对比。装饰器是在保持接口不变的前提下给对象动态添加职责它是包一层过滤器是给数据流加关卡它是串一段。过滤链更像管道上的阀门顺序很重要装饰器一般顺序没那么敏感。理清这些区别后你在设计命名和职责划分时就不容易跑偏。2. 接口设计与三种实现方式2.1 最精简的接口设计虚函数怎么定设计过滤器模式的第一步是定义过滤器的抽象接口。我见过一些团队把接口设计得很复杂传入可变对象、传出过滤结果、还要带上下文参数。实际用下来最朴素、最好用的接口就是这样一个纯虚函数class IFilter { public: virtual ~IFilter() default; // 返回 true 表示通过false 表示拦截 virtual bool pass(const LogRecord record) const 0; };关键设计决策有两个。第一个决策是返回值语义我强烈建议用bool pass()而不是bool filter()或者bool reject()。因为调用方看到if (filter-pass(record))的代码时思维方式是通过就留下这符合管道直觉。如果用filter命名很容易出现在循环里连写三个!的悲剧。第二个决策是传入参数用const T过滤逻辑不该修改原始数据这个约束在接口层级就定死能避免后面有人在过滤器里偷偷改数据。还有一个小细节析构函数要写成虚的virtual ~IFilter() default;。这条在C里属于老生常谈但确实经常被忽略尤其是当过滤器实现类里持有unique_ptr资源时非虚析构会导致子类资源无法释放属于静默内存泄漏。在VSCode里用C插件写这类代码时开一下clang-tidy的virtual-destructor检查项能提前拦住这个问题。2.2 组合过滤器的两种方式拉链与接链接口定好后怎么把多个过滤器组合起来我见到过两种主流写法一种叫拉链式在FilterChain内部用一个容器装所有过滤器通过时遍历全部另一种叫接链式每个过滤器内部持有下一个过滤器的指针像链表一样串起来。拉链式的代码大致是这个样子class FilterChain { public: void add(std::unique_ptrIFilter filter) { filters_.push_back(std::move(filter)); } bool pass(const LogRecord record) const { for (const auto f : filters_) { if (!f-pass(record)) { return false; // 任意一个不过整体拦截 } } return true; } private: std::vectorstd::unique_ptrIFilter filters_; };这种写法的优点是容器可遍历、可动态增删、方便调试。接链式的优点是节点可以独立成链不一定要集中注册但缺点是增删一个中间节点时需要调整前后指针上面说到的责任链模式通常才用这种结构。我的实践结论是做数据过滤场景优先用拉链式做处理链场景才考虑接链式。原因很简单过滤器的需求天然是先注册、后统一执行拉链式最贴合。2.3 用std::function轻量化现代C的正确打开方式上面的接口设计是纯面向对象的思路适合需要多态、需要动态配置的场景。但如果你只是想让代码更整洁不一定非要建一堆类C11之后的std::function配合lambda能给出一个轻量级方案#include functional #include vector using FilterFunc std::functionbool(const LogRecord); class FilterChain { public: void add(FilterFunc f) { filters_.push_back(std::move(f)); } bool pass(const LogRecord record) const { for (const auto f : filters_) { if (!f(record)) return false; } return true; } private: std::vectorFilterFunc filters_; };这段代码把过滤器从抽象类压缩成了一个函数对象。使用的时候FilterChain chain; chain.add([](const LogRecord r) { return r.level LogLevel::INFO; }); chain.add([](const LogRecord r) { return r.message.find(password) std::string::npos; });这个方案的优点是极其轻量不需要为每个过滤器单独建类非常适合过滤器逻辑较短、不需要复用的场景。代价是lambda的闭包状态不透明、不好从配置文件反序列化类型信息也丢失了。我的建议是小项目、一次性项目、原型验证阶段用std::function大项目、过滤器本身有内部状态或者需要配置驱动时用抽象类。两者也可以混用——抽象类实现复杂过滤器std::function适配器包装简单逻辑塞进同一条链。这里有个容易踩的坑lambda捕获引用时要特别小心悬空引用。比如你捕获了一个局部变量std::string keyword然后这个变量在注册后被销毁了那后面执行过滤器时就变成读野指针。C不像Java有GClambda捕获[]要谨慎尽量按值捕获或者确保被捕获对象生命周期覆盖整个链的使用周期。3. 实战把日志过滤链从开卷有IF重构为过滤器模式3.1 需求拆解过滤器粒度怎么切回到我最开始说的日志系统。重构之前那堆if里其实藏着一件很重要的事哪些条件应该合并成一个过滤器哪些应该拆开这是我反思很久之后觉得最有价值的部分。我当时做了一个粗暴但好用的划分标准每个过滤器只回答一个问题。比如日志级别够不够是一个问题消息里有没有敏感词是另一个问题来源模块在不在黑名单是第三个问题。有的条件看着像一个问题实际是两个消息长度不能超过200 消息不能为空就得拆成两个因为空消息长度也是0但语义不同。我还把过滤器分成了三类基础过滤永远启用、配置过滤后台开关控制、临时过滤排查问题时手动加上。这个分类直接决定了后面容器怎么管理。每个过滤器类只需要干一件事测试时就能单独拉出来验证。比如针对PasswordFilter给它构造几组包含password和不包含password的日志记录单元测试一目了然。3.2 手写核心代码抽象类过滤器的完整实现下面是我重构后的核心代码直接展示了抽象类方案的全貌。先定义日志记录结构体和抽象过滤器接口#include string #include vector #include memory #include unordered_set #include iostream #include chrono enum class LogLevel { DEBUG, INFO, WARN, ERROR }; struct LogRecord { LogLevel level; std::string message; std::string module; std::chrono::system_clock::time_point timestamp; }; class IFilter { public: virtual ~IFilter() default; virtual bool pass(const LogRecord record) const 0; };然后实现三个具体的过滤器class LevelFilter : public IFilter { public: explicit LevelFilter(LogLevel minLevel) : minLevel_(minLevel) {} bool pass(const LogRecord record) const override { return record.level minLevel_; } private: LogLevel minLevel_; }; class SensitiveWordFilter : public IFilter { public: explicit SensitiveWordFilter(std::unordered_setstd::string words) : words_(std::move(words)) {} bool pass(const LogRecord record) const override { for (const auto word : words_) { if (record.message.find(word) ! std::string::npos) { return false; } } return true; } private: std::unordered_setstd::string words_; }; class TimeWindowFilter : public IFilter { public: TimeWindowFilter(std::chrono::system_clock::time_point start, std::chrono::system_clock::time_point end) : start_(start), end_(end) {} bool pass(const LogRecord record) const override { return record.timestamp start_ record.timestamp end_; } private: std::chrono::system_clock::time_point start_; std::chrono::system_clock::time_point end_; };改造后的使用方式变成了这样FilterChain chain; chain.add(std::make_uniqueLevelFilter(LogLevel::INFO)); chain.add(std::make_uniqueSensitiveWordFilter( std::unordered_setstd::string{password, token, secret})); chain.add(std::make_uniqueTimeWindowFilter(beginTime, endTime)); for (const auto record : rawLogs) { if (chain.pass(record)) { result.push_back(record); } }从结构上看原来的一组if变成了一组对象但真正的收益在后面新增过滤条件时只需要继承IFilter写一个新类然后chain.add(...)一行代码接上原来那个大循环一个字符都不用动。如果你觉得为一个LevelFilter单独建文件太麻烦也可以在CPP文件里合并实现几个小类C不限制一个文件只能有一个类别为了纯粹的OO洁癖难为自己。3.3 重构后的验证与调试技巧重构完以后的第一件事不是上线而是验证行为不变。我把原来那堆if的所有规则列成一个表格然后为每个规则构造正反两套测试数据过滤器应放行的样例应拦截的样例LevelFilterINFO级别日志DEBUG级别日志SensitiveWordFilter不含敏感词的日志包含password的日志TimeWindowFilter时间在窗口内的日志时间在窗口外的日志这里有个调试技巧我会给每个过滤器加一个name()接口在链的遍历里输出哪个过滤器拦截了哪条日志。不然后续上线后你会遇到一个很尴尬的问题一条日志被吞掉了但完全不知道是哪个环节干的。加上名字后排查直接变成看一条调试日志的事。另一个我踩过的坑是过滤器的执行顺序。比如SensitiveWordFilter里如果包含了非常耗时的字符串扫描而LevelFilter只要一次比较就能拦截大部分低等级日志那应该把LevelFilter放在链的最前面让那些注定被淘汰的日志尽早离场。这种前面放轻量级过滤器后面放重量级过滤器的排列原则在新手手里经常被忽略等数据量涨到百万级时性能差距会非常明显。3.4 性能分析多态调用到底贵不贵使用接口类的方案引入了一次虚函数调用很多C开发者第一反应是性能会不会变差。我在这多说一点。一次虚函数调用的代价通常只是多一次间接跳转和几次CPU分支预测失败带来的惩罚量级大概在几纳秒。对于日志过滤这种IO密集型场景这点开销完全可以忽略。但如果你真的在做一个高性能过滤场景每微秒都要抠可以考虑两种优化。第一种是移除虚函数改用模板策略template typename... Filters bool passAll(const LogRecord record, Filters... filters) { return (filters.pass(record) ...); // C17折叠表达式 }这段代码会把逻辑内联展开彻底消除虚调用代价是过滤器类型必须在编译期固定不能动态配置。第二种优化是在过滤链前面增加一个粗粒度的快速判定比如先用枚举值做一次位运算的粗略筛选通过后再进入虚函数链。我在日志系统里就加过一层级别位图快速路径效果立竿见影。不过还是那句话先测性能别猜瓶颈别一上来就为了不算瓶颈的地方牺牲可维护性。4. 进阶玩法过滤器模式在游戏与数据管线中的组合姿势4.1 游戏实体过滤技能伤害如何同时判断多个目标条件热搜词里有个c游戏游戏逻辑里过滤器模式其实特别常见。举个典型的例子一个AOE技能要选择目标这时你可能要过滤出己方或者敌方阵营、存活状态、距离技能中心小于半径、不是召唤物、且当前没有无敌Buff等单位。如果用过滤器模式每个判断就是一个独立过滤器类FactionFilter、AliveFilter、RangeFilter、SummonFilter、BuffFilter。技能系统把这些过滤器组合成一条目标选取链class TargetSelector { public: void addTargetFilter(std::unique_ptrIFilter filter); std::vectorEntity* select(const std::vectorEntity* allEntities) const { std::vectorEntity* result; for (auto* entity : allEntities) { if (chain_.pass(*entity)) { result.push_back(entity); } } return result; } private: FilterChain chain_; };这个设计的好处在于策划加Buff、加新阵营、加新技能效果时程序员不需要去改select()这个循环只需要注册一个新过滤器。你甚至可以把过滤器组合存成一个Json序列化配置技能表配一串过滤器ID运行时动态装配。这在项目后期维护中能省下大量沟通成本。跟我合作的策划一开始不懂什么叫接口我就打个比方过滤器就是安检口你只需要告诉玩家哪些是违禁品拦住它们的提示由我搞定这么一说需求方反而很容易对齐。4.2 数据清洗管线把注意力和校验逻辑分离除了日志和游戏数据清洗管线也是过滤器模式的经典主场。假设你在写一个用户注册接口前端传过来的数据要先做合法性校验典型规则有用户名长度4到16位、密码至少8位且包含大小写字母、邮箱格式合法、手机号符合运营商号段。用过滤器模式后校验节点可以是class UserNameFilter : public IFilter { // 4~16位用户名 }; class PasswordFilter : public IFilter { // 包含大小写字母和数字长度≥8 }; class EmailFormatFilter : public IFilter { // 正则匹配邮箱格式 };一旦某个过滤器不通过你可以在链里拿到第3个节点拦截从而映射出对应的业务错误码。如果你用bool pass接口错误码就丢了这里就需要做一个小扩展。我在实践中会把接口升级为返回一个枚举或者error codeenum class FilterResult { Pass, Reject }; struct FilterDecision { FilterResult result; int errorCode; }; class IValidator { public: virtual FilterDecision validate(const SignupData data) const 0; };错误信息跟着过滤器走谁拦截谁负责说明原因。这种设计非常符合单一职责原则服务端接口返回给前端的错误提示也能做到精准对应。不过要注意大多数业务系统过滤链是门卫式的一条不过就不继续了而数据清洗有时是手术式的过滤节点要能修正数据而不是简单丢弃那种情况更适合用管道模式而不是过滤模式这也是我之前编码时反复纠结的分界线。4.3 并发过滤链的处理思路如果数据是并行流进来的过滤链的并发安全性也要想清楚。我的建议是只读的过滤器集合可以无锁共享过滤器和过滤链在注册完成后视为只读多线程各自调用pass()即可。真正需要加锁的环节是动态增删过滤器和正在执行的过滤链遍历之间的竞争这种情况用读写锁或者copy-on-write方案比较稳妥。我碰到过一个真实的并发问题后台配置界面更新过滤器列表时线上正在遍历过滤链结果vector边遍历边修改直接崩溃了。后来改成变更时复制一份新链通过atomic指针整体替换彻底绕开了锁竞争。这里的关键思想是配置数据和执行数据分离这跟配置热更新的常见套路一致。如果你用std::function版本且lambda捕获了可变状态那并发安全就要自己负责了因为lambda闭包内部的成员变量同步完全不在过滤器框架的管控范围内。5. 常见问题与避坑记录5.1 过滤链上的典型问题速查表在实际项目里过滤器模式翻车往往不是模式本身的问题而是使用姿势出了问题。我整理了开发过程中遇到过的几个问题做成一张表方便排查现象根因解决方案某条日志始终被拦截但所有过滤器单独测试都通过过滤器链中存在短路逻辑顺序不对对所有过滤器打日志输出每个节点的通过情况链中新增过滤器后原有行为被改变过滤器顺序依赖之前的隐式逻辑在链的开始处统一说明顺序敏感并抽象出标准优先级lambda捕获了局部变量后悬空捕获引用生命周期未管理按值捕获或使用std::shared_ptr延长生命周期虚析构缺失导致资源泄漏基类析构函数没写成虚析构基类加virtual ~IFilter() default;大量过滤后性能下降明显重量级过滤器排在链前面优先排列低成本判断或者加快速淘汰层配置热更新时崩溃动态修改vector与遍历并发冲突copy-on-write atomic指针替换5.2 不要在一个过滤器里塞太多逻辑我见过最典型的反模式是有人把好几个过滤条件写进同一个过滤器类美其名曰减少对象数量。结果这个过滤器类变成了一堆bool函数的聚合器参数列表越长越长测试用例越写越痛苦。这种写法本质上只是把原来循环里的if搬了个家并没有获得可组合性。我自己的约束是一个过滤器类内部逻辑不超过5行核心判断。一旦超过就拆。因为过滤器类的价值在于单点可测、可替换、可复用如果一个类内部有5个条件你复用它的时候没办法只复用其中两个。拆得细一些组合的灵活性才会上来。5.3 面试时怎么聊过滤器模式才有深度如果你在准备C面试题过滤器模式本身可能不是大考点但它是引出很多话题的好引子。面试官问你用过哪些设计模式时你提到过滤器模式并且能说清楚它和责任链模式的区别这就已经比背八股的候选人高半档了。更进一步的加分点是你能指出过滤器模式在使用中的性能考量和C特有实现。比如我用std::function做了轻量实现但在性能敏感路径上我换成了策略模板配合折叠表达式来消除虚调用。这种面向场景选方案的表述恰恰是区分背答案和真做过的分界线。我当年面试时被问到一个实际场景如果过滤链有几十个过滤器而大部分日志在第一层就被拦截你会怎么优化这其实考的是我前面说的权重排序和快速淘汰层实践经验比几百道八股管用得多。5.4 环境配置与编译器版本提醒聊到C避免不了环境。热搜词里能看到很多人搜vscode配置c/c环境、microsoft visual c redistributable这类词说明不少读者还在起步阶段。提醒一句这篇文章里的代码用到C11到C17的特性比如std::make_unique需要C14、lambda需要C11、折叠表达式需要C17。在VSCode里开发时记得在tasks.json里的编译参数中加-stdc17否则std::make_unique会报错。如果你用Visual Studio就设置项目的C语言标准为C17或更高。不只是为了编译通过折叠表达式、结构化绑定这些语法真的能极大简化过滤器链的编写和维护值得用新标准。结束语与一个实用小技巧这篇文章越写越长回顾起来过滤器模式给我最大的启发不是那个接口怎么写而是把变化点显式地建模出来的这个思路。当你看到一段不断往里面加判断的循环时停下来想一想这个判断以后会不会变会不会被复用答案若是有那大概率就适合引入过滤器模式。在我个人实际使用中这种重构带来的安心感比代码行数的减少更珍贵——过滤器是真正的单一职责单元改一个过滤器不影响别处测试也能指哪打哪。最后再分享一个小技巧给过滤链加一个调试统计器统计每个过滤器的拦截次数。当年我就是靠这个统计器发现了一个过滤器拦截了超过60%的数据从而定位到那条太严的规则。实现起来很简单在FilterChain遍历时往一个map里累加计数即可。这个数据在你和产品、策划对齐规则合理性时特别有用几乎每次都能从数据里发现过时或者错误的过滤条件。过滤器模式本身是手段让规则可分析、可观测才是真正的目的。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号