背景
相信不少读者在开发时都有这样的困扰,项目刚开始时,代码量少,效率还可以,可维护性也不错。但随着项目的迭代,添加了各种各样的需求后,代码日积月累臃肿不堪,软件效率开始变得低下,可维护性变差,最后甚至被新人各种吐槽,这时候软件架构就显得尤为重要了。架构是软件开发的基础,直接影响着软件运行效率、代码的可维护性、可扩展性以及可读性。开始新项目时,先设计一个好的架构往往会让往后的开发事半功倍。 软件架构指的是系统的一些列抽象,用于指导软件系统的开发,并且与具体的业务无关。软件架构通常也描述了系统各个元件之间的逻辑关系,这个逻辑关系也可以简单理解为模块化。本文将向读者推荐一种基于广播的模块化架构,以下简称架构。 本文不打算向读者介绍庞大成熟的架构设计,而是为读者提供一种快速简单有效的解决方案。如果你从来没有考虑过系统架构,希望通过本文能够让你重新思考这一基础问题。如果你对架构设计有一定的经验,也许也能够给你不一样的视角。
约定
首先我们对架构进行约定,这种架构至少应该满足以下几点要求:
- 模块化。支持模块自由组合,甚至热插拔。
- 高内聚低耦合。模块之间应遵循最少知识原则。
- 模块间通讯。模块与模块之间应该有良好的通讯方式。
- 线程安全。模块通信应该是线程安全的。
广播
广播是这种架构的主要通讯方式,不同于一般逻辑上的点对点消息,要求有明确消息的发送者与接收者,这不利于降低模块之间的耦合。在这种架构中,消息的发送者与接收者应该尽可能的被弱化,以尽可能的提高消息收发效率,这也是选择广播作为通信方式的原因。与Android系统中的广播类似,系统中的任何模块都可以接收另一个模块发出的广播消息,这也是模块之间唯一的通讯方式。显然,这意味着系统中的每一条消息都没有边界,虽然简单粗暴,但是却能有效降低模块之间的耦合度,提升整个系统的扩展性。 该架构的广播由一条消息总线实现,消息总线是一个无限循环的线程,负责把模块的消息分发给其它所有模块,包括发送广播的模块本身。每一条消息没有特定的接收者,所有模块都可以接收并处理自己感兴趣的消息,这是广播通讯的特征。模块接收到消息之后,对消息进行加工处理,产生的输出再次通过消息广播出去,以此来驱动整个系统的工作。 该架构通过广播把模块之间的函数调用、实例持有关系,简单的抽象成了消息驱动关系,模块之间无需持有其它模块的实例,达到了模块化、高内聚低耦合的目的。
实现
对架构的要求进行约定后,下面我们通过代码来实现该架构的抽象。注意,本文将使用C 11实现,使用其它语言可以参考此实现,同时也会使用部分伪代码。根据架构的约定,我们划分出以下三层结构(图framework):
Processor
作为整个系统的最顶层,承载着用户接口的职能,系统所有的功能实现都应该在Processor层暴露。同时也包含了整个系统的基础资源,比如系统上下文环境、子模块注册应该置于此。注意,除了基础资源的维护,该层不应该也没必要包含任何系统功能的实现,仅仅是一系列外部接口的集合,所有功能都应该划分到子模块,由模块实现。Processor通过发送广播消息,把用户的输入广播到子模块,相关模块接收广播消息后对输入进行处理,处理完成后把输出广播出去,下一个模块可以处理这条广播消息,如此循环驱动整个系统工作。
Pipeline
Pipeline是系统的中间层,只负责广播消息的发送,不负责实际业务的处理。Pipeline包括一个消息总线,以及子模块挂载点,所有子模块都应该在注册到Pipeline,并由其管理。模块注册后便可以接收来自消息总线的广播了。本文的消息总线将参考Android系统的Handler实现,具体细节后面会单独发一篇文章讲解,代码可以参考hwvc项目中的实现。
Unit
Unit是整个系统的最底层,也是系统功能划分的最小单位。Unit可以以具体的业务逻辑或者数据边界做划分,但应遵循最少知识原则。Unit只包含一个最简单的逻辑,接收感兴趣的广播消息,对消息进行加工处理,把处理的结果通过广播发送出去,其它模块也是如此。
代码语言:javascript复制/// Processor层
class AlAbsProcessor {
protected:
/// 用于注册一个模块,建议在构造函数中进行注册
void registerAnUnit(Unit *unit);
/// 发送一条广播消息
void postEvent(AlMessage *msg);
private:
/// Pipeline实例
UnitPipeline *pipeline = new UnitPipeline();
};
代码语言:javascript复制/// 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;
};
代码语言:javascript复制/// 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;
};
UnitPipeline::dispatch和Unit::dispatch是比较重要的两个函数,其中UnitPipeline::dispatch会在mThread中调用,用于分发消息给Unit。Unit注册到UnitPipeline时会被保存到units变量,发送广播时通过遍历存储Unit的vector,并循环调用Unit::dispatch来分发消息。AlHandlerThread是一个比较核心的类,内部封装了一套完整的消息消费机制,消息的消费是有序的,所以天然的线程安全,Android开发者对这个应该比较熟悉,这里不打算展开讨论,感兴趣的读者可以关注我以后发布的文章。
代码语言:javascript复制mThread = AlHandlerThread::create(name);
mHandler = new AlHandler(mThread->getLooper(), [this](AlMessage *msg) {
this->dispatch(msg);
});
代码语言:javascript复制void UnitPipeline::dispatch(AlMessage *msg) {
for (auto itr = units.cbegin(); itr != units.cend(); itr ) {
bool ret = (*itr)->dispatch(msg);
}
}
AlMessage是Unit::dispatch函数的唯一参数,类似于Android系统的Message类,里面包含了what、arg1、arg2、obj四个变量,其中what表示消息类型,其它三个都可以作为数据输入,Unit::dispatch函数通过判断what的值来响应不同的处理,处理完成后通过Unit::postEvent函数把结果广播出去,开始下一个处理,代码实现如下。
代码语言:javascript复制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返回值,这意味每一种消息类型对应的处理函数都应该严格按照该模板定义。
代码语言:javascript复制typedef bool (Unit::*EventFunc)(AlMessage *);
接着新增加一个Event类,用于保存消息和函数的对应关系。
代码语言:javascript复制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内部再次完成了消息的分发,成功移除冗长的条件判断语句。
代码语言:javascript复制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。