接上文常用的设计模式实例-单例/装饰器/观察者/中介者
回过头来我们看看活动要求的数据
话费充值:充话费满100反1
显然我们需要设计一个配置,能表示出100和1的对应关系,否则下一次做满100反2就只能干瞪眼了
根据要求,我们简要设计出两个活动的配置
//话费充值活动的配置 class ActivityTimeLimitTopUpCfg { int ActID;//活动索引ID int ChargingNum;//充值金额 int ReturnNum;//返还金额 };
因为策划需求千变万化,我们决定每一天都是一个新活动,所以配置一共有365条,一天用一条
const int MAX_ACT_TIME_LIMIT_TOP_UP_CFG_NUM = 365;
在内存中我们定义这样的数组保存所有配置
ActivityTimeLimitTopUpCfg TimeLimitTopUpCfg[MAX_ACT_TIME_LIMIT_TOP_UP_CFG_NUM];
完善我们之前充值活动的接口,就按照配置返回指定的金额
void ActivityTimeLimitTopUp::OnDelItem(User& user, int DeleteItemNum) { for (int i = 0; i < MAX_ACT_TIME_LIMIT_TOP_UP_CFG_NUM; ++i) { //找到对应活动的配置 if (user.GetActTimeLimitID() != TimeLimitTopUpCfg[i].ActID) { continue; } //根据充值计算需要返还的金额 int ReturnNum = DeleteItemNum / TimeLimitTopUpCfg[i].ChargingNum * TimeLimitTopUpCfg[i].ReturnNum; //返还金额 ReturnDiscountToUser(user, ReturnNum); //一次充值就返还一次奖励 break; } }//其他的接口就不一一列出了
观察一下就会发现,为了把玩家身上对应活动和配置的活动匹配上,我们需要遍历整个活动列表!
假设有活动配置了十万个,每一次拉取配置都要遍历十万次,这肯定是不能接受的(因为拉取配置是一个非常常见的操作!)
解决遍历最简单有效的方法:初始化时排序, 查找时二分查找
我们设计这么个拉取配置的方法
ActivityTimeLimitTopUp* GetCfgByActID(int ActID) { //简单起见这里就不实现二分查找了 ActivityTimeLimitTopUp* pstCfg = binarySearch(TimeLimitTopUpCfg, ActID); return pstCfg; }
活动的代码可以改造成
void ActivityTimeLimitTopUp::OnDelItem(User& user, int DeleteItemNum) { TimeLimitTopUpCfg* pstCfg = GetCfgByActID(user.GetActTimeLimitID()); if (NULL == pstCfg) { return; } //根据充值计算需要返还的金额 int ReturnNum = DeleteItemNum / pstCfg->ChargingNum * pstCfg->ReturnNum; //返还金额 ReturnDiscountToUser(user, ReturnNum); }
简单明了,时间复杂度还从O(N)降成了O(logN),皆大欢喜
也别高兴太早,如果蚂蚁森林活动也要用这个GetCfgByActID接口该怎么办?
如果海底捞活动也要用怎么办?
因为GetCfgByActID返回的TimeLimitTopUpCfg只有充值话费活动可以用,其他活动的配置比如
//蚂蚁森林活动的配置 class ActivityTreeCfg { int ActID;//活动索引ID int SpendBuy;//消耗购物金额 int BuyEnergy;//购物获得的能量 int SpendWalk;//消耗行走步数 int WalkEnergy;//行走获得的能量 }; const int MAX_ACT_TREE_CFG_NUM = 365; ActivityTreeCfg TreeCfg[MAX_ACT_TREE_CFG_NUM];
难道每添加一个活动就要写一个GetCfgByActID函数吗?
我们的目标就是消灭重复代码,那么问题的关键终于来了,思考下这个流程,是不是和工厂模式出奇的相似
配置中心(工厂) 生产出所有活动配置(零件)
我们想要拿到一个活动的配置(零件),应该直接从配置中心(工厂)拿到,而不是每一个都自己造轮子
这就有个关键问题,观察者模式观察的对象全部都是从ActivityBase继承而来
工厂模式生产的同理也一定是同种抽象零件(ActivityCfgBase)
ActivityTreeCfg和ActivityTimeLimitTopUpCfg两者结构不同,如何才能表现成这种样子
class ItemCfgBase { }; class ActivityTreeCfg : public ItemCfgBase { }; class ActivityTimeLimitTopUpCfg : public ItemCfgBase { };
答案很简单, 像这种没有复杂逻辑但是不同结构的class, 我们通通用char*去保存数据!
class ItemCfgBase { public: static ItemCfgBase& Instance() { static ItemCfgBase m_instance; return m_instance; } ItemCfgBase() { ItemCfgBase(0, 0, 0, NULL); } ItemCfgBase(int iType, int iSize, int iNum, char* cData): m_cfgType(iType),m_unitSize(iSize),m_cfgNum(iNum),m_data(cData){} void* GetCfgByID(int ID);//新的获取配置接口 private: int m_cfgType;//配置的类型 int m_unitSize;//每一个配置的大小 int m_cfgNum;//该配置有多少条 char *m_data;//该配置的所有内容 };
如此我们可以将上面两个活动的配置这样表示
class ItemTreeCfg : public ItemCfgBase { ItemTreeCfg::ItemTreeCfg(): ItemCfgBase(CFG_TYPE_TREE, sizeof(ActivityTreeCfg), 365, TreeCfg);//5个int所以是20 }; class ItemTimeLimitTopUpCfg : public ItemCfgBase { ItemTimeLimitTopUpCfg::ItemTimeLimitTopUpCfg(): ItemCfgBase(CFG_TYPE_TOP_UP, sizeof(ActivityTimeLimitTopUpCfg), 365, TimeLimitTopUpCfg);//3个int所以是12 };
也许你有另一个问题,对char*类型的m_data如何排序?
我们可以要求所有配置必须都有一个4bit的索引值(比如ActivityTreeCfg的ActID)
这个低成本的设计,使得我们可以使用库函数来排序
int CfgCmp(const void* data1, const void* data2) { return( *(int*)data1 - *(int*)data2 ); } qsort(m_data, m_cfgNum, m_unitSize, CfgCmp);
重点在CfgCmp上,将每一个配置的前4字节强制转换为int,根据两个配置data的前4字节大小排序
搜索也同理,直接使用库函数
void* ItemCfgBase::GetCfgByID(int ID)//因为配置的类型不固定,所以返回void*去做强制转换 { return bsearch(&ID, m_data, m_cfgNum, m_unitSize, CfgCmp); }
既然产品都已经生产好了,那么接下来就是设计工厂去贩卖产品了
class ItemCfgFactory { enum CFG_TYPE { CFG_TYPE_TREE,//蚂蚁森林活动配置 CFG_TYPE_TOP_UP,//累充话费活动配置 } public: //工厂只有一个,用单例 static ItemCfgFactory& Instance() { static ItemCfgFactory m_instance; return m_instance; } void CreateAllItems();//所有的配置都添加到工厂 void* GetOneItem(int ID, int Type);//根据配置类型去获取配置 private: std::map<int, ItemCfgBase*> m_items;//保存所有配置 }; void ItemCfgFactory::CreateAllItems() { m_items.insert(make_pair(CFG_TYPE_TREE, ItemTreeCfg::Instance())); m_items.insert(make_pair(CFG_TYPE_TOP_UP, ItemTimeLimitTopUpCfg::Instance())); } void* ItemCfgFactory::GetOneItem(int ID, int Type) { if (m_items.find(Type) != m_items.end()) { return m_items[Type].GetCfgByID(ID); } }
这样我们终于可以回归改造活动接口了
void ActivityTimeLimitTopUp::OnDelItem(User& user, int DeleteItemNum) { TimeLimitTopUpCfg* pstCfg = ItemCfgFactory::Intance().GetOneItem(user.GetActTimeLimitID(), CFG_TYPE_TOP_UP); if (NULL == pstCfg) { return; } //根据充值计算需要返还的金额 int ReturnNum = DeleteItemNum / pstCfg->ChargingNum * pstCfg->ReturnNum; //返还金额 ReturnDiscountToUser(user, ReturnNum); }
往后的任何代码只需要注册到ItemCfgFactory即可
最后,我们觉得返回的void*实在很不安全,可以使用模板替代它
template<class ItemName> class ItemCfgBase { ItemName* GetCfgByID(int ID);//新的获取配置接口 } template<class ItemName> class ItemCfgFactory { ItemName* GetOneItem(int ID, int Type);//根据配置类型去获取配置 } void ActivityTimeLimitTopUp::OnDelItem(User& user, int DeleteItemNum) { TimeLimitTopUpCfg* pstCfg = ItemCfgFactory::Intance().GetOneItem<ActivityTimeLimitTopUpCfg>(user.GetActTimeLimitID(), CFG_TYPE_TOP_UP); }
总结 : 工厂模式的应用很广,可以动态的生成产品而不必要在一开始全部创建,这一点视场景而定。实际应用中,可以使用模板进一步简化代码逻辑