【C++】(常用的设计模式实例2——工厂模式) & (不同类型结构体继承同一父类方法)

88 Views

接上文常用的设计模式实例-单例/装饰器/观察者/中介者
回过头来我们看看活动要求的数据
话费充值:充话费满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);
}

总结 : 工厂模式的应用很广,可以动态的生成产品而不必要在一开始全部创建,这一点视场景而定。实际应用中,可以使用模板进一步简化代码逻辑

留下回复

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据