我的另一篇文章 使用 C++ template 进行多厂商接口的适配,已经描述过如何在编译期通过模板函数区分产品的不同行为,但模板函数无法在代码框架层面支持多客户(产品)。本篇文章,从类设计的角度阐述一下,如何使用 template 让一套代码兼容多个产品。
需求分析
我的聊天机器人有三个语言包:
中文class ChinesePackage;
俄文class RussianPackage;
英文class EnglishPackage;
针对不同的客户,有三个产品型号,要支持不同的语言包:
默认熊猫版本RobotPanda,要支持全部中文+俄文+英文语言包。
北极熊版本RobotBear,要支持中文+俄文。
小绵羊版本RobotSheep,支持中文+英文。
引子:conditional
首先看一下标准库中用于类型选择的 conditional,在《The C++ Programming Language》一书中有比较详细的例子。
template<bool _Cond, typename _Iftrue, typename _Iffalse>struct conditional{ typedef _Iftrue type; };
// Partial specialization for false.template<typename _Iftrue, typename _Iffalse>struct conditional<false, _Iftrue, _Iffalse>{ typedef _Iffalse type; };
复制代码
conditional 的实现很容易理解,就是通过第一个参数_Cond 在编译期做类型选择。
但 bool 值只有两个取值 true/false,也就是我们的类型只有两种选择,能不能有三种或多种呢?这里我们来个举一反三,把 bool _Cond 改为 int _Cond,是不是就可以支持无限个选择了呢?
准备工作
脚本与产品类型宏
为每个产品创建一个编译脚本,传递对应的宏到源码:
make_panda.sh: -DROBOT_TYPE=0x4348
make_bear.sh: -DROBOT_TYPE=0x5255
make_sheep.sh: -DROBOT_TYPE=0x454E
产品类型常量
源码中也要定义对应的常量:
const int ROBOT_TYPE_CH = 0x4348;const int ROBOT_TYPE_RU = 0x5255;const int ROBOT_TYPE_EN = 0x454E;
复制代码
语言包
//用于上层业务选择语言enum LANGUAGE_CHOICE{ LAN_CH = 0, LAN_EN, LAN_RU, LAN_MAX};
class LanguagePackage{ virtual void Speak() {}}
class ChinesePackage : public LanguagePackage{ void Speak() override { //讲中文 }};
class RussianPackage : public LanguagePackage{ void Speak() override { //讲俄文 }};
class EnglishPackage : public LanguagePackage{ void Speak() override { //讲英文 }};
复制代码
举一反三
如果沿用 conditional 的实现方式,三选一的模板就是如下形式:
template<int RobotType, class T_Panda, class T_Bear, class T_Sheep>struct RobotModelType{ using Type = T_Panda;};
template<>struct RobotModelType<ROBOT_TYPE_RU>{ using Type = T_Bear;};
template<>struct RobotModelType<ROBOT_TYPE_EN>{ using Type = T_Sheep;};
using RobotType = RobotModelType<ROBOT_TYPE, RobotPanda, RobotBear, RobotSheep>::Type;
复制代码
但这里有个代码扩展的问题,如果我们再增加新的产品型号 RobotYY,RobotModelType 是不是要增加模板参数呢?如果新增 N 个产品型号呢?
举一反 N
那不妨思维灵活一些,我的代码是在做工程,不是在做 C++标准库,所以不需要兼容不确定的产品类型,只需要兼容我自己设定的 Robot 类型就可以啦。这样就简单了,把要选择的类型从模板参数列表中去掉,直接通过特化来实现。
RobotPanda
template<int RobotType>struct RobotPanda{ void Config(LANGUAGE_CHOICE lan_choice) { switch(lan_choice) { case LAN_CH: m_LanPackage = &m_chinese; break; case LAN_RU: m_LanPackage = &m_russian; break; case LAN_EN: m_LanPackage = &m_english; break; default: //报错,不支持 break; } } ChinesePackage m_chinese; RussianPackage m_russian; EnglishPackage m_english;
LanguagePackage* m_LanPackage = &m_chinese;};
复制代码
RobotBear
template<int RobotType>struct RobotBear{ void Config(LANGUAGE_CHOICE lan_choice) { switch(lan_choice) { case LAN_RU: m_LanPackage = &m_russian; break; case LAN_CH: m_LanPackage = &m_chinese; break; default: //报错,不支持 break; } }
RussianPackage m_russian; ChinesePackage m_chinese;
LanguagePackage* m_LanPackage = &m_chinese;};
复制代码
RobotSheep
template<int RobotType>struct RobotSheep{ void Config(LANGUAGE_CHOICE lan_choice) { switch(lan_choice) { case LAN_EN: m_LanPackage = &m_english; break; case LAN_CH: m_LanPackage = &m_chinese; break; default: //报错,不支持 break; } }
EnglishPackage m_english; ChinesePackage m_chinese;
LanguagePackage* m_LanPackage = &m_chinese;};
复制代码
RobotModelType 重磅来袭
/* 编译期类型选择. 默认 ROBOT_TYPE_CH. */template<int RobotType>struct RobotModelType{ using Type = RobotPanda<ModelType>;};
template<>struct RobotModelType<ROBOT_TYPE_EN>{ using Type = RobotSheep<ROBOT_TYPE_EN>;};
template<>struct RobotModelType<ROBOT_TYPE_RU>{ using Type = RobotBear<ROBOT_TYPE_RU>;};
复制代码
template RobotModelType是本文的重点,理论上它支持无限种产品扩展,新增产品型号时,只需要增加对应的型号常量和特化就可以了。
RobotXX 之所以也设计为模板,是为了在编译一个产品时,其他的产品代码不被生成,减少代码尺寸,也杜绝代码被反编译泄露的可能。
机器人
一个产品脚本,只编译自己项目的机器人。
class RobotTalker : protected RobotModelType<ROBOT_TYPE>::Type{public: void ConfigPackage(LANGUAGE_CHOICE lan_choice) { Config(lan_choice); } void Speak() { m_LanPackage->Speak(); }};
复制代码
类图
为了更直观的理解代码框架,我把每个机器人的类图画出来。
熊猫
RobotPanda,要支持全部语言包。
Panda
北极熊
RobotBear,要支持中文+俄文包。
Bear
小绵羊
RobotSheep,支持中文+英文。
Sheep
结语
这就是模板的威力,不用编译宏,不用拉代码分支,一套代码支持多个产品型号,但我不太想把这种做法定义为编译期多态或者编译期的策略模式,思维模式没必要生拉硬套,能解决问题就好。
当多个产品大部分基本功能相同,仅部分功能有差异,需要针对不同需求进行组合时,本篇中的方法可灵活适配。
评论