【C++】常用的设计模式实例-单例/装饰器/观察者/中介者

以一个实际的User登录某APP为例,概述在后台框架中,其可能使用到的设计模式

User进行登录验证成功后,进入OnLoginSuccess事件,将各个登录逻辑用单例模式包装成事件依次处理,
将内存中的m_user对象作为传参供单例mgr使用,其中例如DataMgr不需要修改user内容的应严格使用const

void User::OnLoginSuccess()
{
    //更新上次登陆时间
    TimerMgr::Instance().UpdateLastLoginTime(m_user);
    //返回给客户端User的信息用于显示
    DataMgr::Instance().SendClientUserInfo(m_user);
    //将广告推送给User
    AdvertiseMgr::Instance().SendNewsToUser(m_user);
}

单例模式

static创建的对象是线程安全的

class DataMgr
{
    public:
        static DataMgr& Instance()
        {
            static DataMgr s_instance;
            return s_instance;
        }
        void SendClientUserInfo(const User& cUser);

    private:
        DataMgr(){};
        ~DataMgr(){};
};

总结 : 单例模式满足进程中只存在一个实例化对象,为一些频繁调用的程序节省了系统资源


现在,你想为玩家提供一些促销活动,
有以下几个活动:
1.限时充值话费满100反1
2.登录赠送海底捞代金券100减2
3.一个蚂蚁森林的复刻版(产生能量、偷能量、收取能量)

分析可以发现,他们都有一些共同的特性:
1.都有开始时间和结束时间
2.都会在活动过程中获得XX
3.可能在活动过程中消耗xx
4.在获得了XX东西后会用一种方式告诉你(直接弹窗或者发一个红点)
总而言之,他们都是为了骗你进入APP,然后送你点甜头,让你花更多的钱或者提高活跃度!

那么事情就简单了,我们需要有这些逻辑
1.过期判断(跨过某个时间点过期) :
登录时可能会触发活动的过期
在线状态下不会触发登录,但是活动也可能会过期
2.User的物品变化(余额、能量、代金券等等)
所有User物品发生变化时可以增加一个监听事件,在监听事件处理这些变化

我们使用单例模式创建一个事件监听器,在各种事件发生时调用他

struct ItemData;

class EventTrigger
{
    public:
        static EventTrigger& Instance()
        {
            static EventTrigger s_instance;
            return s_instance;
        }
        //登录事件
        OnLoginSuccess(User& user);
        //跨天事件
        OnPassDay(User& user);
        //道具增加事件
        OnAddItem(User& user, ItemData& cItemData);
        //道具减少事件
        OnDelItem(User& user, ItemData& cItemData);
};

比如可以这样处理

void User::OnLoginSuccess()
{
    //更新上次登陆时间
    TimerMgr::Instance().UpdateLastLoginTime(m_user);
    //返回给客户端User的信息用于显示
    DataMgr::Instance().SendClientUserInfo(m_user);
    //将广告推送给User
    AdvertiseMgr::Instance().SendNewsToUser(m_user);
    //触发登录事件
    EventTrigger::Instance().OnLoginSuccess(m_user);
}

现在我们可以开始设计我们的活动类了,一个基础的活动类,
1.需要检查活动是否在有效期内
2.活动开始时会做一些处理(发优惠券)
3.活动结束时会做一些处理(删除过期的道具)
4.道具发生变化时需要做一些处理
5.活动过程中的各种操作都是独有的,不可复用

/*ActivityBase.h*/
struct ActivityData;
//通知方法
enum MSG_TYPE
{
    MSG_TYPE_ALL = 0,//丧心病狂的全部打包通知
    MSG_TYPE_REDPOINT = 1,//红点提示
    MSG_TYPE_WINDOWS = 2,//弹窗消息
    MSG_TYPE_SHORTMSG = 3;//短信通知
};
//活动状态
enum ACT_STATUS
{
    ACT_STATUS_DEFAULT = 0,//默认
    ACT_STATUS_NOTOPEN = 1,//未开启
    ACT_STATUS_OPEN = 2,//刚开启
    ACT_STATUS_RUNNING = 3,//进行中
    ACT_STATUS_CLOSE = 4,//刚关闭
    ACT_STATUS_EXPIRED = 5;//已过期
};

class ActivityBase
{
    public:

        //登录成功后的处理
        virtual void OnLoinSuccess();
        //跨天的处理
        virtual void OnPassDay();
        //获得道具的处理
        virtual void OnAddItem(User& user);
        //失去道具的处理
        virtual void OnDelItem(User& user);
        //是否可以发送消息给User
        virtual bool CanSendMsgToUser(User& user){return true};
        //以某种形式发给玩家消息
        virtual void SendMgsToUser(User& user, const MSG_TYPE &eMsgType);

        void SetActivityData(const ActivityData& cActivityData)
        { 
            m_ActivityData = cActivityData;
        };
        ActivityData& GetActivityData()
        { 
            return m_ActivityData;
        };

    private:
        //刷新活动
        virtual void RefreshActcity();
        //改变活动的状态
        virtual void ChangeActcityStatus(ACT_STATUS& eStatus);
        //活动开始时的事件
        virtual void OnActStart(User& user) = 0;
        //活动结束时的事件
        virtual void OnActEnd(User& user) = 0;

        ActivityData m_ActivityData;
};

OnActStart和OnActEnd都是纯虚函数,这是因为每个活动开始和结束的操作都不一样,我们不能设计通用的函数去处理它
那么OnLoinSuccess、OnPassDay他们会带来什么影响呢,显然他们最大的影响就是改变了活动的状态,可能会让活动开启或关闭

/*ActivityBase.cpp*/
void ActivityBase::OnLoinSuccess()
{
    UpdateActcityStatus();
    switch(m_ActivityData.Status)
    {
        case ACT_STATUS_NOTOPEN:break;
        case ACT_STATUS_OPEN:
            OnActStart();
            ChangeActcityStatus(ACT_STATUS_RUNNING);
            break;
        case ACT_STATUS_RUNNING:break;
        case ACT_STATUS_CLOSE:
            OnActStop();
            ChangeActcityStatus(ACT_STATUS_EXPIRED);
            break;
        case ACT_STATUS_EXPIRED:break;
    }
}
void ActivityBase::OnPassDay()
{
    //因为逻辑和OnLoginSuccess一样,所以不用再写一遍
    OnLoginSuccess();
}

同理,大部分物品的变化都是会给玩家通知,告诉玩家发生了什么(恭喜你获得了满100减1代金券!呵呵)

/*ActivityBase.cpp*/
void ActivityBase::OnAddItem(User& user)
{
    if (CanSendMsgToUser(user))
    {
        SendMsgToUser(user, m_ActivityData.MsgType);
    }
}
void ActivityBase::OnDelItem(User& user)
{
    if (CanSendMsgToUser(user))
    {
        SendMsgToUser(user, m_ActivityData.MsgType);
    }
}

重头戏来了,终于进入到了关键

装饰器模式

先看看限时充值话费活动

/*ActivityTimeLimitTopUp.h*/
template <typename T>
class SimpleSingleton;

class ActivityTimeLimitTopUp : 
    public SimpleSingleton<ActivityTimeLimitTopUp>, public ActivityBase
{
    public:
        //充值话费消耗余额
        virtual void OnDelItem(User& user);
    private:
        //活动开始时的事件
        virtual void OnActStart(User& user);
        //活动结束时的事件
        virtual void OnActEnd(User& user);
};

/*ActivityTimeLimitTopUp.cpp*/
void ActivityTimeLimitTopUp::OnActStart(User& user)
{
    //二话不说先告诉大家可以享受充值优惠啦
    SendMsgToUser(user, m_ActivityData.MsgType);
}
void ActivityTimeLimitTopUp::OnActEnd(User& user)
{
    //悄悄的结束,啥都不做
}
void ActivityTimeLimitTopUp::OnDelItem(User& user)
{
    //还你们1块钱
    ReturnDiscountToUser(user);
}

可以看到onloginsuccess/onpassday这些终于不用再ctrl c+v啦,他们会自动继承父类的方法
delitem因为需要所以重写了逻辑,但是总体而言只需要写一些自己独有的函数,整个类都变得清爽了
同理再看看其他两个活动

/*ActivitySeaBottomCatch.h*/
template <typename T>
class SimpleSingleton;

class ActivitySeaBottomCatch: 
    public SimpleSingleton<ActivitySeaBottomCatch>, public ActivityBase
{
    private:
        //活动开始时的事件
        virtual void OnActStart(User& user);
        //活动结束时的事件
        virtual void OnActEnd(User& user);
        //聪明的产品经理选择返利给客户
        void ConvertCouponToMoney(User& user);
};

/*ActivitySeaBottomCatch.cpp*/
void ActivitySeaBottomCatch::OnActStart(User& user)
{
    //兑换券发给user
    GiveCouponToUser(user);
    //快来消费
    SendMsgToUser(user, m_ActivityData.MsgType);
}
void ActivitySeaBottomCatch::OnActEnd(User& user)
{
    //大出血,把活动结束没用的兑换券按一张1分钱还给user
    ConvertCouponToMoney(user);
}

/*ActivityTree.h*/
template <typename T>
class SimpleSingleton;

class ActivityTree: 
    public SimpleSingleton<ActivityTree>, public ActivityBase
{
    public:
        //消耗了某种东西(购物获得能量)
        virtual void OnDelItem(User& user);
        //增加了某种东西(增加步行数获得能量)
        virtual void OnAddItem(User& user);
        //偷别人的能量
        void StealOtherEnergy();
        //收自己的能量
        void GetMyOwnEnergy();
        //用能量浇水
        void UseEnergyToWater();
        //重写CanSend方法
        virtual bool CanSendMsgToUser(User& user);
    private:
        //活动开始时的事件
        virtual void OnActStart(User& user);
        //活动结束时的事件
        virtual void OnActEnd(User& user);

        void AddEnergy(User& user);
        void DelEnergy(User& user);
        //如有有能量可以收,通知玩家去收能量
        bool IfHaveEnergyCanGet(User& user);
};

/*ActivityTree.cpp*/
void ActivityTree::OnActStart(User& user)
{
    //告诉大家可以偷能量了
    SendMsgToUser(user, m_ActivityData.MsgType);
}
void ActivityTree::OnActEnd(User& user)
{
    //活动结束了,感谢参与!有缘再见!
    SendMsgToUser(user, m_ActivityData.MsgType);
}
void ActivityTree::OnAddItem(User& user)
{
    //增加能量
    AddEnergy(user);
}
void ActivityTree::OnDelItem(User& user)
{
    //增加能量
    AddEnergy(user);
}
void ActivityTree::StealOtherEnergy(User& user)
{
    //增加能量
    AddEnergy(user);
}
void ActivityTree::GetMyOwnEnergy(User& user)
{
    //增加能量
    AddEnergy(user);
}
void ActivityTree::UseEnergyToWater(User& user)
{
    //减少能量
    DelEnergy(user);
}
bool ActivityTree::CanSendMsgToUser(User& user)
{
    if (IfHaveEnergyCanGet(user)
    {
        return true;
    }
    else 
        return ActivityBase::CanSendMgeToUser(user);
}

海底捞活动只需要在活动开始时发一张兑换券,而蚂蚁森林活动就复杂得多,重写了父类的CanSendMsg方法,其他新增的功能函数可以通过APP的TCP通信协议来调用。
三种不同的活动都是基于ActivityBase稍作修改继承来的子类。

总结 : 装饰器模式其实是将多个功能类的通用功能抽象出来,用一个父类做统一处理,
每一个子类自己可以在继承父类的基础上添加新的功能,大大减少了代码的行数


这一过程看起来很好,其实忽略了一个问题:
如何触发这些事件?
EventTrigger可以在各个事件发生时监听,但是活动的事件又不会自动被监听,还是要手动写上去
所以EventTrigger应该是这样的

/*EventTrigger.cpp*/
//登录事件
void EventTrigger::OnLoginSuccess(User& user)
{
    ActivityTimeLimitTopUp::Instance().OnLoginSuccess(user);
    ActivitySeaBottomCatch::Instance().OnLoginSuccess(user);
    ActivityTree::Instance().OnLoginSuccess(user);
}
//跨天事件
void EventTrigger::OnPassDay(User& user)
{
    ActivityTimeLimitTopUp::Instance().OnPassDay(user);
    ActivitySeaBottomCatch::Instance().OnPassDay(user);
    ActivityTree::Instance().OnPassDay(user);
}
//道具增加事件
void EventTrigger::OnAddItem(User& user, ItemData& cItemData)
{
    ActivityTimeLimitTopUp::Instance().OnAddItem(user);
    ActivitySeaBottomCatch::Instance().OnAddItem(user);
    ActivityTree::Instance().OnAddItem(user);
}
//道具减少事件
void EventTrigger::OnDelItem(User& user, ItemData& cItemData)
{
    ActivityTimeLimitTopUp::Instance().OnDelItem(user);
    ActivitySeaBottomCatch::Instance().OnDelItem(user);
    ActivityTree::Instance().OnDelItem(user);
}

这样看起来挺蠢的,每多一个活动就要在所有的事件里面添加触发代码,每多一个事件就要为所有的活动写实现的代码
真让人头大,但是ActivityBase是没有实体的,整个程序运行过程中我们并没有创建他的实例对象,
其实我们需要的是有一个家伙,把所有活动都记录在内,能在发生事件的时候通知所有的活动去执行对应的事件,这样就省事多了,它就是

观察者模式

首先观察者初始化时要把所有的活动都添加到观察者列表中

enum ACTIVITY_TYPE
{
    ACTIVITY_TYPE_TIMELIMITTOPUP = 1,
    ACTIVITY_TYPE_SEABOTTOMCATCH = 2,
    ACTIVITY_TYPE_TREE = 3;
};
/*ActivityObserver.h*/
class ActivityObserver
{
    public:
        static ActivityObserver& Instance()
        {
            static ActivityObserver s_instance;
            return s_instance;
        }
        //观察者需要初始化自己
        void RegisterActivity();
    private:
        //用map不用vector是考虑对特定活动做处理,索引速度更快
        map<uint64_t, ActivityBase*> m_ActivityMap;

}

/*ActivityObserver.cpp*/
void ActivityObserver::RegisterActivity()
{
    m_ActivityMap.clear();
    m_ActivityMap.insert(std::make_pair(ACTIVITY_TYPE_TIMELIMITTOPUP, ActivityTimeLimitTopUp::Instance()));
    m_ActivityMap.insert(std::make_pair(ACTIVITY_TYPE_SEABOTTOMCATCH, ActivitySeaBottomCatch::Instance()));
    m_ActivityMap.insert(std::make_pair(ACTIVITY_TYPE_TREE, ActivityTree::Instance()));
}

那么观察者什么时候初始化自己呢,我觉得在把它放在进程的启动函数里面是一个不错的选择,
毕竟程序启动以后,观察者就不需要动态的添加活动到自己的监视列表了

void System::Start()
{
    ActivityObserver::Instance().RegisterActivity();
}

接下来我们依然需要把各个事件的触发逻辑完善

/*ActivityObserver.cpp*/
void ActivityObserver::OnLoginSuccess(User& user)
{
    for (auto &it : m_ActivityMap)
    {
        it.sencond->OnLoginSuccess(user)
    }
}
void ActivityObserver::OnPassDay(User& user)
{
    for (auto &it : m_ActivityMap)
    {
        it.sencond->OnPassDay(user)
    }
}
void ActivityObserver::OnAddItem(User& user)
{
    for (auto &it : m_ActivityMap)
    {
        it.sencond->OnAddItem(user)
    }
}
void ActivityObserver::OnDelItem(User& user)
{
    for (auto &it : m_ActivityMap)
    {
        it.sencond->OnDelItem(user)
    }
}

最后再把观察者的事件填到事件触发器中去

/*EventTrigger.cpp*/
//登录事件
void EventTrigger::OnLoginSuccess(User& user)
{
    ActivityObserver::Instance().OnLoginSuccess(user);
}
//跨天事件
void EventTrigger::OnPassDay(User& user)
{
    ActivityObserver::Instance().OnPassDay(user);
}
//道具增加事件
void EventTrigger::OnAddItem(User& user, ItemData& cItemData)
{
    ActivityObserver::Instance().OnAddItem(user);
}
//道具减少事件
void EventTrigger::OnDelItem(User& user, ItemData& cItemData)
{
    ActivityObserver::Instance().OnDelItem(user);
}

这样再加入新的活动,只需要在RegisterActivity中添加就好啦

总结 : 观察者模式将多个实体列为观察对象,在事件中批量处理对象的触发逻辑,减少搬砖工作量


在蚂蚁森林活动里面,聪明的产品忽然想到,应该来一个大家来种树,4个人一起种一棵树,提高大家的积极性
这样在收取能量的时候要告诉一起种树的人能量被收了,要不然别人还会再次收到能量就不好了
于是又让开发加班加点的设计方案,年轻的开发小哥想到那就要多一个房间的概念

/*ActivityTreeRoom.h*/
class ActivityTreeRoom
{
    private:
        vector<uint64_t> m_UserIDList;//Room中有哪些人
        int RoomID;//Room的ID
};
class ActivityTreeRoomMgr
{
    public:
        static ActivityTreeRoomMgr& Instance()
        {
            static ActivityTreeRoomMgr;
            return s_instance;
        }
        //一定有一个user最先开始创建房间
        void CreateRoom(User& user);
        //加入房间
        void JoinRoom(User& user, int RoomID);
        //拉取到Room中的所有人
        void GetRoomMember(const vector<uint64_t>& RoomMember, int RoomID);
    private:
        vector<ActivityTreeRoom> m_Rooms;
};
//为了加快查找,User加入房间后,自己要记录RoomID

再拾能量的时候就需要这么一个函数

/*ActivityTree.cpp*/
void ActivityTree::GetOurEnergy(User& user)
{
    AddEnergy(user);
    vector<uint64_t> RoomMember;
    ActivityTreeRoomMgr::Instance().GetRoomMember(RoomMember, user.RoomID);
    for (uint32_t i = 0; i < RoomMember.size(); ++i)
    {
        User& otherUser = UserMgr::Instance().GetUserbyID(RoomMember[i]);
        //能量已经被捡走了
        SendMsgToUser(otherUser);
    }
}

乍一看是没有什么问题,但是ActivityTree为什么要处理Room的逻辑呢。
他们之间并没有直接的联系,如果再加上下线通知、上线通知,内容会越来越多。

中介者模式

其实就是把应该做的活,统统交给Mgr处理就好了

/*ActivityTree.cpp*/
void ActivityTree::GetOurEnergy(User& user)
{
    AddEnergy(user);
    ActivityTreeRoomMgr::Instance().NotifyMsgToRoomMember(user.RoomID);
}
/*ActivityTreeRoomMgr.cpp*/
void ActivityTreeRoomMgr::NotifyMsgToRoomMember(int RoomID)
{
    vector<uint64_t> RoomMember;
    GetRoomMember(RoomMember, user.RoomID);
    for (uint32_t i = 0; i < RoomMember.size(); ++i)
    {
        User& otherUser = UserMgr::Instance().GetUserbyID(RoomMember[i]);
        SendMsgToUser(otherUser);
    }
}

总结 : 中介者模式是基于两个对象之间不方便直接交流的情况下设定的

发布者

VC-Robot

游戏爱好者,动漫迷,C++修炼中,编程菜鸟,随性

发表评论

电子邮件地址不会被公开。 必填项已用*标注

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