【C++】(常用的设计模式实例2——工厂模式) & (不同类型结构体继承同一父类方法)
接上文常用的设计模式实例-单例/装饰器/观察者/中介者
回过头来我们看看活动要求的数据
话费充值:充话费满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);
}
总结 : 工厂模式的应用很广,可以动态的生成产品而不必要在一开始全部创建,这一点视场景而定。实际应用中,可以使用模板进一步简化代码逻辑