一种基于广播的模块化架构简单实现

背景

  相信不少读者在开发时都有这样的困扰,项目刚开始时,代码量少,效率还可以,可维护性也不错。但随着项目的迭代,添加了各种各样的需求后,代码日积月累臃肿不堪,软件效率开始变得低下,可维护性变差,最后甚至被新人各种吐槽,这时候软件架构就显得尤为重要了。架构是软件开发的基础,直接影响着软件运行效率、代码的可维护性、可扩展性以及可读性。开始新项目时,先设计一个好的架构往往会让往后的开发事半功倍。
  软件架构指的是系统的一些列抽象,用于指导软件系统的开发,并且与具体的业务无关。软件架构通常也描述了系统各个元件之间的逻辑关系,这个逻辑关系也可以简单理解为模块化。本文将向读者推荐一种基于广播的模块化架构,以下简称架构
  本文不打算向读者介绍庞大成熟的架构设计,而是为读者提供一种快速简单有效的解决方案。如果你从来没有考虑过系统架构,希望通过本文能够让你重新思考这一基础问题。如果你对架构设计有一定的经验,也许也能够给你不一样的视角。

约定

  首先我们对架构进行约定,这种架构至少应该满足以下几点要求:

  • 模块化。支持模块自由组合,甚至热插拔。
  • 高内聚低耦合。模块之间应遵循最少知识原则
  • 模块间通讯。模块与模块之间应该有良好的通讯方式。
  • 线程安全。模块通信应该是线程安全的。

广播

  广播是这种架构的主要通讯方式,不同于一般逻辑上的点对点消息,要求有明确消息的发送者与接收者,这不利于降低模块之间的耦合。在这种架构中,消息的发送者与接收者应该尽可能的被弱化,以尽可能的提高消息收发效率,这也是选择广播作为通信方式的原因。与Android系统中的广播类似,系统中的任何模块都可以接收另一个模块发出的广播消息,这也是模块之间唯一的通讯方式。显然,这意味着系统中的每一条消息都没有边界,虽然简单粗暴,但是却能有效降低模块之间的耦合度,提升整个系统的扩展性。
  该架构的广播由一条消息总线实现,消息总线是一个无限循环的线程,负责把模块的消息分发给其它所有模块,包括发送广播的模块本身。每一条消息没有特定的接收者,所有模块都可以接收并处理自己感兴趣的消息,这是广播通讯的特征。模块接收到消息之后,对消息进行加工处理,产生的输出再次通过消息广播出去,以此来驱动整个系统的工作。
  该架构通过广播把模块之间的函数调用、实例持有关系,简单的抽象成了消息驱动关系,模块之间无需持有其它模块的实例,达到了模块化、高内聚低耦合的目的。

实现

  对架构的要求进行约定后,下面我们通过代码来实现该架构的抽象。注意,本文将使用C++11实现,使用其它语言可以参考此实现,同时也会使用部分伪代码。根据架构的约定,我们划分出以下三层结构(图framework):


framework

Processor

  作为整个系统的最顶层,承载着用户接口的职能,系统所有的功能实现都应该在Processor层暴露。同时也包含了整个系统的基础资源,比如系统上下文环境、子模块注册应该置于此。注意,除了基础资源的维护,该层不应该也没必要包含任何系统功能的实现,仅仅是一系列外部接口的集合,所有功能都应该划分到子模块,由模块实现。Processor通过发送广播消息,把用户的输入广播到子模块,相关模块接收广播消息后对输入进行处理,处理完成后把输出广播出去,下一个模块可以处理这条广播消息,如此循环驱动整个系统工作。

Pipeline

  Pipeline是系统的中间层,只负责广播消息的发送,不负责实际业务的处理。Pipeline包括一个消息总线,以及子模块挂载点,所有子模块都应该在注册到Pipeline,并由其管理。模块注册后便可以接收来自消息总线的广播了。本文的消息总线将参考Android系统的Handler实现,具体细节后面会单独发一篇文章讲解,代码可以参考hwvc项目中的实现。

Unit

  Unit是整个系统的最底层,也是系统功能划分的最小单位。Unit可以以具体的业务逻辑或者数据边界做划分,但应遵循最少知识原则。Unit只包含一个最简单的逻辑,接收感兴趣的广播消息,对消息进行加工处理,把处理的结果通过广播发送出去,其它模块也是如此。

message flow.
/// Processor层
class AlAbsProcessor {
protected:
    /// 用于注册一个模块,建议在构造函数中进行注册
    void registerAnUnit(Unit *unit);
    /// 发送一条广播消息
    void postEvent(AlMessage *msg);

private:
    /// Pipeline实例
    UnitPipeline *pipeline = new UnitPipeline();
};
/// Pipeline实现
class UnitPipeline {
public:
    /// 发送一条广播消息,通常由Processor和Unit调用
    void postEvent(AlMessage *msg);
    /// 用于注册一个模块,通常由Processor调用
    int registerAnUnit(Unit *unit);
    
private:
    /// 在线程中分发一条广播到子模块
    void dispatch(AlMessage *msg);

private:
    /// 子线程
    AlHandlerThread *mThread = nullptr;
    AlHandler *mHandler = nullptr;
    /// 子模块列表,通过registerAnUnit注册保存在这里
    vector<Unit *> units;
};
/// Unit层
class Unit {
public:
    /// 用于设置UnitPipeline实例,便于发送广播
    virtual void setController(UnitPipeline *pipeline);

protected:
    /// 发送一条广播消息
    void postEvent(AlMessage *msg);

     /// 广播分发接收函数,通常由UnitPipeline调用
     /// \param msg 事件消息
     /// \return true:我可以处理这个事件,false:无法处理这个事件
    bool dispatch(AlMessage *msg);

private:
    /// Pipeline实例
    UnitPipeline *pipeline = nullptr;
};
UML

  UnitPipeline::dispatch和Unit::dispatch是比较重要的两个函数,其中UnitPipeline::dispatch会在mThread中调用,用于分发消息给Unit。Unit注册到UnitPipeline时会被保存到units变量,发送广播时通过遍历存储Unit的vector,并循环调用Unit::dispatch来分发消息。AlHandlerThread是一个比较核心的类,内部封装了一套完整的消息消费机制,消息的消费是有序的,所以天然的线程安全,Android开发者对这个应该比较熟悉,这里不打算展开讨论,感兴趣的读者可以关注我以后发布的文章。

    mThread = AlHandlerThread::create(name);
    mHandler = new AlHandler(mThread->getLooper(), [this](AlMessage *msg) {
        this->dispatch(msg);
    });
void UnitPipeline::dispatch(AlMessage *msg) {
    for (auto itr = units.cbegin(); itr != units.cend(); itr++) {
        bool ret = (*itr)->dispatch(msg);
    }
}

  AlMessage是Unit::dispatch函数的唯一参数,类似于Android系统的Message类,里面包含了whatarg1arg2obj四个变量,其中what表示消息类型,其它三个都可以作为数据输入,Unit::dispatch函数通过判断what的值来响应不同的处理,处理完成后通过Unit::postEvent函数把结果广播出去,开始下一个处理,代码实现如下。

bool Unit::dispatch(AlMessage *msg) {
    switch (msg->what){
        case 1:
            break;
        case 3:
            break;
        case 4:
            break;
        default:
            break;
    }
    return true;
}

  这里可能有读者察觉到了,通过switch条件判断处理消息,如果Unit要处理的事情越来越多,Unit::dispatch函数将变得越来越长。作为一个“简单实现”怎么可以容忍这种条件判断代码的存在,下面我们来想办法把switch干掉。
  方法很简单,想办法把不同的消息类型一一对应到不同的处理函数即可,即每收到一条广播消息,Unit内部自动把消息分发到对应的函数。我们先定义一个名为EventFunc的类型,该类型是一个"函数模板",只有AlMessage *一个参数和一个bool返回值,这意味每一种消息类型对应的处理函数都应该严格按照该模板定义。

typedef bool (Unit::*EventFunc)(AlMessage *);

  接着新增加一个Event类,用于保存消息和函数的对应关系。

class Event {
public:
    Event(int what, EventFunc handler);

    virtual ~Event();

    bool dispatch(Unit *unit, AlMessage *msg)  {
        return (unit->*handler)(msg);
    }

protected:
    int what = 0;
    EventFunc handler;
};

  然后给Unit添加一个eventMap:map<int, Event *>变量,便于根据消息类型查找对应的处理函数。再新增一个Unit::registerEvent函数,用于在Unit构造函数中注册该Unit感兴趣的消息,以及对应的处理函数。此时Unit::dispatch变可以抽象成一个通用的函数,当UnitPipeline把消息广播到每一个Unit::dispatch函数时,该函数会在eventMap中查找是否注册过该消息类型,如果注册过则取出Event对象,然后通过该对象的dispatch函数调用对应的消息处理函数,至此便在Unit内部再次完成了消息的分发,成功移除冗长的条件判断语句。

map<int, Event *> eventMap;
bool Unit::registerEvent(int what, EventFunc handler) {
    eventMap.insert(pair<int, Event *>(what, new Event(what, handler)));
    return true;
}
bool Unit::dispatch(AlMessage *msg) {
    auto itr = eventMap.find(msg->what);
    if (eventMap.end() != itr) {
        return itr->second->dispatch(this, msg);
    }
    return false;
}

  至此,这个简单的架构便实现了文章开始约定的模块化低耦合模块间通讯线程安全。以上示例代码看似简单,但是通过这种架构,我们可以做很多事情,比如为每一个模块添加生命周期函数(类似于Android系统Activity的生命周期)、对所有模块的资源进行统一管理等,这些都可以轻而易举地实现。

总结

  该架构虽然简单有效,但也难免存在一些问题。

  • 安全性。虽然使用广播简单高效,但是却使得信息没有边界,缺乏安全性。如果不配合靠谱的信息加密,该架构不适用于网络系统。
  • 无法向Processor层发送消息。目前Unit向Processor通讯只能通过持有Processor层指针的形式,该指针可以通过广播发送到Unit,这个显然是不合理的(违背最少知识原则),后面会尝试优化。

  对于这套框架,目前有一个完整的示例,感兴趣的读者可以去阅读AlImageProcessor.cpp中的源码。该示例是一个强大的图片编辑器,如果你是Android开发者,可以运行hwvc项目的demo进行体验,该示例的入口在AlImageActivity

hwvc


Homepage

推荐阅读更多精彩内容

  • 1.NSTimer不准时的原因:(1).RunLoop循环处理时间,每次循环是固定时间,只有在这段时间才会去查看N...
    稻春阅读 580评论 0 3
  • 早上六点起 6:30三十仰卧起坐 7:00早餐 7:30孩子阅读 8:30医院洗牙 10:00阅读30分钟 10:...
    彡丫头阅读 74评论 0 0
  • 他撩你到一半就走了,你还在后悔没有提前答应。其实不过是他在你之后找到了他更喜欢的人。他只是玩玩而已,而你却...
    珂嘤嘤阅读 48评论 0 1
  • 说起法国女人,给人的印象就是时髦和优雅。 她们的穿衣好品味似乎与生俱来,随便一件单品就能穿出无数花样来。 人称“e...
    朱七七来了阅读 452评论 0 3