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

事件驱动

在这里插入图片描述
事件驱动架构(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;
else
    ucMaxDay = 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语言开发》是一本探讨嵌入式C语言开发测试驱动开发(TDD)方法的书籍。TDD是一种先写测试用例,再编写代码实现这些测试用例的开发方式,其主要目的是保证代码的质量和可维护性。 这本书主要通过实例演示如何实现嵌入式C语言项目的测试驱动开发过程。它介绍了如何使用Unity测试框架和CMock模拟框架来进行单元测试,使得嵌入式软件开发过程的测试更加简单且高效。书还详细讨论了如何使用不同的测试策略和设计模式来提高代码的可测性和可维护性。 此外,书还介绍了如何使用版本控制系统和持续集成工具来增强测试驱动嵌入式C语言开发过程。通过使用这些工具,开发人员可以更好地管理代码库的版本,并及时发现和修复潜在的问题。 《测试驱动嵌入式C语言开发》适合具有一定嵌入式开发基础的读者,特别是那些希望学习如何应用测试驱动开发方法来提高嵌入式软件质量的开发人员。这本书内容丰富,实例详细,可以帮助读者更好地理解和掌握测试驱动开发方法在嵌入式C语言开发的应用。 总之,这本书通过具体实例和详细讲解,向读者展示了如何使用测试驱动开发方法进行嵌入式C语言开发,并提高软件质量和可维护性。阅读本书可以使开发人员更好地理解和掌握测试驱动开发的技巧和方法,从而在嵌入式开发取得更好的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值