嵌入式C开发中编程模型

  • 事件驱动
  • 消息驱动
    • 事件驱动vs消息驱动
  • 数据驱动
    • 1. 表驱动法(Table-Driven)
      • 用表驱动法来实现
      • Unix设计原则中的“分离原则”和“表示原则”
    • 2. 基于数据模型编程
    • **数据驱动思考**
  • 总结

事件驱动


事件驱动架构(Event-Driven Architecture)是一种用于设计应用的软件架构和模型,程序的执行流由外部事件来决定,它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。主要包括 4 个基本组件:

  • 事件队列(event queue):接收事件的入口,存储待处理事件
  • 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元
  • 事件通道(event channel):分发器与处理器之间的联系渠道
  • 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作
  • 为什么采用事件驱动模型?
    事件驱动模型也就是我们常说的观察者,或者发布-订阅模型。
    理解它的几个关键点:

    • 首先是一种对象间的一对多的关系;最简单的如交通信号灯,信号灯是目标(一方),行人注视着信号灯(多方);
    • 当目标发送改变(发布),观察者(订阅者)就可以接收到改变;
    • 观察者如何处理(如行人如何走,是快走/慢走/不走,目标不会管的),目标无需干涉;所以就松散耦合了它们之间的关系。

    许多现代应用设计都是由事件驱动的,事件驱动应用可以用任何一种编程语言来创建,因为事件驱动本身是一种编程方法,而不是一种编程语言。

    • 松耦合——服务不需要(也不应该)知道或依赖于其他服务。在使用事件时,服务独立运行,不了解其他服务,包括其实现细节和传输协议。事件模型下的服务可以独立地、更容易地更新、测试和部署。

    • 易扩展——通过高度独立和解耦的事件处理器自然地实现了可扩展性。每个事件处理器都可以单独扩展,从而实现细粒度的可扩展性。

    • 恢复支持——带有队列的事件驱动架构可以通过“重播”过去的事件来恢复丢失的工作。当用户需要恢复时,这对于防止数据丢失非常有用。

    事件驱动架构可以最大程度减少耦合度,因此是现代化分布式应用架构的理想之选。

  • 深入理解事件驱动

    1. 异步处理和主动轮训,要理解事件驱动和程序,就需要与非事件驱动的程序进行比较。实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu。
    2. IO模型,事件驱动框架一般是采用Reactor模式或者Proactor模式的IO模型。
      Reactor模式其中非常重要的一环就是调用函数来完成数据拷贝,这部分是应用程序自己完成的,内核只负责通知监控的事件到来了,所以本质上Reactor模式属于非阻塞同步IO。

      Proactor模式,借助于系统本身的异步IO特性,由操作系统进行数据拷贝,在完成之后来通知应用程序来取就可以,效率更高一些,但是底层需要借助于内核的异步IO机制来实现,可能借助于DMA和Zero-Copy技术来实现,理论上性能更高。
      当前Windows系统通过IOCP实现了真正的异步I/O,而在Linux 系统的异步I/O还不完善,比如Linux中的boost.asio模块就是异步IO的支持,但是目前Linux系统还是以基于Reactor模式的非阻塞同步IO为主。
    3. 事件队列,事件驱动的程序必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件,这个事件队列,可以采用消息队列。
    4. 事件串联,事件驱动的程序的行为,完全受外部输入的事件控制,所以事件驱动框架中,存在大量处理程序逻辑,可以通过事件把各个处理流程关联起来。
    5. 顺序性和原子化,事件驱动的程序可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往被用于保证某些过程的顺序性和原子化。
  • 事件驱动的缺点

    • 事件驱动架构,就是通过引入中间层 来实现事件发布-订阅机制进行组件解耦,看似能带来不少诱人的优点,也必然会增加系统的复杂度,间接增加开发难度和维护难度。
    • 事件驱动架构改变了编程思维,将完整的功能过程,拆解为了不同的异步事件处理,也丧失了连贯的流程处理能力。如果事件数量众多,就容易在“事件丛林”中迷了路,比如中断风暴,惊群效应等。
  • 常用的事件驱动框架

    • select
    • poll
    • epoll
    • libev
    • 中断系统

消息驱动


消息驱动和事件驱动很类似,都是先有一个事件,然后产生一个相应的消息,再把消息放入消息队列,由需要的项目获取。他们只是一些细微区别,一般都采用相同框架,细微的区别:

  • 消息驱动:生产者A发送一个消息到消息队列,消费者B收到该消息。生产者A很明确这个消息是发给消费者B的。通常是P2P模式。
  • 事件驱动:生产者A发出一个事件,消费者B或者消费者C收到这个事件,或者没人收到这个事件,生产者A只会产生一个事件,不关心谁会处理这个事件 ,通常是发布-订阅模型。

现代软件系统是跨多个端点运行并通过大型网络连接的分布式系统。例如,考虑一位航空公司客户通过 Web 浏览器购买机票。该订单可能会通过API,然后通过一系列返回结果的过程。这些来回通信的一个术语是消息传递。在消息驱动架构中,这些 API 调用看起来非常像一个函数调用:API 知道它在调用什么,期待某个结果并等待该结果。

消息驱动的优点

  • 开发难度低:消息驱动类似经典的编程模型,调用一个函数,等待一个结果,对结果做一些事情,编程简单快速,开发难度低。
  • 方便调试维护:因为编程逻辑清晰简单,流程清晰,调试起来更加直接方便,后期维护也容易。
/** Structure used for queue entries that can be either simple messages, conditional or timed. */
typedef struct AppMessage
{struct AppMessage *next;     /**< point to next msg in message list */uint32 due;                  /**< Millisecond time to deliver this message */union{Task task;               /**< Receiving task (if unicast) - typedef void (*Handler)(Task t, MessageId id, Message m); */Task *tlist;             /**< Ptr to receiving task list (if multicast) */} t;void *message;               /**< Pointer to the message payload */const void *condition_addr;  /**< Pointer to condition value */uint16 id;                   /**< Message ID */CONDITION_WIDTH c_width;     /**< Width of condition value(16bit or 32bit or unuse) */unsigned int multicast:1;    /**< If multicast, task is a null-terminated list */
} AppMessage;/*** Send a message after a time delay* @param task Pointer to task(s) to deliver the message to* @param multicast Whether 'task' is a pointer to a list or not* @param id ID of the message* @param message The message contents* @param delay Number of milliseconds to wait before delivering the message* @param c Optional pointer to a condition which must be zero for the* message to be delivered.* @param c_width Whether to test the condition as a 16 or 32-bit variable* or @c CONDITION_WIDTH_UNUSED if @c c is NULL.*/
void message_send_later(Task *task, bool multicast, uint16 id, void *message, uint32 delay, const void * c, CONDITION_WIDTH c_width)
{uint32 timenow = get_milli_time();a = pnew(AppMessage);......a->due = delay + timenow;......if(insert(a)){event_trigger();}......}/*** Call from the scheduler with a message posted to this scheduler queue to* cause a message to be delivered from the message queue. This function* posts a new scheduler message if there is more work to be done (messages in the queue).* ppriv Unused context*/
void api_sched_msg_handler(void **ppriv)
{AppMessage **p = &vm_message_queue;/* Find the first message which isn't blocked on a condition */while((a = *p) != 0){const void * c = a->condition_addr;if(c==0 || get_message_condition_value(c, a->c_width) == 0){break;/* No condition or it's satisfied */}p = &a->next;}if(a){/* Found a message */uint32 now  = get_milli_time();int32 delta = VM_DIFF(a->due, now);......if(delta == 0) {....../* Unlink the message from the queue */*p = a->next;/* Deliver the message to the handler(s) */if (!a->multicast){if(a->t.task && a->t.task->handler){VALIDATE_FN_PTR(a->t.task->handler);a->t.task->handler(a->t.task, a->id, a->message);}}......pfree(a);event_trigger();}}
}

常用的消息驱动框架

  • API网关
  • gRPC
  • 微服务架构

事件驱动vs消息驱动

消息驱动的方法与事件驱动的方法一样有很多优点和缺点,但每种方法都有自己最适合的情况。

  • 消息感觉很像经典的编程模型:调用一个函数,等待一个结果,对结果做一些事情。除了为大多数程序员所熟悉之外,这种结构还可以使调试更加直接。另一个优点是消息“阻塞”,这意味着呼叫和响应的各个单元坐下来等待轮到接收者进行处理。

  • 事件驱动系统使单个事件易于隔离测试。然而,这种与整个应用系统的分离也抑制了这些单元报告错误、重试调用程序甚至只是向用户确认进程已完成的能力。换句话说:当事件驱动系统中发生错误时,很难追踪到底是哪里出了问题。可观察性工具正在应对调试复杂事件链的挑战。但是,添加到业务交易交叉点的每个工具都会为负责管理这些工作流的程序员带来另一层复杂性。

  • 如果通信通常以一对一的方式进行,并且优先接收定期状态更新或确认,那么您将倾向于使用基于消息的方法。但是,如果系统之间的交互特别复杂,并且确认和状态更新导致的延迟使得等待它们变得不切实际,那么事件驱动的设计可能更合适。但是请记住,大多数大型组织最终会采用混合策略,一些面向客户/API 调用使用消息驱动,而企业本身使用事件驱动。因此,尽可能多地熟悉两者并没有什么坏处。

数据驱动

数据驱动核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应该尽可能的将设计的复杂度从程序代码转移至数据。

例子:
假设有一个程序,需要处理其他程序发送的消息,消息类型是字符串,每个消息都需要一个函数进行处理。第一印象,我们可能会这样处理:

上面的消息类型取自sip协议(不完全相同,sip协议借鉴了http协议),消息类型可能还会增加。看着常常的流程可能有点累,检测一下中间某个消息有没有处理也比较费劲,而且,每增加一个消息,就要增加一个流程分支。

按照数据驱动编程的思路,可能会这样设计:

下面这种思路的优势:

  1. 可读性更强,消息处理流程一目了然。
  2. 更容易修改,要增加新的消息,只要修改数据即可,不需要修改流程。
  3. 重用,第一种方案的很多的else if其实只是消息类型和处理函数不同,但是逻辑是一样的。下面的这种方案就是将这种相同的逻辑提取出来,而把容易发生变化的部分提到外面。

很多设计思路背后的原理其实都是相通的,隐含在数据驱动编程背后的实现思想包括:

  1. 控制复杂度。通过把程序逻辑的复杂度转移到人类更容易处理的数据中来,从而达到控制复杂度的目标。
  2. 隔离变化。像上面的例子,每个消息处理的逻辑是不变的,但是消息可能是变化的,那就把容易变化的消息和不容易变化的逻辑分离。
  3. 机制和策略的分离。和第二点很像,本书中很多地方提到了机制和策略。上例中,我的理解,机制就是消息的处理逻辑,策略就是不同的消息处理。

数据驱动编程可以用来做什么

1. 表驱动法(Table-Driven)

消除重复代码,考虑一个消息(事件)驱动的系统,系统的某一模块需要和其他的几个模块进行通信。它收到消息后,需要根据消息的发送方,消息的类型,自身的状态,进行不同的处理。比较常见的一个做法是用三个级联的switch分支实现通过硬编码来实现:

switch(sendMode)
{case:
}
switch(msgEvent)
{case:
}
switch(myStatus)
{case:
}

这种方法的缺点:

  • 可读性不高:找一个消息的处理部分代码需要跳转多层代码。
  • 过多的switch分支,这其实也是一种重复代码。他们都有共同的特性,还 可以再进一步进行提炼。
  • 可扩展性差:如果为程序增加一种新的模块的状态,这可能要改变所有的 消息处理的函数,非常的不方便,而且过程容易出错。
  • 程序缺少核心主干:缺少一个能够提纲挈领的主干,程序的主干被淹没在 大量的代码逻辑之中。

用表驱动法来实现

根据定义的三个枚举:模块类型,消息类型,自身模块状态,定义一个函数跳转表:

typedef struct  __EVENT_DRIVE
{MODE_TYPE mod;//消息的发送模块EVENT_TYPE event;//消息类型STATUS_TYPE status;//自身状态EVENT_FUN eventfun;//此状态下的处理函数指针
}EVENT_DRIVE;EVENT_DRIVE eventdriver[] = //这就是一张表的定义,不一定是数据库中的表。也可以使自己定义的一个结构体数组。
{{MODE_A, EVENT_a, STATUS_1, fun1}{MODE_A, EVENT_a, STATUS_2, fun2}{MODE_A, EVENT_a, STATUS_3, fun3}{MODE_A, EVENT_b, STATUS_1, fun4}{MODE_A, EVENT_b, STATUS_2, fun5}{MODE_B, EVENT_a, STATUS_1, fun6}{MODE_B, EVENT_a, STATUS_2, fun7}{MODE_B, EVENT_a, STATUS_3, fun8}{MODE_B, EVENT_b, STATUS_1, fun9}{MODE_B, EVENT_b, STATUS_2, fun10}
};int driversize = sizeof(eventdriver) / sizeof(EVENT_DRIVE)//驱动表的大小EVENT_FUN GetFunFromDriver(MODE_TYPE mod, EVENT_TYPE event, STATUS_TYPE status)//驱动表查找函数
{int i = 0;for (i = 0; i < driversize; i ++){if ((eventdriver[i].mod == mod) && (eventdriver[i].event == event) &&(eventdriver[i].status == status)){return eventdriver[i].eventfun;}}return NULL;
}

月天校验:对给定年份和月份的天数进行校验(需区分平年和闰年)

#define MONTH_OF_YEAR 12    /* 一年中的月份数 *//* 闰年:能被4整除且不能被100整除,或能被400整除 */
#define IS_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))/* 平年中的各月天数,下标对应月份 */
INT8U aDayOfCommonMonth[MONTH_OF_YEAR] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};INT8U ucMaxDay = 0;
if((OnuTime.Month == 2) && (IS_LEAP_YEAR(OnuTime.Year)))ucMaxDay = aDayOfCommonMonth[1] + 1;
elseucMaxDay = aDayOfCommonMonth[OnuTime.Month-1];if((OnuTime.Day < 1) || (OnuTime.Day > ucMaxDay)
{CtcOamLog(FUNCTION_Pon,"Month %d doesn't have this Day: %d(1~%d)!!!\n",OnuTime.Month, OnuTime.Day, ucMaxDay);retcode = S_ERROR;
}

名称构造:根据WAN接口承载的业务类型(Bitmap)构造业务类型名称字符串。

/* 获取var变量第bit位,编号从右至左 */
#define  GET_BIT(var, bit)   (((var) >> (bit)) & 0x1)
const CHAR* paSvrNames[] = {"_INTERNET", "_TR069", "_VOIP", "_OTHER"};
const INT8U ucSvrNameNum = sizeof(paSvrNames) / sizeof(paSvrNames[0]);VOID SetServerType(CHAR *pszSvrType, INT16U wSvrType)
{INT8U ucIdx = 0;for(; ucIdx < ucSvrNameNum; ucIdx++){if(1 == GET_BIT(wSvrType, ucIdx))strcat(pszSvrType, paSvrNames[ucIdx]);}
}

版本控制:控制OLT与ONU之间的版本协商。ONU本地设置三比特控制字,其中bit2(MSB)~bit0(LSB)分别对应0x21、0x30和0xAA版本号

pstSendTlv->ucLength = 0x1f;
if (gOamCtrlCode == 0)
{vosMemCpy(pstSendTlv->aucVersionList, ctc_oui, 3);pstSendTlv->aucVersionList[3] = 0x30;vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);pstSendTlv->aucVersionList[7] = 0x21;vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);pstSendTlv->aucVersionList[11] = 0x20;vosMemCpy(&(pstSendTlv->aucVersionList[12]), ctc_oui, 3);pstSendTlv->aucVersionList[15] = 0x13;vosMemCpy(&(pstSendTlv->aucVersionList[16]), ctc_oui, 3);pstSendTlv->aucVersionList[19] = 0x01;vosMemCpy(&(pstSendTlv->aucVersionList[20]), ctc_oui, 3);pstSendTlv->aucVersionList[23] = 0xaa;
}
else if (gOamCtrlCode == 1)
{vosMemCpy(pstSendTlv->aucVersionList, ctc_oui, 3);pstSendTlv->aucVersionList[3] = 0x30;vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);pstSendTlv->aucVersionList[7] = 0x21;vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);pstSendTlv->aucVersionList[11] = 0x20;vosMemCpy(&(pstSendTlv->aucVersionList[12]), ctc_oui, 3);pstSendTlv->aucVersionList[15] = 0x13;vosMemCpy(&(pstSendTlv->aucVersionList[16]), ctc_oui, 3);pstSendTlv->aucVersionList[19] = 0x01;
}
//此处省略gOamCtrlCode == 2~6的处理代码
else if (gOamCtrlCode == 7)
{vosMemCpy(&(pstSendTlv->aucVersionList), ctc_oui, 3);pstSendTlv->aucVersionList[3] = 0x20;vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);pstSendTlv->aucVersionList[7] = 0x13;vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);pstSendTlv->aucVersionList[11] = 0x01;
}

消息处理:终端输入不同的打印命令,调用相应的打印函数,以控制不同级别的打印

typedef struct{OAM_LOG_OFF = (INT8U)0,OAM_LOG_ON  = (INT8U)1
}E_OAM_LOG_MODE;
typedef FUNC_STATUS (*OamLogHandler)(VOID);
typedef struct{CHAR           *pszLogCls;    /* 打印级别 */E_OAM_LOG_MODE eLogMode;      /* 打印模式 */OamLogHandler  fnLogHandler;  /* 打印函数 */
}T_OAM_LOG_MAP;T_OAM_LOG_MAP gOamLogMap[] = {{"all",         OAM_LOG_OFF,       noanylog},{"oam",         OAM_LOG_OFF,       nologOam},//... ...{"version",     OAM_LOG_OFF,       nologVersion},{"all",         OAM_LOG_ON,        logall},{"oam",         OAM_LOG_ON,        logOam},//... ...{"version",     OAM_LOG_ON,        logVersion}
};
INT32U gOamLogMapNum = sizeof(gOamLogMap) / sizeof(T_OAM_LOG_MAP);VOID logExec(CHAR *pszName, INT8U ucSwitch)
{INT8U ucIdx = 0;for(; ucIdx < gOamLogMapNum; ucIdx++){if((ucSwitch == gOamLogMap[ucIdx].eLogMode) &&(!strcasecmp(pszName, gOamLogMap[ucIdx].pszLogCls));{gOamLogMap[ucIdx].fnLogHandler();return;}}if(ucIdx == gOamLogMapNum){printf("Unknown LogClass(%s) or LogMode(%d)!\n", pszName, ucSwitch);return;}
}

这种方法的好处:

  • 提高了程序的可读性。一个消息如何处理,只要看一下驱动表就知道,非常明显。
  • 减少了重复代码。这种方法的代码量肯定比第一种少。为什么?因为它把一些重复的东西:switch分支处理进行了抽象,把其中公共的东西——根据三个元素查找处理方法抽象成了一个函数GetFunFromDriver外加一个驱动表。
  • 可扩展性。注意这个函数指针,他的定义其实就是一种契约,类似于java中的接口,c++中的纯虚函数,只有满足这个条件(入参,返回值),才可以作为一个事件的处理函数。这个有一点插件结构的味道,你可以对这些插件进行方便替换,新增,删除,从而改变程序的行为。而这种改变,对事件处理函数的查找又是隔离的(也可以叫做隔离了变化)。
  • 程序有一个明显的清晰主干。
  • 降低了复杂度。通过把程序逻辑的复杂度转移到人类更容易处理的数据中来,从而达到控制复杂度的目标。

Unix设计原则中的“分离原则”和“表示原则”

在《Unix编程艺术》和《代码大全2》中均认为人类阅读复杂数据结构远比复杂的控制流程容易,即相对于程序逻辑,人类更擅长于处理数据。

  • 分离原则:策略同机制分离,接口同引擎分离
    机制即提供的功能;策略即如何使用功能。
    策略的变化要远远快于机制的变化。将两者分离,可以使机制相对保持稳定,而同时支持策略的变化。
    代码大全中提到“隔离变化”的概念,以及设计模式中提到的将易变化的部分和不易变化的部分分离也是这个思路。

  • 表示原则:把知识叠入数据以求逻辑质朴而健壮
    即使最简单的程序逻辑让人类来验证也很困难,但就算是很复杂的数据,对人类来说,还是相对容易推导和建模的。数据比编程逻辑更容易驾驭。在复杂数据和复杂代码中选择,宁可选择前者。更进一步,在设计中,应该主动将代码的复杂度转移到数据中去。
    在“消息处理”示例中,每个消息处理的逻辑不变,但消息可能是变化的。将容易变化的消息和不容易变化的查找逻辑分离,即“隔离变化”。此外,该例也体现消息内部的处理逻辑(机制)和不同的消息处理(策略)分离。

数据驱动编程可以应用于:

  1. 函数级设计,如本文示例。
  2. 程序级设计,如用表驱动法实现状态机。
  3. 系统级设计,如DSL。
  • Booch的《面向对象分析与设计》一书中,提到所有的程序设计语言大概有3个源流:结构化编程;面向对象编程;数据驱动编程。我认为数据驱动编程的本质是“参数化抽象”的思想,不同于OO的“规范化抽象”的思想。
  • 数据驱动编程在网络游戏开发过程中很常用,但是少有人专门提到这个词。数据驱动编程有很多名字:元编程,解释器/虚拟机,LOP/微语言/DSL等。包括声明式编程、标记语言、甚至所见即所得的拖放控件,都算是数据驱动编程的一种吧。
    数据驱动编程可以帮助处理复杂性,和结构化编程、OO 均可相容。(正交的角度)
  • 将变和不变的部分分离,策略和机制分离,由此联想到的还有:(数据和代码的分离,微语言和解释器的分离,被生成代码和代码生成器的分离);更近一步:(微内核插件式体系结构)
  • 元编程应该说是更加泛化的数据驱动编程,元编程不是新加入一个间接层,而是退居一步,使得当前的层变成一个间接层。元编程分为静态元编程(编译时)和动态元编程(运行时),静态元编程本质上是一种 代码生成技术或者编译器技术;动态元编程一般通过解释器(或虚拟机)加以实现。
  • 数据驱动编程当然也不应该说是“反抽象的”,但的确与“OO抽象”的思维方式是迥然不同,泾渭分明的,如TAOUP一书中所述:“在Unix的模块化传统和围绕OO语言发展起来的使用模式之间,存在着紧张的对立关系”应该说数据驱动编程的思路与结构化编程和OO是正交的,更类似一种“跳出三界外,不在五行中”的做法

2. 基于数据模型编程

  • 基于Yang模型编程(DSL),YANG是一种语言,是用来建立数据模型的语言,可以通过定义业务数据模型,自动生成对应数据处理逻辑(比如参数校验,范围,存储方式,权限控制等),典型的数据驱动编程;
  • Linux内核DTS设备树模型,删除大量hardcode,精简内核驱动代码。
  • 基于xml,protobuf数据模型编程,界面显示,web配置逻辑,RPC微服务等;

数据驱动思考

  • 它不是一个全新的编程模型:它只是一种设计思路,而且历史悠久,在unix/linux社区应用很多;
  • 它不同于面向对象设计中的数据:“数据驱动编程中,数据不但表示了某个对象的状态,实际上还定义了程序的流程;OO看重的是封装,而数据驱动编程看重的是编写尽可能少的代码。”
  • 数据压倒一切。如果选择了正确的数据结构并把一切组织的井井有条,正确的算法就不言自明。编程的核心是数据结构,而不是算法。——Rob Pike
  • 程序员束手无策,只有跳脱代码,直起腰,仔细思考数据才是最好的行动。表达式编程的精髓。——Fred Brooks
  • 数据比程序逻辑更易驾驭。尽可能把设计的复杂度从代码转移至数据是个好实践。——《unix编程艺术》作者。

总结

  • 设计模式(古典)主要针对OOP领域编程设计方法的抽象。这里的编程模型,主要是针对业务编程框架的抽象。

  • 消息驱动和事件驱动,本身有很多相似地方,消息驱动主要代表是经典跨进程通信架构,让消息处理和函数调用一样,逻辑依然可以保持清晰简单。而事件驱动采取异步处理方式,最大化解耦,让程序耦合更低,框架更易扩展,两种编程模型都有各自优缺点,只有根据具体的场景找到一种合适使用方法。

  • 数据驱动是一种新的编程思考,坚持"data as program"准则,把处理逻辑数据化,这样可以通过不同数据配置来实现不同的逻辑,让核心代码更精炼简单,框架更易扩展。

本文大部分摘录 嵌入式中重要的编程模型,少量自己修改整合而来!

嵌入式C开发中编程模型——重点事件驱动和表驱动相关推荐

  1. 嵌入式开发-浅谈嵌入式MCU开发中的三个常见误区

    浅谈嵌入式MCU开发中的三个常见误区 原创 2017-09-30 胡恩伟 汽车电子expert成长之路 目录 (1)嵌入式MCU与MPU的区分 (2)误区一:MCU的程序都是存储在片上Flash上,然 ...

  2. 嵌入式Qt开发中配置文件的尝试——TOML

    嵌入式Qt开发中配置文件的尝试--TOML 1 起因 2 关于 QSetting 3 重新寻找方案 TOML 3.1 TOML 初体验 4 后话 1 起因 在大多数项目中,系统启动时都需要读取一份配置 ...

  3. 如何解决嵌入式培训开发中的PCB设计问题?

    不管是嵌入式培训开发还是学习嵌入式的过程中肯定都是或多或少都能遇到一些小问题的,但是不管这个问题有多小如果说你不解决好的话那么你就很难能够进行到下一步的.今天粤嵌科技就也来给大家说下如何解决嵌入式培训 ...

  4. 电子地图开发中栅格模型与矢量模型的区别

    随着gis行业的不断发展,加上电子地图的日益普及,使得如今市场上与电子地图相关的产品越来越多,从事电子地图开发的企业也如雨后春笋般纷纷出现,既有像百度.谷歌.超图等这些行业巨头,也有像上海为卓.广州哈 ...

  5. JAVA的嵌入式脚本开发(中)

    本文将分以下几个小节来学习: Java的JavaScript脚本引擎 脚本开发中涉及到的类和接口简介 开发步骤 脚本与java数据传递 脚本执行上下文 作用域 方法的调用 编译脚本 脚本与java数据 ...

  6. 实际开发中,是否会在数据库表中设置外键?

    本科学习数据库的时候,书上明确的写了对于多对多关系必须要创建外键,可是最近在跟师兄做一个B/S架构的项目,发现所设计的数据库表虽然是多对多关系但并没有要求外键,查了一下之后才发现目前的大型系统中(尤其 ...

  7. 嵌入式linux开发中常见的虚拟机和主机的文件共享问题

    在嵌入式开发中常会用到linux系统,而有些单位必须使用虚拟机安装linux系统. 在使用vmware安装ubuntu系统后,要创建一个用作虚拟机和主机之间的共享目录. 但是这样做完之后,在ubunt ...

  8. 回调函数的注册机制为什么会在嵌入式固件开发中应用如此广泛?

    击上方"嵌入式应用研究院",选择"置顶/星标公众号" 干货福利,第一时间送达! 在我们平时开发STM32或者其它单片机时,我们经常都会用到原厂提供的固件库函数, ...

  9. 嵌入式项目开发中的问题及解决方法

    High-precisionmeasurement 司南板卡串口阻塞读(select)超时(timeout)原因: 1.波特率设置不正确,出厂时为115200.因为BSP设计时加入的uartlite模 ...

最新文章

  1. 32位处理器是什么意思
  2. 在大规模系统中使用Scala
  3. 你的Wi-Fi 还安全吗?全球重大漏洞WPA2 KRACK 详细分析报告
  4. c语言电报关系的题目,c语言所有题目以跟答案.doc
  5. 面向对象的代码生成方法
  6. 单片机sleep函数的头文件_单片机代码模块化设计思想浅谈
  7. PWN-PRACTICE-BUUCTF-2
  8. sql server 2008 r2卸载重装_免费下载:Intouch软件、Windows操作系统、SQL数据库,VB6.0、C#...
  9. Xception,Inception-ResNet,SENet(Squeeze-and-Excitation)
  10. Sublime Text 2搭建Java开发环境
  11. 【es】es 分布式一致性原理剖析(二)-Meta篇
  12. 洛谷 [P1387] 最大正方形
  13. sql 获取当前整点时间,当前时间减去12小时
  14. 在建立与服务器的连接时出错。provider: TCP 提供程序, error: 0 - 由于目标机器积极拒绝,无法连接。)...
  15. Linux里面qt的可执行文件在命令行中可以打开,双击可执行文件打不开
  16. C语言实现求斐波那契数列中的第n项
  17. Office - 0x4004F00C解决方法
  18. 并行计算(一):简介
  19. [源码解析] PyTorch 分布式之弹性训练(4)---Rendezvous 架构和逻辑
  20. 大道至简(原标题:少是指数级的多)

热门文章

  1. 用java实现KFC收银:请同学们参考肯德基官网的信息模拟肯德基快餐店的收银系统
  2. unity 鼠标悬停事件
  3. 16秋南开计算机应用答案,南开大学16秋学期《计算机应用基础》在线作业.doc
  4. 《小强升职记》摘录笔记
  5. Silk和PCM数据之间的换转
  6. Introduction to the Spring Framework By Rod Johnson(完整中文翻译版)
  7. [mysql]-inception和archer安装
  8. 5G+VoLTE,真香!
  9. Windows VS2017使用GoogleTest
  10. Linux下ppp拨号+3G模块(evdo 中兴MC8630模块)