目录
[四、策略模式的解法:LLMProvider 抽象基类](#四、策略模式的解法:LLMProvider 抽象基类)
[4.1 源码](#4.1 源码)
[4.2 逐行拆解](#4.2 逐行拆解)
[4.3 三个子类的实现结构](#4.3 三个子类的实现结构)
一、开篇
任何一个项目,随着时间推移,需求都会越来越多。加新功能 → 改老代码 → 代码越来越臃肿 → 改一个地方影响一片 → 变成屎山。
好的设计模式就是对抗代码腐烂的武器。
这一篇,我们来讲这个项目最核心的设计------策略模式 。面试官如果问你这个项目,十有八九会从这里开始问。把这篇文章吃透,面试你就稳了一半。
二、先看一个生活类比
假设你是一个大学生,每天要去实验室。你有三种方式:
- 走路去:免费但慢
- 骑车去:快一点但得找车
- 坐公交去:最快但要花钱
用代码写出来,最直接的方式是:
cpp
// if-else 大法
void goToLab(const std::string& way) {
if (way == "walk") {
std::cout << "走路去实验室,耗时20分钟" << std::endl;
} else if (way == "bike") {
std::cout << "骑车去实验室,耗时10分钟" << std::endl;
} else if (way == "bus") {
std::cout << "坐公交去实验室,耗时5分钟" << std::endl;
}
}
问题在哪? 如果以后有了新的交通方式(比如滑板、打车),你就要改 goToLab 这个函数,增加一个 else if。改着改着这个函数就几百行了,而且每次改都可能引入 bug。
策略模式的做法:
cpp
// 抽象策略:定义"去实验室"的接口
class TransportStrategy {
public:
virtual void go() = 0; // "去实验室"这个行为
virtual ~TransportStrategy() = default;
};
// 具体策略1:走路
class WalkStrategy : public TransportStrategy {
virtual void go() override {
std::cout << "走路去实验室,耗时20分钟" << std::endl;
}
};
// 具体策略2:骑车
class BikeStrategy : public TransportStrategy {
virtual void go() override {
std::cout << "骑车去实验室,耗时10分钟" << std::endl;
}
};
// 具体策略3:公交
class BusStrategy : public TransportStrategy {
virtual void go() override {
std::cout << "坐公交去实验室,耗时5分钟" << std::endl;
}
};
// 上下文:学生
class Student {
TransportStrategy* _strategy;
public:
void setStrategy(TransportStrategy* s) { _strategy = s; }
void goToLab() { _strategy->go(); }
};
// 使用
int main() {
Student me;
me.setStrategy(new WalkStrategy());
me.goToLab(); // 走路去
me.setStrategy(new BusStrategy());
me.goToLab(); // 坐公交去------换策略不用改Student代码!
return 0;
}
优势:
Student类不需要知道任何交通方式的具体实现- 加新的交通方式(滑板),只需要新增
SkateboardStrategy,不需要改Student的代码
这就是策略模式------定义一系列算法,把它们一个个封装起来,让它们可以互相替换。算法的变化独立于使用算法的客户。
三、回到项目,如果不策略模式,代码会变什么样?
我们的项目需要对接 DeepSeek、ChatGPT、Gemini 三家 AI 厂商。如果不做设计,代码会写成这样
cpp
// 所有逻辑写在一个函数里
std::string sendMessage(const std::string& model,
const std::vector<Message>& messages,
const std::map<std::string, std::string>& params) {
std::string jsonBody;
std::string endpoint;
std::string path;
if (model == "deepseek-chat") {
// DeepSeek 的请求体格式
jsonBody = buildDeepSeekBody(messages, params);
endpoint = "https://api.deepseek.com";
path = "/v1/chat/completions";
} else if (model == "gpt-4o-mini") {
// ChatGPT 的请求体格式(字段名完全不同)
jsonBody = buildChatGPTBody(messages, params);
endpoint = "https://api.openai.com";
path = "/v1/responses";
} else if (model == "gemini-2.0-flash") {
// Gemini 又是另一套
jsonBody = buildGeminiBody(messages, params);
endpoint = "https://generativelanguage.googleapis.com";
path = "/v1beta/models/gemini-2.0-flash:generateContent";
} else {
return "unsupported model";
}
// 发送 HTTP 请求...
// 解析响应...
// 每个模型的响应结构也不一样
}
这个函数的恐怖之处:
| 问题 | 说明 |
|---|---|
| 越长越大 | 加一个新模型,这个函数就膨胀一次 |
| 职责不单一 | 一个函数同时管了 N 个模型的细节 |
| 测试困难 | 没法单独测试某个模型的逻辑 |
| 新人上手难 | 新人要看懂所有模型才能改一行代码 |
| 合并冲突 | 多人在同一个函数里改,Git 冲突不断 |
这就是典型的"违反开闭原则"(对扩展开放,对修改关闭)。
加一个新功能(新模型),要修改已有的代码(这个巨大的 if-else 函数)。
四、策略模式的解法:LLMProvider 抽象基类
4.1 源码
cpp
#pragma once
#include <functional>
#include <string>
#include <map>
#include <vector>
#include "common.h"
namespace ai_chat_sdk {
// LLMProvider:抽象基类(策略模式中的"抽象策略")
class LLMProvider {
public:
// 初始化模型(注入 API Key、endpoint 等配置)
virtual bool initModel(const std::map<std::string, std::string>& modelConfig) = 0;
// 检测模型是否可用
virtual bool isAvailable() const = 0;
// 获取模型名称
virtual std::string getModelName() const = 0;
// 获取模型描述
virtual std::string getModelDesc() const = 0;
// 发送消息 - 全量返回
virtual std::string sendMessage(
const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam
) = 0;
// 发送消息 - 增量返回(流式响应)
virtual std::string sendMessageStream(
const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback
) = 0;
protected:
bool _isAvailable = false; // 标记模型是否初始化成功
std::string _apiKey; // API 密钥
std::string _endpoint; // API 基础地址
};
} // end namespace
4.2 逐行拆解
纯虚函数 = 0 的含义:
- 等号后面的
= 0表示这个函数是纯虚函数- 包含纯虚函数的类叫抽象类 ,不能实例化(不能
new LLMProvider())- 子类必须实现所有纯虚函数,否则子类也是抽象类,也不能实例化
- 这就形成了一道强制契约:任何想当"AI Provider"的类,都必须实现这 6 个方法
五个纯虚函数各司其职:
| 函数 | 作用 | 返回值 |
|---|---|---|
initModel |
注入配置(API Key、地址等) | true/false |
isAvailable |
检查是否初始化成功 | true/false |
getModelName |
返回模型标识,如 "deepseek-chat" | string |
getModelDesc |
返回模型描述,给前端展示用 | string |
sendMessage |
发送消息,等待完整回复 | 回复文本 |
sendMessageStream |
发送消息,回调式流式接收 | 完整回复文本 |
两个重要的 protected 成员变量:
cpp
protected:
bool _isAvailable = false;
std::string _apiKey;
std::string _endpoint;
protected表示:子类可以直接访问,外部不能访问- 每个 Provider 初始化时设置
_apiKey和_endpoint,sendMessage里直接用_isAvailable默认false,initModel成功后才设为true
回调函数的设计:
cpp
std::function<void(const std::string&, bool)> callback
- 第一个参数
const std::string&:AI 本次返回的增量文本片段- 第二个参数
bool:false表示还有后续;true表示这是最后一块数据std::function是 C++11 的通用函数包装器,可以接受 lambda、函数指针、bind 表达式等
这个回调的设计模式叫做观察者模式的变体------Provider 产生数据,通过回调通知消费者,消费者自己决定怎么处理这些数据。
4.3 三个子类的实现结构
DeepSeekProvider:
cpp
class DeepSeekProvider : public LLMProvider {
public:
virtual bool initModel(const std::map<std::string, std::string>& config) override;
virtual bool isAvailable() const override;
virtual std::string getModelName() const override; // 返回 "deepseek-chat"
virtual std::string getModelDesc() const override;
virtual std::string sendMessage(...) override;
virtual std::string sendMessageStream(...) override;
};
GeminiProvider 和 OllamaLLMProvider 同理
关键点:接口完全一样,但内部实现完全不同。 DeepSeek 调 /v1/chat/completions,ChatGPT 调 /v1/responses,Gemini 调 /v1beta/models/...:generateContent。但因为都继承自 LLMProvider,上层代码感知不到这些差异。
五、LLMManager:把这些策略管理起来
光有策略还不够,还需要一个地方来注册所有策略 ,并按模型名称路由。
cpp
class LLMManager {
public:
// 注册一个 Provider
bool registerProvider(const std::string& modelName,
std::unique_ptr<LLMProvider> provider);
// 初始化指定模型
bool initModel(const std::string& modelName,
const std::map<std::string, std::string>& params);
// 检查模型是否可用
bool isModelAvailable(const std::string& modelName) const;
// 获取可用模型列表
std::vector<ModelInfo> getAvailableModels() const;
// 发送消息(内部按 modelName 找到对应的 Provider,调它的 sendMessage)
std::string sendMessage(const std::string& modelName, ...);
std::string sendMessageStream(const std::string& modelName, ..., callback);
private:
// 注册表:模型名称 → Provider(unique_ptr 独占所有权)
std::map<std::string, std::unique_ptr<LLMProvider>> _providers;
// 模型信息表
std::map<std::string, ModelInfo> _modelInfos;
};
核心方法 sendMessage 的实现:
cpp
std::string LLMManager::sendMessage(const std::string& modelName,
const std::vector<Message>& messages,
const std::map<std::string, std::string>& params) {
// 1. 根据模型名称找到对应的 Provider
auto it = _providers.find(modelName);
if (it == _providers.end()) {
ERR("model provider not found: {}", modelName);
return "";
}
// 2. 检查是否可用
if (!it->second->isAvailable()) {
ERR("model not available: {}", modelName);
return "";
}
// 3. 调用多态,it->second 是 LLMProvider*,实际指向 DeepSeekProvider
return it->second->sendMessage(messages, params);
}
关键点就在第 3 步:it->second->sendMessage(messages, params)。
it->second的类型是unique_ptr<LLMProvider>,但实际指向的是DeepSeekProvider或ChatGPTProvider对象。调用sendMessage时,C++ 的虚函数机制会动态地找到实际对象的sendMessage方法。 这就是多态(polymorphism)。
六、开闭原则的完美体现
现在问你:如果要加一个新模型,比如阿里的通义千问(Qwen),需要改几行代码?
答案是:新增两个文件,改一行代码。
cpp
// 改一行:在 ChatSDK::registerAllProvider() 中
// 原来的代码:
_llmManager.registerProvider("deepseek-chat", make_unique<DeepSeekProvider>());
_llmManager.registerProvider("gpt-4o-mini", make_unique<ChatGPTProvider>());
_llmManager.registerProvider("gemini-2.0-flash", make_unique<GeminiProvider>());
// 新加一行:
_llmManager.registerProvider("qwen-max", make_unique<QwenProvider>());
不需要修改的代码:
- ❌ 不需要改
LLMProvider.h(基类不变) - ❌ 不需要改
LLMManager.cpp(路由逻辑不变) - ❌ 不需要改
ChatSDK.cpp(外观模式不变) - ❌ 不需要改
ChatServer.cpp(HTTP 路由不变) - ❌ 不需要改
DeepSeekProvider.cpp(已有的不受影响)
只新增(对扩展开放),不修改(对修改关闭) 。这就是开闭原则。
七、问答专场
Q1:说说策略模式在项目中的应用?
我们这个项目需要对接多家 AI 厂商的 API。每家厂商的调用方式、请求格式、响应结构都不一样。如果用 if-else 来判断模型类型,代码会变得臃肿且难以维护。
所以我们设计了 LLMProvider 抽象基类,定义六个纯虚函数作为契约。DeepSeekProvider、ChatGPTProvider、GeminiProvider 分别继承并实现自己的版本。LLMManager 维护一个模型名称到 Provider 的映射表,调用时按名称找到对应的 Provider,利用 C++ 的虚函数多态机制自动调用正确的实现。
这样当我们要加新模型时,只需要新增一个 Provider 子类,在注册表里加一行,不需要修改任何现有代码。符合开闭原则。
Q2:纯虚函数和虚函数有什么区别?
| 对比 | 虚函数(virtual) | 纯虚函数(virtual ... = 0) |
|---|---|---|
| 基类是否需要实现 | 可以有自己的实现 | 不能有实现 |
| 子类是否必须重写 | 可以不重写(用基类的) | 必须重写 |
| 基类能否实例化 | 可以 | 不能(抽象类) |
Q3:为什么用 unique_ptr 而不是 shared_ptr 管理 Provider?
std::map<std::string, std::unique_ptr<LLMProvider>> _providers;
unique_ptr表示独占所有权:Provider 的生命周期由 LLMManager 独占管理unique_ptr没有引用计数,零开销抽象shared_ptr适合共享所有权场景,这里不需要
Q4:std::move 在这里的作用?
_llmManager.registerProvider("deepseek-chat", std::move(deepseekProvider));
unique_ptr 不能拷贝(拷贝构造函数被 delete 了),只能通过移动语义转移所有权。std::move 把 deepseekProvider 的所有权转移给 _providers 内部,原来的 deepseekProvider 变成空指针。
Q5:回调函数 std::function<void(const string&, bool)>,为什么不用函数指针?
std::function可以包装 lambda、函数指针、bind 表达式等- C++ 的函数指针不能捕获外部变量,而 lambda 可以
- 在实际使用中,
sendMessageStream的回调经常需要捕获this指针或局部变量,lambda 更方便
八、总结
这一篇的核心就一句话:
面向接口编程,而不是面向实现编程。
把变化的部分(各厂商的 API 差异)封装在独立的类中,让稳定的部分(调用逻辑、会话管理、HTTP 路由)依赖于稳定的抽象接口。
整个项目中用到策略模式的文件:
cpp
LLMProvider.h ← 抽象策略(6 个纯虚函数的接口)
DeepSeekProvider.cpp ← 具体策略 1
ChatGPTProvider.cpp ← 具体策略 2
GeminiProvider.cpp ← 具体策略 3
OllamaLLMProvider.cpp ← 具体策略 4
LLMManager.cpp ← 策略的路由/管理器
ChatSDK.cpp ← 策略的使用者(通过 LLMManager 间接使用)