目录导航
💡 开场故事
"老板让我做一个用户系统。"
"用什么思想?"
"面向对象啊!类、对象、继承、多态……"
"后来需求变了,要加审计日志、缓存、消息队列、权限校验……"
"代码越来越乱,类之间的关系像意大利面。"
——这不是你的问题,是你选的"编程世界观"不适合当前场景。换一种范式,问题可能迎刃而解。
"老板让我做一个用户系统。"
"用什么思想?"
"面向对象啊!类、对象、继承、多态……"
"后来需求变了,要加审计日志、缓存、消息队列、权限校验……"
"代码越来越乱,类之间的关系像意大利面。"
——这不是你的问题,是你选的"编程世界观"不适合当前场景。换一种范式,问题可能迎刃而解。
一、编程思想是什么?为什么要了解?
编程思想(Programming Paradigm)不是具体的框架或语言,而是看待问题和组织代码的"世界观"。就像建筑师设计房屋时可以选择"现代风格"、"古典风格"、"中式风格"——风格本身没有好坏,但不同风格适合不同场景。
🎯 阅读建议
本文每个范式都包含:
① 一句话定义 ② 生活类比 ③ C++ 代码示例 ④ 适用场景 ⑤ 与其他范式的对比
建议通读全文形成整体认识,再根据自己的项目需要深入研究感兴趣的范式。
本文每个范式都包含:
① 一句话定义 ② 生活类比 ③ C++ 代码示例 ④ 适用场景 ⑤ 与其他范式的对比
建议通读全文形成整体认识,再根据自己的项目需要深入研究感兴趣的范式。
二、面向对象编程(OOP):万物皆对象
定义:把现实世界的"事物"抽象成程序中的"对象",通过类(Class)描述对象的属性和行为。
🏠 生活类比:开一家公司
你不会把所有工作交给一个人做。你会定义"员工"类——有姓名、工号、薪资(属性),会打卡、写代码、开会(行为)。然后创建具体的员工对象:张三、李四。
还会有继承关系:"经理"继承"员工",额外有审批权;"CTO"继承"经理",额外有战略决策权。
这就是 OOP 的三大支柱:封装、继承、多态。
你不会把所有工作交给一个人做。你会定义"员工"类——有姓名、工号、薪资(属性),会打卡、写代码、开会(行为)。然后创建具体的员工对象:张三、李四。
还会有继承关系:"经理"继承"员工",额外有审批权;"CTO"继承"经理",额外有战略决策权。
这就是 OOP 的三大支柱:封装、继承、多态。
2.1 C++ 实现示例:一个简单的图形系统
#include <iostream> #include <memory> #include <vector> // ① 封装:基类 Shape 封装公共属性 area() class Shape { public: virtual ~Shape() = default; virtual double area() const = 0; // 纯虚函数 }; // ② 继承:Circle 继承 Shape class Circle : public Shape { double radius; public: explicit Circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; } }; // ② 继承:Rectangle 继承 Shape class Rectangle : public Shape { double width, height; public: Rectangle(double w, double h) : width(w), height(h) {} double area() const override { return width * height; } }; // ③ 多态:统一调用 area(),自动分发到具体子类 int main() { std::vector<std::unique_ptr<Shape>> shapes; shapes.push_back(std::make_unique<Circle>>(5.0)); shapes.push_back(std::make_unique<Rectangle>>(3.0, 4.0)); for (auto& s : shapes) { std::cout << "面积: " << s->area() << std::endl; // Circle::area() 或 Rectangle::area() 自动被调用 } return 0; }
⚠️ OOP 的常见陷阱
· 过度继承:继承链过深导致脆弱基类问题。建议继承层次 ≤ 3 层。
· 上帝类:一个类承担太多职责,违反单一职责。
· 循环依赖:类 A 依赖 B,B 又依赖 A,导致编译和维护困难。
· 口诀:"组合优于继承"——大多数时候用组合能获得更灵活的结构。
· 过度继承:继承链过深导致脆弱基类问题。建议继承层次 ≤ 3 层。
· 上帝类:一个类承担太多职责,违反单一职责。
· 循环依赖:类 A 依赖 B,B 又依赖 A,导致编译和维护困难。
· 口诀:"组合优于继承"——大多数时候用组合能获得更灵活的结构。
三、面向过程编程:按步骤来
定义:把问题分解成一系列函数(过程),按顺序调用解决。核心是"算法 + 数据结构"。
🍜 生活类比:煮一碗方便面
1. 烧开水 → 2. 放入面饼 → 3. 加入调料包 → 4. 等待 3 分钟 → 5. 关火盛碗
每一步都是一个函数,数据(面条、调料、水)在步骤间传递。没有"对象",只有"过程"。
1. 烧开水 → 2. 放入面饼 → 3. 加入调料包 → 4. 等待 3 分钟 → 5. 关火盛碗
每一步都是一个函数,数据(面条、调料、水)在步骤间传递。没有"对象",只有"过程"。
3.1 C++ 实现示例:计算学生成绩(过程式 vs 对象式对比)
// ❌ 过程式写法:数据和函数分离 struct Student { char name[32]; double scores[5]; int count; }; double calcAverage(const Student& s) { double sum = 0; for (int i = 0; i < s.count; ++i) sum += s.scores[i]; return sum / s.count; } void printStudent(const Student& s) { std::cout << s.name << " 平均分: " << calcAverage(s); } // ✅ 对象式写法:数据和行为绑定 class StudentObj { std::string name; std::vector<double> scores; public: StudentObj(const std::string& n) : name(n) {} void addScore(double s) { scores.push_back(s); } double average() const { if (scores.empty()) return 0; double sum = 0; for (auto s : scores) sum += s; return sum / scores.size(); } };
💡 过程式在现代 C++ 中仍有价值
· 算法题:LeetCode、ACM 竞赛中,过程式写法更直接
· 嵌入式/底层开发:资源受限,函数式开销更小
· 脚本工具:快速原型,一次性处理数据
· 性能敏感路径:高频调用的计算密集型代码
· 算法题:LeetCode、ACM 竞赛中,过程式写法更直接
· 嵌入式/底层开发:资源受限,函数式开销更小
· 脚本工具:快速原型,一次性处理数据
· 性能敏感路径:高频调用的计算密集型代码
四、函数式编程(FP):把函数当公民
定义:函数是一等公民——可以赋值给变量、作为参数传递、作为返回值。强调不可变性和纯函数。
🎨 生活类比:工厂流水线
想象一条数据加工流水线:原材料 → 清洗 → 切割 → 包装 → 出厂。每个环节都是一个函数,输入什么输出什么,不会修改原材料本身(不可变)。
你可以灵活组合不同的加工步骤,改变流水线顺序,新增环节——而不需要修改已有函数。
想象一条数据加工流水线:原材料 → 清洗 → 切割 → 包装 → 出厂。每个环节都是一个函数,输入什么输出什么,不会修改原材料本身(不可变)。
你可以灵活组合不同的加工步骤,改变流水线顺序,新增环节——而不需要修改已有函数。
4.1 核心概念
- 不可变性(Immutability):数据一旦创建就不能修改,而是创建新数据
- 纯函数(Pure Function):相同输入总是相同输出,无副作用
- 高阶函数(HOF):接受函数作为参数或返回函数的函数
- 闭包(Closure):函数能"记住"并访问其词法作用域
- 递归:用递归替代循环,优雅地处理层级结构
4.2 C++ 实现示例:用函数式思想处理数据
#include <iostream> #include <vector> #include <algorithm> #include <numeric> #include <functional> using namespace std; int main() { vector<int> nums = {1, 2, 3, 4, 5, 6}; // ① 高阶函数:map(转换每个元素) auto doubled = vector<int>(); transform(nums.begin(), nums.end(), back_inserter(doubled), [](int x) { return x * 2; }); // nums 不变!doubled = {2,4,6,8,10,12} // ② 过滤:filter(筛选符合条件的元素) auto evens = vector<int>(); copy_if(nums.begin(), nums.end(), back_inserter(evens), [](int x) { return x % 2 == 0; }); // evens = {2,4,6} // ③ 归约:reduce(聚合为单个值) int sum = accumulate(nums.begin(), nums.end(), 0, [](int a, int b) { return a + b; }); // sum = 21 // ④ 函数组合:创建新函数 = 双增 + 过滤 + 求和 auto doubleSum = [](const vector<int>& v) { auto d = vector<int>(); transform(v.begin(), v.end(), back_inserter(d), [](int x) { return x * 2; }); return accumulate(d.begin(), d.end(), 0); }; cout << doubleSum(nums) << endl; // 42 }
✅ FP 适用场景
· 数据处理:ETL 管线、日志分析、推荐系统
· 并发系统:不可变性天然支持多线程
· DSL 构建:用高阶函数构建领域特定语言
· React/Vue:前端框架采用"不可变数据 + 纯渲染"思想
· 数据处理:ETL 管线、日志分析、推荐系统
· 并发系统:不可变性天然支持多线程
· DSL 构建:用高阶函数构建领域特定语言
· React/Vue:前端框架采用"不可变数据 + 纯渲染"思想
五、事件驱动编程:发布-订阅的艺术
定义:系统由事件驱动——当某事件发生时,自动触发注册的处理器。核心是发布-订阅模式。
📢 生活类比:广播电台
电台(发布者)只管播音,不关心谁在听。听众(订阅者)选择自己感兴趣的频道。有新节目时,电台广播,所有订阅者同时收到。
电台和听众完全解耦——电台不需要知道听众是谁、在哪里。
电台(发布者)只管播音,不关心谁在听。听众(订阅者)选择自己感兴趣的频道。有新节目时,电台广播,所有订阅者同时收到。
电台和听众完全解耦——电台不需要知道听众是谁、在哪里。
5.1 C++ 实现示例:简易事件总线
#include <iostream> #include <functional> #include <unordered_map> #include <vector> #include <string> class EventBus { using Handler = std::function<void(const std::string&)>; std::unordered_map<std::string, std::vector<Handler>> subscribers; public: // 订阅事件 void subscribe(const std::string& event, Handler handler) { subscribers[event].push_back(std::move(handler)); } // 发布事件 void publish(const std::string& event, const std::string& data) { auto it = subscribers.find(event); if (it != subscribers.end) { for (auto& handler : it->second) { handler(data); } } } }; int main() { EventBus bus; // 订阅日志事件 bus.subscribe("log", [](const std::string& msg) { std::cout << [LOG] " << msg << std::endl; }); // 订阅告警事件 bus.subscribe("alert", [](const std::string& msg) { std::cout << [ALERT] 🚨 " << msg << std::endl; }); // 多个订阅者可以监听同一个事件 bus.subscribe("log", [](const std::string& msg) { // 发送邮件通知... std::cout << [EMAIL] 记录已归档: " << msg << std::endl; }); // 发布者不知道谁在监听 bus.publish("log", "用户登录成功"); bus.publish("alert", "磁盘空间不足"); }
💡 事件驱动在现实中的应用
· Qt 信号槽:GUI 框架的事件通知机制
· Node.js EventEmitter:服务器端异步处理
· Kafka/RabbitMQ:分布式系统的消息队列
· 浏览器 DOM 事件:click、scroll、input 等
· Qt 信号槽:GUI 框架的事件通知机制
· Node.js EventEmitter:服务器端异步处理
· Kafka/RabbitMQ:分布式系统的消息队列
· 浏览器 DOM 事件:click、scroll、input 等
六、面向切面编程(AOP):横切关注点
定义:把横切关注点(日志、权限、缓存、事务)从业务代码中分离出来,以"切面"的方式统一管理。
✂️ 生活类比:给衣服加装饰
你有 5 件不同款式的衣服,每件都需要加"口袋"。面向对象的做法是:每件衣服都继承一个"带口袋的衣服"。
AOP 的做法是:做一个"口袋模板",在穿衣服时自动套上——衣服本身不需要知道自己有口袋。
业务代码 = 衣服;日志/权限 = 口袋;切面 = 口袋模板。
你有 5 件不同款式的衣服,每件都需要加"口袋"。面向对象的做法是:每件衣服都继承一个"带口袋的衣服"。
AOP 的做法是:做一个"口袋模板",在穿衣服时自动套上——衣服本身不需要知道自己有口袋。
业务代码 = 衣服;日志/权限 = 口袋;切面 = 口袋模板。
6.1 C++ 实现示例:用装饰器/模板方法模拟 AOP
// 传统做法:在每个业务方法里手动加日志(代码污染) void UserService::createUser() { qDebug() << "[LOG] createUser 开始"; // 日志代码 if (!checkPermission("admin")) { // 权限代码 qDebug() << "[LOG] 权限不足"; return; } // === 真正的业务逻辑 === userRepo->save(user); // === 业务逻辑结束 === qDebug() << "[LOG] createUser 结束"; // 日志代码 } // ✅ AOP 思想:用模板方法分离关注点 template<typename Func> void withLogging(const std::string& name, Func&& func) { qDebug() << "[LOG] " << name << " 开始"; try { func(); // 执行真正的业务逻辑 qDebug() << "[LOG] " << name << " 结束 ✅"; } catch (const std::exception& e) { qDebug() << "[LOG] " << name << " 失败 ❌: " << e.what(); throw; } } // 使用:业务代码干净,日志自动织入 withLogging("createUser", []() { // 只有真正的业务逻辑,没有日志、权限等横切代码 userRepo->save(user); sendWelcomeEmail(user); });
七、响应式编程:数据流的交响
定义:面向异步数据流的编程范式——数据是持续流动的,代码对数据流的变化做出响应。核心是观察者模式 + 函数式管道。
📺 生活类比:直播弹幕
想象你在看直播,屏幕上不断滚动弹幕(数据流)。你不需要主动刷新页面(轮询),而是弹幕来了就自动显示(响应)。
还可以过滤"只看 VIP 弹幕"、合并"显示前 10 条热评"、转换"翻译成英文"——所有操作都是数据流上的管道。
想象你在看直播,屏幕上不断滚动弹幕(数据流)。你不需要主动刷新页面(轮询),而是弹幕来了就自动显示(响应)。
还可以过滤"只看 VIP 弹幕"、合并"显示前 10 条热评"、转换"翻译成英文"——所有操作都是数据流上的管道。
7.1 核心概念
- Observable:数据源,可以是事件、定时器、HTTP 请求、集合等
- Operator:对数据流的操作——map、filter、merge、debounce 等
- Subscriber:订阅者,定义数据到达时的行为
- Scheduler:控制线程,支持异步和并发
7.2 C++ 实现示例:用 range-v3 模拟响应式操作
#include <iostream> #include <vector> #include <chrono> #include <thread> template<typename Func> class SimpleObservable { std::vector<Func> subscribers; public: void subscribe(Func f) { subscribers.push_back(std::move(f)); } void emit(int value) { for (auto& s : subscribers) s(value); } }; int main() { SimpleObservable<std::function<void(int)>> sensor; // 管道:filter(>50) → map(×2) → 输出 sensor.subscribe([](int v) { if (v > 50) { // filter int doubled = v * 2; // map std::cout << [REACTIVE] 传感器值: << doubled << std::endl; } }); // 模拟传感器每 100ms 产生一个读数 for (int i = 0; i < 20; ++i) { int reading = 30 + i * 5; // 30, 35, 40, ... sensor.emit(reading); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } }
💡 响应式编程的典型应用
· RxJava/RxJS:Android/iOS/Web 异步编程
· Spring WebFlux:响应式 Web 框架
· Kotlin Flow:协程数据流
· 实时数据仪表盘:股票行情、IoT 传感器监控
· RxJava/RxJS:Android/iOS/Web 异步编程
· Spring WebFlux:响应式 Web 框架
· Kotlin Flow:协程数据流
· 实时数据仪表盘:股票行情、IoT 传感器监控
八、声明式编程:描述"做什么"而非"怎么做"
定义:只描述目标(做什么),不描述步骤(怎么做)。由底层引擎决定最优执行路径。
🗺️ 生活类比:导航
命令式:从家出发→左转→300米→右转→500米→到达目的地(描述每一步怎么走)
声明式:输入地址"天安门",导航自动规划路线(只说去哪,不说怎么走)
命令式:从家出发→左转→300米→右转→500米→到达目的地(描述每一步怎么走)
声明式:输入地址"天安门",导航自动规划路线(只说去哪,不说怎么走)
8.1 对比:命令式 SQL vs 声明式 SQL
// ❌ 命令式:用 C++ 写查询逻辑 vector<Employee> result; for (auto& emp : allEmployees) { if (emp.department == "IT" && emp.salary > 10000) { bool found = false; for (auto& proj : emp.projects) { if (proj.status == "active") { found = true; break; } } if (found) result.push_back(emp); } } // ✅ 声明式:SQL 只描述"要什么",不写"怎么找" // SELECT * FROM employees // WHERE department = 'IT' // AND salary > 10000 // AND EXISTS (SELECT 1 FROM projects // WHERE projects.emp_id = employees.id // AND status = 'active');
✅ 声明式编程的优势
· 代码简洁:SQL 一行等价于 20+ 行 C++
· 引擎优化:数据库引擎自动选择索引、排序策略
· 无需关心实现:从"如何实现"解放出来,聚焦业务逻辑
· 语言示例:SQL、HTML/CSS、正则表达式、Prolog、Datalog
· 代码简洁:SQL 一行等价于 20+ 行 C++
· 引擎优化:数据库引擎自动选择索引、排序策略
· 无需关心实现:从"如何实现"解放出来,聚焦业务逻辑
· 语言示例:SQL、HTML/CSS、正则表达式、Prolog、Datalog
8.2 C++ 中的声明式思想:Range-based for & STL 算法
// 声明式思想在 C++ 的体现:用 STL 算法描述意图 vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6}; // "找出所有偶数并排序"——用 STL 算法描述意图 vector<int> evens; copy_if(nums.begin(), nums.end(), back_inserter(evens), [](int x) { return x % 2 == 0; }); sort(evens.begin(), evens.end); // 不需要写循环,不需要写排序算法 // 只声明"我要偶数" + "我要排序"
九、领域驱动设计(DDD):业务即代码
定义:以业务领域为核心建模,代码结构与业务语言保持一致。核心是统一语言(Ubiquitous Language)和限界上下文(Bounded Context)。
🏗️ 生活类比:建筑师与图纸
传统做法:先选材料(技术栈),再画图纸(设计代码),最后建房子。
DDD 做法:先理解客户的业务需求(造一个银行/电商/物流系统),用业务语言画图纸,最后选材料。
图纸(模型)必须和业务人员说的话一一对应——代码里的 Order、Customer、Payment 就是业务里的订单、客户、支付。
传统做法:先选材料(技术栈),再画图纸(设计代码),最后建房子。
DDD 做法:先理解客户的业务需求(造一个银行/电商/物流系统),用业务语言画图纸,最后选材料。
图纸(模型)必须和业务人员说的话一一对应——代码里的 Order、Customer、Payment 就是业务里的订单、客户、支付。
9.1 DDD 的核心概念
- 实体(Entity):有唯一 ID 的业务对象,如 Order、Customer
- 值对象(Value Object):无 ID,只关心值,如 Money、Address、DateRange
- 聚合根(Aggregate Root):实体的容器,保证内部一致性,如 Order(包含 OrderItem)
- 领域服务(Domain Service):跨聚合的业务逻辑,如 TransferService
- 领域事件(Domain Event):业务中发生的重要事情,如 OrderPlaced、PaymentReceived
- 仓储(Repository):数据持久化的抽象,如 IOrderRepository
9.2 C++ 实现示例:订单聚合根
// 值对象:Money 不可变,自带校验 class Money { double amount; std::string currency; public: Money(double amt, const std::string& cur) : amount(amt), currency(cur) { if (amt < 0) throw std::invalid_argument("金额不能为负"); } bool operator==(const Money& o) const { return amount == o.amount && currency == o.currency; } }; // 实体:订单明细 class OrderItem { int productId; int quantity; Money unitPrice; public: Money subtotal() const { return Money(quantity * unitPriceamount(), unitPricecurrency()); } }; // 聚合根:Order 保证内部一致性 class Order { int orderId; std::vector<OrderItem> items; enum class Status { Pending, Paid, Shipped, Cancelled }; Status status; public: void pay() { if (status != Status::Pending) throw std::runtime_error("订单只能在待支付状态下支付"); status = Status::Paid; // 发布领域事件 DomainEvents::publish(OrderPaid{orderId}); } Money totalAmount() const { double total = 0; for (auto& item : items) total += item.subtotal()amount(); return Money(total, "CNY"); } };
🎯 DDD 适用场景
· 复杂业务系统:银行、电商、ERP、物流等核心领域
· 业务规则多变:需求频繁变化,需要代码与业务保持一致
· 限界上下文:大型系统按业务边界拆分,每个上下文独立建模
· 注意:简单 CRUD 系统不需要 DDD,过犹不及
· 复杂业务系统:银行、电商、ERP、物流等核心领域
· 业务规则多变:需求频繁变化,需要代码与业务保持一致
· 限界上下文:大型系统按业务边界拆分,每个上下文独立建模
· 注意:简单 CRUD 系统不需要 DDD,过犹不及
十、面向接口编程:抽象即稳定
定义:面向接口编程(Interface-Oriented Programming)是一种强调先定义抽象契约(接口),再实现具体类的编程思想。核心是"针对接口编程,而不是针对实现编程"。
🔌 生活类比:电器与插座
你买电器时,只需要看"这个电器是三头插头还是两头插头"(接口),不需要关心电器内部是怎么工作的(实现)。只要插头标准统一(接口定义),任何品牌的电器都能插上去用。
更换电器时(替换实现),不需要更换插座(接口)。
你买电器时,只需要看"这个电器是三头插头还是两头插头"(接口),不需要关心电器内部是怎么工作的(实现)。只要插头标准统一(接口定义),任何品牌的电器都能插上去用。
更换电器时(替换实现),不需要更换插座(接口)。
10.1 核心原则
- 先有接口,再有实现:设计时先定义抽象契约,后续再填充实现
- 依赖倒置:高层模块依赖接口,不依赖具体实现
- 里氏替换:任何实现类都可以替换接口使用而不破坏系统
- 最小接口:接口应该尽可能小,只暴露必要的方法
10.2 C++ 实现示例:日志系统
// ① 先定义接口(抽象契约) class ILogger { public: virtual ~ILogger() = default; virtual void log(const std::string& msg) = 0; virtual void error(const std::string& msg) = 0; }; // ② 不同实现:控制台、文件、远程 class ConsoleLogger : public ILogger { void log(const std::string& msg) override { std::cout << [LOG] " << msg << std::endl; } void error(const std::string& msg) override { std::cerr << [ERROR] " << msg << std::endl; } }; class FileLogger : public ILogger { std::ofstream file; public: explicit FileLogger(const std::string&&path) : file(path, std::ios::app) {} void log(const std::string& msg) override { file << [LOG] " << msg << std::endl; } void error(const std::string& msg) override { file << [ERROR] " << msg << std::endl; } }; // ③ 使用者依赖接口,不依赖实现 class OrderService { std::shared_ptr<ILogger> logger; // 依赖接口 public: explicit OrderService(std::shared_ptr<ILogger> l) : logger(std::move(l)) {} void createOrder() { logger->log("Creating order..."); // 业务逻辑... logger->log("Order created"); } };
✅ 面向接口的优势
· 解耦:使用者不需要知道具体实现,只需要知道接口
· 可替换:运行时可以切换实现(如从 FileLogger 切到 RemoteLogger)
· 可测试:单元测试中可以用 Mock 实现替换真实实现
· 并行开发:前端/后端可以基于接口并行开发
· 解耦:使用者不需要知道具体实现,只需要知道接口
· 可替换:运行时可以切换实现(如从 FileLogger 切到 RemoteLogger)
· 可测试:单元测试中可以用 Mock 实现替换真实实现
· 并行开发:前端/后端可以基于接口并行开发
十一、面向数据编程:数据驱动逻辑
定义:面向数据编程是将程序逻辑与数据分离——规则、配置、行为存储在数据中(而非硬编码在逻辑中),运行时根据数据驱动程序行为。
📊 生活类比:Excel 公式
你不需要为每个表格都写一个新程序。只需要在单元格里填入公式(数据),Excel 引擎(程序)就会自动计算。
改变公式(数据变更)不需要改变 Excel(程序逻辑)。
你不需要为每个表格都写一个新程序。只需要在单元格里填入公式(数据),Excel 引擎(程序)就会自动计算。
改变公式(数据变更)不需要改变 Excel(程序逻辑)。
11.1 C++ 实现示例:基于数据的规则引擎
#include <iostream> #include <vector> #include <functional> #include <string> // 数据结构:规则 = 条件 + 动作 struct Rule { std::string name; std::function<bool(int)> condition; // 条件 std::function<void(int)> action; // 动作 }; class RuleEngine { std::vector<Rule> rules; public: void addRule(Rule r) { rules.push_back(std::move(r)); } void execute(int value) { for (auto& rule : rules) { if (rule.condition(value)) { rule.action(value); } } } }; int main() { RuleEngine engine; // 规则是数据,不是硬编码的 if-else engine.addRule({"负数检测", [](int v) { return v < 0; }, [](int v) { std::cout << "❌ 负数: " << v << std::endl; } }); engine.addRule({"偶数检测", [](int v) { return v % 2 == 0; }, [](int v) { std::cout << "✨ 偶数: " << v << std::endl; } }); engine.addRule({"大数检测", [](int v) { return v > 100; }, [](int v) { std::cout << "⚠️ 大数值: " << v << std::endl; } }); // 执行时按数据驱动 engine.execute(-5); // 触发: 负数 engine.execute(42); // 触发: 偶数 engine.execute(150); // 触发: 偶数 + 大数 }
💡 面向数据编程的应用
· 游戏引擎:角色属性、技能配置存储在 XML/JSON 中
· 工作流引擎:流程定义存储在数据库中
· Spring 配置:Bean 定义在 XML/注解中
· Qt:QSS 样式表、信号槽连接可以用字符串配置
· 游戏引擎:角色属性、技能配置存储在 XML/JSON 中
· 工作流引擎:流程定义存储在数据库中
· Spring 配置:Bean 定义在 XML/注解中
· Qt:QSS 样式表、信号槽连接可以用字符串配置
十二、元编程:代码即数据
定义:元编程是"编写编写程序的程序"——代码在编译期或运行期检查和修改程序自身的结构,生成或变换代码。
🤖 生活类比:工厂生产机器
普通编程:你是工人,手工组装每个产品。
元编程:你制造一台"生产机器",机器自动组装产品。
编写元程序 = 编写"生产机器",而非手工生产每件产品。
普通编程:你是工人,手工组装每个产品。
元编程:你制造一台"生产机器",机器自动组装产品。
编写元程序 = 编写"生产机器",而非手工生产每件产品。
12.1 C++ 中的元编程:模板元编程(TMP)
// 编译期计算阶乘——经典模板元编程 template<int N> struct Factorial { static constexpr int value = N * Factorial<N - 1>::value; }; template<> struct Factorial<0> { static constexpr int value = 1; }; // 编译期就确定了结果 constexpr int result = Factorial<5>::value; // 120,编译器就算出了 // type_traits:编译期类型检查 #include <type_traits> template<typename T> auto process(T value) -> std::enable_if_t<std::is_arithmetic_v<T>> { return value * 2; // 只有算术类型才能调用 } // 反射元编程:C++17 的 std::variant + 访问器 #include <variant> using Value = std::variant<int, double, std::string>; struct Printer { void operator()(int v) { std::cout << "int: " << v; } void operator()(double v) { std::cout << "double: " << v; } void operator()(const std::string& v) { std::cout << "string: " << v; } }; Value v = "hello"; std::visit(Printer{}, v); // 运行期自动分派
⚠️ 元编程的双刃剑
· 优势:编译期计算 → 运行期零开销;类型安全
· 劣势:错误信息晦涩难懂;编译时间长;调试困难
· 建议:C++ 元编程用于库开发(如 STL、Qt),应用层谨慎使用
· 优势:编译期计算 → 运行期零开销;类型安全
· 劣势:错误信息晦涩难懂;编译时间长;调试困难
· 建议:C++ 元编程用于库开发(如 STL、Qt),应用层谨慎使用
十三、防御式编程:假设最坏,准备最好
定义:防御式编程是一种假设"一切都可能出错"的编程态度——通过前置条件检查、异常处理、断言、边界检查等手段,在错误发生时优雅降级而非崩溃。
🛡️ 生活类比:汽车安全系统
防御式编程就像汽车的安全设计:
· 安全带(边界检查):防止在极端情况下受伤
· 气囊(异常处理):事故发生时保护乘客
· ABS(断言):防止系统失控
· 备胎(容错):即使一个轮胎坏了还能继续行驶
防御式编程就像汽车的安全设计:
· 安全带(边界检查):防止在极端情况下受伤
· 气囊(异常处理):事故发生时保护乘客
· ABS(断言):防止系统失控
· 备胎(容错):即使一个轮胎坏了还能继续行驶
13.1 C++ 实现示例:防御式编程检查清单
#include <iostream> #include <stdexcept> #include <cassert> class SafeArray { std::vector<int> data; public: // ① 防御 1:参数校验(前置条件) void setAt(int index, int value) { if (index < 0 || index >= static_cast<int>(data.size())) { throw std::out_of_range("Index out of bounds"); } data[index] = value; } // ② 防御 2:返回值检查 int getAt(int index) const { if (index < 0 || index >= static_cast<int>(data.size())) { return -1; // 优雅降级 } return data[index]; } // ③ 防御 3:断言(开发期检查) void invariantCheck() const { assert(data.size() >= 0); // 永远成立的不变量 } }; // ④ 防御 4:异常安全保证 template<typename Func> void safeExecute(Func&& func) { try { func(); } catch (const std::exception& e) { std::cerr << "Error caught: " << e.what() << std::endl; // 记录日志,通知监控系统,不崩溃 } catch (...) { std::cerr << "Unknown error" << std::endl; } }
🎯 防御式编程清单
1. 输入校验:检查所有外部输入的合法性
2. 边界检查:数组下标、指针空值、除零等
3. 异常处理:用 RAII 管理资源,用 try-catch 隔离错误
4. 断言:用 assert 检查开发期的不变量
5. 容错设计:考虑超时、重试、降级策略
1. 输入校验:检查所有外部输入的合法性
2. 边界检查:数组下标、指针空值、除零等
3. 异常处理:用 RAII 管理资源,用 try-catch 隔离错误
4. 断言:用 assert 检查开发期的不变量
5. 容错设计:考虑超时、重试、降级策略
十四、契约式编程:契约即保障
定义:契约式编程(Design by Contract, DbC)由 Bertrand Meyer 提出——将软件组件之间的关系视为"商业契约",明确规定双方的权利和义务。
📝 生活类比:合同
甲方调用乙方的方法,相当于签订合同:
· 前置条件(Precondition):甲方必须满足什么条件才能调用
· 后置条件(Postcondition):乙方保证调用后达到什么状态
· 不变量(Invariant):双方共同承诺永远保持的规则
违反合同的一方承担责任。
甲方调用乙方的方法,相当于签订合同:
· 前置条件(Precondition):甲方必须满足什么条件才能调用
· 后置条件(Postcondition):乙方保证调用后达到什么状态
· 不变量(Invariant):双方共同承诺永远保持的规则
违反合同的一方承担责任。
14.1 C++ 实现示例:显式契约
#include <iostream> #include <cassert> #include <stdexcept> // 用宏模拟 Eiffel 语言的契约机制 #define REQUIRE(cond) \ if (!(cond)) throw std::invalid_argument("Precondition failed: " #cond) #define ENSURE(cond) \ if (!(cond)) throw std::logic_error("Postcondition failed: " #cond) #define INVARIANT(cond) assert(cond) class BankAccount { double balance; bool frozen; public: BankAccount() : balance(0), frozen(false) { INVARIANT(balance >= 0); // 不变量:余额永不为负 } // 前置:金额为正且账户未冻结 void deposit(double amount) { REQUIRE(amount > 0); // 前置条件 REQUIRE(!frozen); // 前置条件 double oldBalance = balance; balance += amount; ENSURE(balance == oldBalance + amount); // 后置条件 ENSURE(balance > oldBalance); // 后置条件 INVARIANT(balance >= 0); // 不变量保持 } // 前置:金额为正、余额充足、账户未冻结 bool withdraw(double amount) { REQUIRE(amount > 0); REQUIRE(balance >= amount); REQUIRE(!frozen); double oldBalance = balance; balance -= amount; ENSURE(balance == oldBalance - amount); ENSURE(balance < oldBalance); INVARIANT(balance >= 0); return true; } };
✅ 契约式编程的价值
· 明确责任:清晰规定调用方和实现方的责任
· 早期发现错误:违反契约立即报错,不会拖延成隐蔽 Bug
· 文档即代码:契约本身就是最好的接口文档
· Eiffel 语言原生支持,C++/Java 通过断言/异常模拟
· 明确责任:清晰规定调用方和实现方的责任
· 早期发现错误:违反契约立即报错,不会拖延成隐蔽 Bug
· 文档即代码:契约本身就是最好的接口文档
· Eiffel 语言原生支持,C++/Java 通过断言/异常模拟
十五、各范式对比与选型指南
15.1 全景对比表
15.2 选型决策流程图
15.3 实战建议:混合使用多种范式
💡 现代 C++ 项目的典型范式组合
· OOP 组织领域模型(User、Order、Product)
· DDD 定义聚合根和值对象,保证业务一致性
· FP 处理集合操作(STL 算法、lambda、std::function)
· AOP 用模板/装饰器实现日志、缓存等横切关注
· 事件驱动 用观察者模式解耦模块间通信
· 声明式 用 range-for 和 STL 算法描述意图
· OOP 组织领域模型(User、Order、Product)
· DDD 定义聚合根和值对象,保证业务一致性
· FP 处理集合操作(STL 算法、lambda、std::function)
· AOP 用模板/装饰器实现日志、缓存等横切关注
· 事件驱动 用观察者模式解耦模块间通信
· 声明式 用 range-for 和 STL 算法描述意图
🌟 范式组合示例:电商订单系统
- DDD:Order 聚合根、Money 值对象、OrderPlaced 领域事件
- OOP:OrderService、PaymentService、NotificationService
- 事件驱动:OrderPlaced 事件触发库存扣减、邮件通知
- 函数式:订单列表的过滤、排序、分组统计
- AOP:@Log 日志注解、@Cache 缓存装饰器
- 声明式:用 SQL 查询订单、用 JSON/XML 描述配置
十六、总结:没有银弹,只有合适的选择
😆 最后的忠告
"我用函数式重写了我们的业务系统!"
"然后呢?"
"每个业务逻辑都变成了 map/filter/reduce 的嵌套组合,新同事看代码像看天书。"
"过度追求纯粹,反而让代码更难维护。"
——范式是工具,不是宗教。合适的才是最好的。
"我用函数式重写了我们的业务系统!"
"然后呢?"
"每个业务逻辑都变成了 map/filter/reduce 的嵌套组合,新同事看代码像看天书。"
"过度追求纯粹,反而让代码更难维护。"
——范式是工具,不是宗教。合适的才是最好的。
核心心法
- 了解但不迷信:知道每种范式的优劣,不要只信自己熟悉的那一个
- 场景驱动:根据问题选工具,而不是反过来
- 混合使用:现代语言支持多范式,大胆组合
- 渐进引入:在新项目或新模块中尝试新范式,不要大爆炸式重构
- 团队共识:团队成员的熟悉程度决定了范式的实际效果
📚 进阶阅读推荐
· 函数式编程:《Functional Programming Principles in Scala》— Martin Odersky
· DDD:《Domain-Driven Design: Tackling Complexity in the Heart of Software》— Eric Evans
· 响应式编程:《Reactive Programming with RxJava》— Tomasz Nurkiewicz
· OOP 设计原则:本文配套的《七大设计原则详解》
· 函数式编程:《Functional Programming Principles in Scala》— Martin Odersky
· DDD:《Domain-Driven Design: Tackling Complexity in the Heart of Software》— Eric Evans
· 响应式编程:《Reactive Programming with RxJava》— Tomasz Nurkiewicz
· OOP 设计原则:本文配套的《七大设计原则详解》
🎯 终极心法
"编程不是关于写代码,而是关于组织思维。好的范式让你用更少的代码表达更清晰的意图,让后来的维护者能快速理解你的设计思路——这才是编程的真正艺术。"
"编程不是关于写代码,而是关于组织思维。好的范式让你用更少的代码表达更清晰的意图,让后来的维护者能快速理解你的设计思路——这才是编程的真正艺术。"