周立功:MVC 框架的应用

周立功教授新书《面向AMetal框架与接口的编程(上)》,对AMetal框架进行了详细介绍,通过阅读这本书,你可以学到高度复用的软件设计原则和面向接口编程的开发思想,聚焦自己的“核心域”,改变自己的编程思维,实现企业和个人的共同进步。经周立功教授授权,即日起,致远电子公众号将对该书内容进行连载,愿共勉之。

第九章为BLE&zigbee 无线模块,本文内容为9.3 MVC 框架。

>>>9.3.1 MVC 模式

模型-视图-控制器(Model-View-Controller,MVC)模式是应用面向对象编程 SoC 原则的典型示例,模式的名称来自用于切分软件应用的三个主要部分,即模型部分、视图部分和控制器部分。它是 Smalltalk 中的用户界面框架,其目的是将模型从用户界面解耦。因为 Model相对来说比较稳定,而 View 和 Controller 相对来说容易变化,所以通过分层可以隔离变化。

而且视图与模型的分离带来的好处允许美工专心设计 UI 部分,程序员专心开发软件,互相不会干扰。MVC 包括 3 类组件:

Model:模型代表应用信息,负责“内部实现”的具体功能,包含和管理(业务)逻辑、数据、状态以及应用的规则,不依赖 UI;

View:通常在一个人机接口上呈现 Model 信息的抽象视图,即视图是模型的外在表现——用户界面的一部分,视图只是展示数据,但不处理数据。视图并非一定是图形化的,文本输出也是视图;

Controller:将用户输入分配到模型与视图中去,控制器也是用户界面的一部分,定义用户界面对用户输入的响应方式。

如图 9.10 所示为 MVC 框架的示意图,视图和控制器合起来组成用户界面,用户界面包括输入和输出两部分:视图相当于输出部分——显示结果给用户,控制器相当于输入部分——响应用户的操作。这 3 类组件通过交互进行协作,View创建 Controller 后,Controller 根据用户交互调用Model 的相应服务。而 Model 会将自身状态的改变通知View,View则会读取Model的信息更新自身。比如,当用户通过单击(键入或触摸等)某个按钮触发一个视图时,视图将用户操作告知控制器。控制器处理用户输入,并与模型交互。模型执行所有必要的校验和状态改变,并通知控制器应该做什么。控制器按照模型给出的指令,指导视图更新显示内容输出。

图 9.10 MVC 框架示意图

通常 MVC 被认为是一种框架模式,而不是一种设计模式,因为框架模式与设计模式之间的区别在于,前者比后者的范畴更广泛。其主要特征在于它能够为多个不同的视图提供数据,即同一个模型可以支持多个视图,模型的代码只需要写一次就可以被多个视图重用。假设在两个视图中使用同一个模型的数据,无论何时更改了模型,都需要更新两个视图,可以使用观察者模式解决。

>>>9.3.2 观察者模式

观察者模式定义了一对多的对象之间的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新,因此观察者模式是一种行为模式,其适用于根据观察对象状态进行相应处理的场景。

在温度检测仪中,当温度传感器得到的值发生变化时,希望视图的内容同步改变。虽然可以在温度检测代码中附加更新显示的功能,但在本质上更新显示与温度检测是完全不同的两种处理方法,因此相互之间形成了高度依赖性的关系。

观察者模式就是一种避免高度依赖性的方法,构成观察者模式的有两个对象:发生变化的对象称为 观察对象(Subject),而被通知的对象称为 观察者(Observer)。如果观察对象的状态发生变化,则所有的观察者都会收到消息,同步更新自己的状态,因此这种交互方式又被称为“依赖”或“发布—订阅”。虽然观察对象是消息的发布者,但它发布消息时并不需要知道谁是它的观察者,因此观察者的数量是不限的,即观察对象维护了观察者对象的结合。现在的问题是,如果观察者与观察对象互相引用,它们变得互相依赖,这可能会对一个系统的分层和重用性产生负面影响。基于此,观察者模式通过定义一个接口通知观察对象发生了变化,从而将观察者与观察对象解耦,只依赖于观察者和观察对象的抽象类,从而保证了订阅系统的灵活性和可扩展性。

图 9.11 观察者模式的实现结构

在如图 9.11 所示的观察者模式的结构图中,观察者类(Observer)、观察对象类(Subject)、具体的观察者类(ConcreteObserver)和具体的观察对象类(ConcreteSubject)共同完成观察者模式的各项职责,使用“添加、通知和删除”的方法实现观察者模式。

Observer(观察者):Observer 角色负责接收来自 Subject 角色状态变化的通知,即Subject 调用每个 Observer 的 update 方法,发消息通知所有的 Observer,从而将 Subject 和Observer 解耦。因此,当对象间有数据依赖时,最好用观察者模式对它们解耦。

由于 Observer 角色是抽象的,虽然它声明了 update 方法,但不提供任何实现,该方法在子类中实现。

Subject(观察对象):Subject 角色表示观察对象,定义观察对象必须实现的职责:管理(添加或删除)观察者并通知观察者。从 Subject 指向 Observer 的箭头线表明 Subject包含了 Observer 类型的实例,箭头前面的实心圆圈表示多于一个实例。

当观察对象的状态变化时,由于它不知道该将消息发送给谁,因此 Subject 角色定义了一个可以存储 N 个观察者对象“列表”,以及添加(attach)、删除(detach)和通知(notify)观察者的抽象方法。

当观察对象决定通知它的所有观察者对象时,notify 遍历观察者对象“列表”,调用每个 Observer 的 update 方法,发消息通知所有的 Observer,告诉它们“我的状态改变了,请更新显示内容”。如果在 Subject 角色中注册了多个 Observer 角色,谁先注册就先调用谁的update 方法,不能改变调用顺序。

ConcreteObserver(具体的观察者):ConcreteObserver 角色表示具体的观察者,当它的方法被调用后,就会去获取具体的观察对象的最新状态。从 ConcreteObserver 指向ConcreteSubject 的箭头线表明 ConcreteObserver 包含了 ConcreteSubject 类型的实例,由于没有实心圆圈,则表示有且仅有一个实例。

ConcreteSubject (具体的观察对象):ConcreteSubject 角色表示具体的观察对象,其职责非常明确——谁能观察,谁不能观察。它不仅提供函数的实现,而且提供获取和管理它发布数据的方法。当自身的状态发生改变时,它会通知所有已经注册的 Observer 角色。除了要支持 Subject 角色外,根据业务的不同,可能还需要提供诸如 getState()、setState()这样的函数,用于具体的观察者获取或设置相关的状态。

观察者模式中的 Subject 和 Observer 接口是为了处理 Subject 的变化而设计的,因此当对象之间有数据依赖时,最好用观察者模式对它们进行解耦。观察者模式的适用范围如下:

当一个抽象模型有两个方面时,如果其中一个方面依赖于另一个方面,则只要将这两者封装在独立的对象中,即可使它们各自独立地改变和复用。

当改变一个对象需要同时改变其它对象时,却不知道具体有多少个对象有待改变。

当一个对象必须通知其它对象时,而又无法预知其它对象是谁,而你不希望这些对象是紧耦合的。

注意事项:

在观察者模式中,观察对象通知了观察者,而这个观察者同时也是一个观察对象,它会通知其它的观察者。因而常常会产生过于复杂的设计,并且使调试变得更加困难。当遇到这种情况时,Mediator(仲裁)模式可能会帮助我们改进这类代码。

根据经验建议,最多允许出现一个对象既是观察者也是观察对象,即消息最多转发一次(两次),否则逻辑关系就会比较复杂且难以维护。

如果观察者比较多,且处理时间比较长,虽然可以使用异步处理方式,但要考虑线程安全和队列问题。

当观察者与观察对象的关系是一对多时,一是使用多线程技术(异步),不管谁启动线程,都可以明显地提高系统性能。二是使用缓存技术(同步),但需要足够多的资源。

观察对象可以自己做主决定是否通知观察者,以达到减轻负担的目的。

MVC 框架是一个典型的观察者模式示例,Model 提供的数据是 View 的观察对象,发布者是 Model,订阅者是 View。Model 是指操作“不依赖于显示形式的内部模型”,View 是管理 Model“如何显示”的,通常一个 Model 对应多个 View。

下面将以 AM824ZB 开发板为载体展示 MVC 框架,当视图观察到模型生成的布尔类型value 值时,既可以通过 LED0 显示,也可以通过 zigbee 发送出去,使得可以无线远程监控value 的值。其用例描述如下:

在初始状态时,value 为 AM_FALSE, LED 熄灭,zigbee 发送“0”。当有键按下时,则 value 值为 AM_TRUE,LED 点亮,zigbee 发送“1”;当键再次按下时,则 value 值为AM_FALSE,LED0 熄灭,zigbee 发送“1”......如此周而复始。

其中,value 值对应于 Observer 模式中的模型, LED 和 zigbee 对应于 Observer 模式中的视图,Observer 模式描述了基本数据和它可能为数众多的用户界面元素之间的关系。

每份数据都被封装在一个 Subject 对象中;

与 Subject 对应的每个用户界面元素被封装在一个 Observer 对象中;

一个 Subject 同时可以有多个 Observer;

当一个 Subject 改变时,会通知它所有的 Observer;

Observer 也会从对应的 Subject 处获取相应的信息,并及时更新显示内容。

最终的信息存储在 Subject 中,当 Subject 中的信息发生变化时,Observer 会及时更新相应的显示内容。当用户保存数据时,其保存的是 Subject 中的信息,而 Observer 中的信息不需要保存,因为它们显示的信息来自对应的 Subject。

Observer 模式规定了单独的 Subject 类层次和 Observer 类层次,其中的抽象基类定义了通知的协议,以及用于添加(attach)和删除(detach)视图的 Observer 的接口。ConcreteSubject子类实现特定的接口,为了让具体的 Observer 知道什么东西发生了变化,它还需要增加相应的接口,同时 ConcreteObserver 子类通过它们的 update 操作指定如何对自己进行更新,从而以独一无二的方式显示它们的 Subject。

>>>9.3.3 领域模型

1. 类模型

创建类模型的第一步就是从问题域寻找相关的对象类,类常常与名词对应,不要精挑细选,要记下所有可能的每个类。因为我们的目的是捕获概念,一方面并不是所有的名词都是概念,另一方面概念也会在语句的其它部分中得到体现。比如,暂定类为 value(bool 值)、key(按键)、LED(发光二极管)和 zigbee。然后通过共性和差异化分析,将它们归类到更广泛的范畴内。

虽然 LED 和 zigbee 属于不同类型的对象,且它们的显示函数也不一样,但它们共同的概念是“视图”和“显示函数”。LED 具有“编号(led_id)”属性,比如,LED0 的编号“0”用 led_id 表示,而 zigbee 具有“实例句柄(zm516x_handle)”属性,通过实例句柄即可进行数据的收发。因此将共同的概念用抽象类 observer_t 表示,其中的属性通过具体类 view_led_t和 view_zigbee_t 实现。

虽然 value 是一个 am_bool_t 值,可以将它归类到业务逻辑,但同样要对它建模,创建相应的基类 model_t 和具体类 model_bool_t,而 value 是model_bool_t 的属性。

下一步是寻找类间的关联,两个或多个类之间的结构化关系就是关联,从一个类到另一个类的引用也是关联,因此 model_t 与 observer_t 是一对多的关系。接着使用继承共享公共结构组织类,其相应的类模型详见图 9.12。

图 9.12 类模型

2. 交互模型

显然有了类,即可创建模型对象model_bool 与视图对象 view_led0 和view_zigbee。由于视图需要知道如何调用显示函数,因此将通过在类中定义方法表示这些职责。当模型对象的状态变化时,需要调用视图对应的显示函数 view_update 才能更新显示内容。虽然不同视图(LED 视图、zigbee视图)的显示函数的实现不一样(LED 亮灭、zigbee 发送“0”或“1”),但其共性是“显示函数”,因此可以共用 pfn_update_view 函数指针调用显示函数。

如图 9.13 所示的类-职责-协作序列图展示了 model_bool、view_led0 和 view_zigbee 对象之间的消息流和由消息引起的方法调用。

图 9.13 类-职责-协作序列图

当有键按下时,即可调用 model_bool_set()修改模型对象的 value 值。当模型对象 value值改变后,调用 model_notify()遍历视图对象链表通知所有的视图,即调用视图显示函数pfn_update_view()。视图对象在 pfn_update_view()函数的实现中,调用 model_bool_get()从模型对象中获取 value 值,以便更新显示内容。

>>>9.3.4 子系统体系结构

集成通信图是所有开发用于支持用例的通信图的合成,其形象地描述了对象之间的相互连接以及所传递的消息。通常不同用例之间存在执行的优先顺序,通信图合成的顺序应该与用例执行的顺序一致,MVC 框架的子系统接口图详见图 9.14。

图 9.14 子系统接口图

按键

当 key1 键按下时,则布尔模型的值发生改变。首先通过 model_bool_get()得到当前的布尔值,接着将该布尔值取反,然后调用 model_bool_set()将取反后的布尔值重新设置到布尔模型中。

布尔模型

布尔模型负责维护一个布尔值,外界可以通过 model_bool_set()设置布尔值,也可以通过 model_bool_get()获取布尔值。当布尔值发生改变时,布尔模型将依次调用各个视图的显示更新函数 pfn_update_view()通知各个视图更新显示,各视图根据自身功能决定显示方式。

视图

图中包含了两个视图:view_led0 和 view_zigbee。

当 LED0 视图(view_led0)接收到布尔模型发出的更新显示通知时,首先通过模型接口 model_bool_get()获取当前模型的布尔值。若值为 AM_TRUE,则调用 am_led_on()点亮LED0;若值为 AM_FALSE,则调用 am_led_off()熄灭 LED0。

当 zigbee 视图(view_zigbee)接收到布尔模型发出的更新显示通知时,首先通过模型接口 model_bool_get()获取当前模型的布尔值,然后通过 am_zm516x_send ()函数将布尔值通过 zigbee 发送出去。

>>>9.3.5 软件体系结构

1. 设计模型类图

由于触发事件的模型对象无法预测订阅该事件的所有视图对象,因此要求将视图添加到模型的列表中保存起来。虽然可以将与模型关联的视图对象存放在数组中,却不利于在运行时动态地添加和删除视图,因此选择单向链表。

图 9.15 单向链表示意图

单向链表是由一个 slist_head_t 类型的头结点和若干个 slist_node_t 类型的普通结点“链”起来的,其示意图详见图 9.15,链表的数据结构定义如下:

由于模型需要管理(添加、删除和遍历)存储视图的链表,因此模型需要“持有”整个视图链表的头结点。基于此,需要将链表的 slist_head_t 类型(slist.h)头结点 head 包含在model_t 抽象模型类中作为数据成员:

其中,head 为链表的头结点指针,指向存储视图的链表,其相应的数据结构示意图详见图 9.16。由于视图对象是存储在单向链表中的一个结点,因此需要将链表的 slist_node_t 类型(slist.h)普通结点 node 包含在 observer_t 抽象视图类中作为数据成员:

图 9.16 模型数据结构图

当需要增加视图、删除视图或遍历视图时,将会用到与链表对应的 4 个接口函数。下面将逐一介绍,并在定义了 model_t 类型的模型对象 model 和 observer_t 类型的视图对象observer 的前提下,展示了接口的调用形式。

链表初始化

链表初始化的函数原型如下:

其调用形式如下:

在初始状态时,模型与视图没有任何关系,而添加和删除视图是调用 model_attach()和model_detach()实现的,这是分别调用插入链表结点函数 slist_add_head()和删除链表结点函数 slist_del()实现的。

添加视图

添加视图的 slist_add_head()函数原型如下:

其调用形式如下:

删除视图

删除视图的 slist_del()函数原型如下:

其调用形式如下:

图 9.17 模型内部状态图

如图 9.17 所示在模型内部的链表中添加和删除视图的状态图,图中的 attach/detach 省略了 model_固定前缀。当调用 model_attach()时,则模型关联了一个视图,即从初始状态转移到关联一个视图状态。当再次调用 model_attach()时,则模型对象关联了两个视图,即从关联一个视图状态转移到两个视图状态,以此类推。

在观察对象的声明周期中,如果不需要删除视图的功能,则不要实现 model_detach()。如果不需要在运行时动态地添加和删除视图,即可在初始化时将视图存储在数组中,那么不再需要 model_attach()和 model_detach()函数。

遍历视图

当模型的状态发生变化时,则需要调用 model_notify()遍历保存在模型中的视图链表,并调用每个视图的 pfn_update_view(),才能通知所有的视图更新显示内容。而 model_notify()又是调用遍历链表函数 slist_foreach()实现的,遍历视图的 slist_foreach()函数原型如下:

其调用形式如下:

除了在序列图中显示了对象协作的动态视图外,还需要设计模型类图表示类定义的静态视图描述类的属性和方法。由于链表属于基础设施领域的概念,不是业务逻辑领域的概念,说明复用级别是基于基础设施域的,没有基于核心域,因此存储视图的是数组、链表还是其它的容器,都不影响核心域的概念。这就是链表不会出现在分析工作流中,只有设计工作流中才考虑的原因。基于此,不仅需要将链表的 slist_head_t 类型(slist.h)头结点 head 包含在model_t 抽象模型类中作为数据成员,而且需要将链表的 slist_node_t 类型(slist.h)普通结点node包含在observer_t抽象视图类中作为数据成员。

图 9.18 设计模型类图

如图 9.18所示为 MVC框架的设计模型类图,图中的“0..*”说明模型与视图呈现一对多的关系。显然基类的方法子类也有,由于 model_attach()、model_detach()和model_notify()是在model_t类的接口中实现的,而在 model_bool_t 子类中没有实现,因此在绘制 UML 图时则不需要重复表示,但子类还是继承了父类的方法。而 observer_t 基类的 pfn_update_view()抽象方法是在子类中实现的,因此在绘制 UML 图时必须显式地表示。

2. 抽象视图/ 模型

(1)抽象视图类

当模型对象的状态变化时,所有视图共用函数指针 pfn_update_view,调用各自对应的显示函数 view_update 更新显示内容。update_view_t 类型定义如下:

在面向对象 C++编程中时,方法是通过一个隐式的 p_this 指针,使其指向函数要操作的对象,访问自身的数据成员。而在面向对象 C 编程时,则需要显式地声明 p_this 指针。使其指向函数将要操作的视图对象,访问视图对象的数据成员。

p_model 指向视图观察的模型,其目的是获取模型的数据,使显示数据与模型数据保持一致。由于 pfn_update_view()方法使用了模型类定义的指针变量 p_model,而在该函数的实现中调用了模型类的方法 model_bool_get(),即一个类使用了另一个类的操作,因此可以说视图依赖于模型。

依赖是两个元素之间的一种关系,其中一个元素变化,将会导致另一个元素变化。虽然依赖的同义词就是耦合和共生,但依赖是不可避免的,重要的是如何务实地应付变化,这就是良性依赖原则。通常在 UML 中将依赖画成一条有向的虚线,指向被依赖的类。

由此可见,良性依赖可以帮助我们抵御 SOLID 原则与设计模式的诱惑,以免陷入过度设计的陷阱,带来不必要的复杂性。

由于链表的每个结点存储都是视图,因此遍历视图链表需要从头结点 node 开始。这是一种常见的 is-a 层次结构,其共同的概念用抽象类表示,差异化分析所发现的变化将通过从抽象类派生而来的具体类实现。observer_t 抽象类的定义如下:

其中,node 为链表结点成员,pfn_update_view 指向与视图对应的显示函数,比如,view_update,抽象类 observer_t 的类图详见图 9.19。

图 9.19 抽象视图类

显然,有了 observer_t 类,就可以定义相应的实例 view,当将对象 view 的地址传给形参 p_this 后:

即可按 this 的指向引用其它成员。

虽然 pfn_update_view 看起来是一个“数据成员”,但从概念视角来看,其定义的是一个在具体视图类中实现的抽象方法,其目的是将模型和视图“解耦”。

在面向对象编程时,每个对象(类)都有一个用于对象初始化的“构造函数”,初始化成员变量等,而 C 语言则需要显式地调用 view_init()初始化函数。其函数原型如下:

其中,p_this 指向视图对象,pfn_update_view 指向与视图对应的显示函数,其调用形式详见程序清单 9.54。

程序清单 9.54 视图初始化函数范例程序

在 main()中调用对象 view 的初始化函数 view_init(),用 OOP 术语来说,这是给对象 view发送一条消息,通知它进行自我初始化。view_init()的实现详见程序清单 9.55。

程序清单 9.55 view_init()初始化函数

在面向对象 C++编程时,虽然每个类都有“构造函数”,但有时候可能为空,因此不会将构造函数作为方法展示在类图中。而面向对象 C 编程——虽然 view_init()看起来像抽象视图类提供的接口,但其功能类似于“构造函数”,因此没有呈现在相应的类图中。

(2)抽象模型类

由于抽象模型仅需管理与之关联的视图,其本质上是管理了一个视图链表,因此仅包含一个链表头结。此外,还需提供增加、删除、遍历视图的方法,model_t抽象类的定义如下:

其中,head 为链表的头结点,其类图详见图 9.20。显然有了 model_t 类,即可定义相应的实例,当将 model 的地址传给形参 p_model 后:

即可按 p_model 的指向引用其它成员。

图 9.20 抽象模型类

类似地,需要初始化模型中的各个成员,其函数原型如下:

其中,p_this 指向模型对象,其调用形式如下:

model_init()模型初始化函数的实现详见程序清单 9.56。

程序清单 9.56 model_init()模型初始化函数

初始化后,需要将视图保存到链表中。当模型的状态变化时,即可遍历链表找到与视图对应的显示函数,通知视图更新显示内容。其函数原型如下:

其中,p_this 指向模型对象,p_observer 指向视图对象,其调用形式详见程序清单 9.57。

程序清单 9.57 添加视图范例程序

为了避免直接访问数据,将通过接口函数和对象交互,model_attach()添加视图函数的实现详见程序清单 9.58。

程序清单 9.58 model_attach()添加视图函数

如果观察者只对某一事件感兴趣,则可以扩展观察对象的注册接口,让观察者注册为“仅对特定时间感兴趣”,以提高更新的效率。

当不再使用某个视图时,则将其从链表中删除,其函数原型如下:

其中,p_this 指向模型对象,p_observer 指向视图对象,其调用形式详见程序清单 9.59。

程序清单 9.59 删除视图范例程序

model_detach()删除视图函数的实现详见程序清单 9.60。

程序清单 9.60 model_detach()删除视图函数

当模型对象的状态变化时,需要遍历保存在模型中的视图对象链表,并调用每个视图对象的 pfn_update_view(),才能通知所有的视图更新显示内容。其函数原型如下:

其中,p_this 指向模型对象,其调用形式详见程序清单 9.61。

程序清单 9.61 遍历视图链表范例程序

model_notify()通知更新显示内容函数的实现详见程序清单 9.62。

程序清单 9.62 model_notify()通知更新显示函数

其中,slist_foreach()为遍历视图链表函数,__view_process()回调函数依次处理各个链表结点(即视图),“处理”就是调用视图中的 pfn_update_view 函数,其对应的函数原型为:

通常在调用 pfn_update_view 函数时,需要传递 2 个参数,其分别为指向视图的指针和指向模型的指针。

由于视图的第一个成员为node,p_node为指向node的指针,其值为视图首元素的地址,与视图的地址相等,强制转换即可得到指向视图自身的指针:

此外,在 model_notify()函数调用 slist_foreach()函数时,将指向模型的指针作为回调函数的参数,因此__view_process()函数中的 p_arg 为指向模型的指针。为了类型匹配,强制转换即可得到指向模型的指针:

此前介绍的示例只是为了展示接口的使用,实际上 model_t 和 observer_t 并没有提供具体的实现,需要在应用中定义具体的视图类和模型类,比如,针对 LED 显示可以定义一个LED 视图类,针对布尔模型可以定义一个布尔模型类。

为了便于查阅,程序清单 9.63 展示了 mvc.h 文件的内容。

程序清单 9.63 mvc.h 文件内容

对称性

其实程序中处处充满了对称性,比如,model_attach()方法总会伴随着 model_detach()方法,一组方法接受同样的参数,一个对象中所有的成员都具有相同的生命周期。识别出对称性,将它清晰地表达出来,使代码更容易阅读。一旦阅读者理解了对称性所涵盖的某一半,自然也就很快地理解了另一半。

程序中的对称性指的是概念上的对称,无论在什么地方,同样的概念都会以同样的形式呈现。在准备消灭重复之前,常常需要寻找并表示出代码中的对称性。

3. 具体模型/ 视图

(1)布尔模型

虽然 model_bool_t 实现了 model_t 接口,但抽象模型中并没有与应用相关的业务逻辑,所以要在布尔模型中增加相应的数据,因为视图的核心就是观察数据并实时同步显示。

图 9.21 布尔模型类

虽然作为示例布尔模型仅包含一个值为 AM_TRUE 或 AM_FALSE的布尔值,但是各个视图都可以观察这个布尔值并实时同步显示。其职责是管理观察对象的状态,实现 model_bool_get()获取布尔值和model_bool_set()修改布尔值的方法,以及在状态发生改变时,调用基类的方法 model_notify()通知所有关联的视图更新显示内容。布尔模型的类图详见图 9.21,其定义如下:

其中,am_bool_t 是 AMetal 在 am_types.h 中自定义的类型,其值为 AM_TRUE 或AM_FALSE。由于有了布尔模型,即可定义相应的实例,当将 model_bool 的地址传给形参p_this 后:

即可按 p_this 的指向引用其它成员。

value 的初值将通过参数传递给初始化函数,其函数原型如下:

其中,p_this 指向模型对象,init_value 为布尔模型初值。其调用形式如下:

通常应该先初始化基类化,接着再初始化自身特有数据,model_bool_init()的实现详见程序清单 9.64。

程序清单 9.64 model_bool_init()模型初始化函数

由于布尔模型维护了一个 am_bool_t 类型 value 值,因此需要提供设置和获取 value 值的接口。model_bool_set()用于设置布尔模型当前的布尔值,比如,当有键按下时,可以使用该接口修改布尔模型的值。在设置布尔模型的值时,可能会使布尔模型的值发生变化。当value 值改变时,视为布尔模型的状态发生变化,此时需要调用 model_notify()通知所有的视图。如果布尔值未发生任何改变,则无需做任何实际动作。其函数原型如下:

其中,p_this 指向模型对象,value 为设置的当前值,设置布尔模型当前值 value 的范例程序详见程序清单 9.65。

程序清单 9.65 设置布尔模型当前值 value 的范例程序

model_bool_set()函数的实现详见程序清单 9.66。

程序清单 9.66 model_bool_set()函数

类似地,model_bool_get()用于获取布尔模型当前的布尔值,比如,当布尔模型的值发生变化时,模型会通知所有的视图更新显示。此时,在视图显示函数中,则需要调用该函数得到当前最新的布尔值,同步更新显示。获取 value 当前值的函数原型如下:

其中,p_this 指向模型对象,p_value 为获取当前值的指针,获取布尔模型当前值的范例程序详见程序清单 9.67。

程序清单 9.67 获取布尔模型当前值的范例程序

model_bool_get()函数的实现详见程序清单 9.68。

程序清单 9.68 model_bool_get()函数

为了便于查阅,程序清单 9.69 展示了 model_bool.h 文件的内容。

程序清单 9.69 model_bool.h 文件内容

(2)具体视图

由于具体视图类实现了 observer_t 接口,因此具体视图还必须实现在抽象视图 observer_t中定义的 update 方法,即要给抽象视图中的 pfn_update_view 函数指针赋值,使其指向实际的 update 函数。

当布尔模型的数据发生变化时,视图显示函数需要调用 model_bool_get()获取最新的布尔值。当获取布尔值后,即可根据具体视图的实际功能同步显示相应的数据。

对于 LED 视图来说,则将观察到的 bool 值通过 LED 显示出来。即 bool 值为 AM_FALSE时 LED 熄灭,bool 值为 AM_TRUE 时 LED 点亮;对于 zigbee 视图来说,则将观察到的 bool值通过 zigbee 发送出去,即 bool 值为 AM_FALSE 时 zigbee 发送“0”,bool 值为 AM_TRUE时 zigbee 发送“1”。

LED 视图

LED 视图继承自抽象视图,同时具有一个私有数据成员 led_id,用于表示 LED 灯的 ID号,LED 视图定义如下:

其中的 led_id 为 LED 的下标编号。LED 视图类 view_led_t 实现了 observer 接口,其对应的类图详见图 9.22。

图 9.22 LED 视图类

显然有了 LED 视图,即可定义相应的视图对象,当将view_led的地址传给形参p_view_led后:

即可按 p_view_led 的指向引用其它成员。

接着初始化视图对象,显然只要将 view_led、led_id 值传递给 view_led_init()函数,即可初始化 LED 视图对象,其函数原型(view_led.h)如下:

其中,p_view_led 指向 LED 视图对象,led_id 为 LED 的编号。其调用形式如下:

通常需要先初始化抽象视图(基类),接着再初始化私有数据成员 led_id 等,初始化抽象视图的函数原型(mvc.h)如下:

在实现 view_led_init()时,需要先实现与其对应的显示函数,详见程序清单 9.70。

程序清单 9.70 LED 视图显示函数的实现

基于此,LED 视图初始化函数的实现详见程序清单 9.71。

程序清单 9.71 LED 视图初始化函数的实现

视图在得到通知后,需要知道究竟是 model_t 类中哪个状态发生了变化,发生了何种变化。通常在通知接口 pfn_update_view 中不传送这些信息,而是在视图得到通知后,再反过来调用 model_bool_get()查询状态的函数。然后视图再决定自己应该做什么事情,这时pfn_update_view 的参数就是 model_t 类的指针。

当布尔模型的状态发生变化时,为了实现自动调用 LED 视图对应的显示函数,那么 LED视图需要预先将自己添加到模型对象的链表中保存起来。比如:

为了便于查阅,程序清单 9.72 展示了 view_led.h 文件的内容。

程序清单 9.72 view_led.h 文件内容

至此,实现了一个具体模型(布尔模型)和一个具体视图(LED 视图),具有单个视图的模型完整示例详见程序清单 9.73。

程序清单 9.73 单个视图的范例程序(main.c)

zigbee 视图

zigbee 视图继承自抽象视图,同时具有一个私有数据成员 zm516x_handle,其为 zigbee实例句柄,通过该句柄,即可使用相应的接口函数操作 zigbee 模块,zigbee 视图定义如下:

zigbee 视图类 view_zigbee_t 实现了 observer_t 接口,其对应的类图详见图 9.23。

图 9.23 zigbee 视图类

有了zigbee 视图,即可定义相应的视图对象,当将 view_zigbee 的地址传给 p_view_zigbee 后:

即可按 p_view_zigbee 的指向引用其它成员。

类似地,定义 zigbee 视图的初始化函数原型如下:

其中,p_view_zigbee 指向 zigbee 视图对象,zm516x_handle 是 zigbee 模块的实例句柄,可通过 ZM516X 模块的实例初始化函数获得。其调用形式如下:

在实现 view_zigbee_init()时,也需要先实现与其对应的显示函数,详见程序清单 9.74。

程序清单 9.74 zigbee 视图显示函数的实现

其中,am_zm516x_send()函数的作用是通过 zigbee 发送字符串至目标地址的节点(目标地址在初始化时配置),其函数原型(am_zm516x.h)为:

基于此,zigbee 视图初始化函数的实现详见程序清单 9.75,其除了初始化抽象视图类外,还完成了 zigbee 模块的地址配置,配置本地地址为 0x2001,目标地址为 0x2002。

程序清单 9.75 zigbee 视图初始化函数的实现

为了便于查阅,程序清单 9.76 展示了 view_zigbee.h 文件的内容。

程序清单 9.76 view_zigbee.h 文件内容

由此可见,当新增加 zigbee 视图后,虽然与 LED 视图不一样,但可以共用同一个模型。

且 LED 视图和布尔模型都不需要做任何修改,同时也没有一行重复的视图代码,说明“用户界面与内部实现”真正做到了分离。

3. MVC 应用

在 MVC 模式中,其核心是视图接收来自模型和控制器的数据并决定如何显示,控制器捕捉用户的输入事件和系统产生的事件。当控制器检测到有键按下时,它将外部的事件转换为内部的数据请求,控制器决定调用模型的那个函数进行处理,然后确定用那个视图来显示模型提供的数据,详见程序清单 9.77。

程序清单 9.77 MVC 模式应用范例程序(main.c)

至此,实现了具有 LED 和 zigbee 两个视图的 MVC 应用程序,为了验证 zigbee 视图,实现远程“监控”,需要使用另外一个 zigbee 来接收 MVC 应用中 zigbee 视图发出的数据“0”或“1”。为便于观察,使用另外一块 AM824ZB 开发板来接收数据,当接收到“0”时,其LED0 熄灭,当接收到“1”时,其 LED0 点亮,范例程序详见程序清单 9.78。

程序清单 9.78 新增 AM84ZB 板用以接收 zigbee 数据的范例程序

在 MVC 应用中,zigbee 视图将 zigbee 的本地地址设置为 0x2001,目标地址设置为0x2002,。新的 AM824ZB 开发板为了能够接收到其发出的数据,需要对应的将本地地址设置为 0x2002,目标地址设置为 0x2001。

实际上,MVC 模式常用于处理 GUI 窗口事件,比如,每个窗口部件都是 GUI 相关事件的发布者,其它对象可以订阅所关注的事件。比如,当按下 A 按钮时,会发布相应的“动作事件”。另一个对象对这个按钮进行注册,便于在此按钮按下时,得到相应的消息,然后完成某一动作。

由此可见,观察者模式背后的思想等同于关注点分离原则背后的思想,其目的是降低发布者和订阅者之间的耦合,便于在运行时动态地添加和删除订阅者。模式在抽象的原则和具体的实践之间架起了一座桥梁,其主要动机是将变化带来的影响局部化。

局部化影响的必然结果就是“捆绑逻辑和数据”,如果有可能尽量将其放在一个方法中,至少要放在一个对象里,最起码也要放到一个包下面。在发生变化时,逻辑和数据很可能会同时被改动。如果将它们放在一起,那么修改它们所造成的的影响停留在局部。

其次,观察者模式的最大推动力来自于 OCP 开放闭合原则,其动机就是为了在增加新的观察者对象时,无需更改观察对象,从而使观察对象保持封闭,这对于系统的扩展性和灵活性有很大的提高。显然由继承实现的 OCP,使设计模式成为应变能力更强的工具。

>>>9.3.6 MVC 应用程序优化

在整个布尔模型的应用中,使用的硬件外设资源有 1 个按键、1 个 LED 和 zigbee 模块,这些资源都有相应的可以跨平台的通用接口。虽然程序清单 9.77 中绝大部分程序都没有与硬件绑定,可以跨平台复用,但是唯一的不足之处在于在应用程序中调用了实例初始化函数am_zm516x_inst_init(),而实例初始化函数是与平台相关的,不同平台可能不同,因此若实例初始化函数修改,则应用程序必须进行对应的修改。

显然,实例初始化函数是初始化具体实例的,而应用程序并不关心具体实例,其只需要使用具体实例提供的通用服务(如 LED、zigbee、KEY)。基于此,将实例初始化函数的调用从应用程序中“分离”出去,应用程序全部使用通用接口实现。使用 LED,需要 LED 对应的 ID 号,使用按键,需要按键对应的编码,使用 zigbee,需要 zigbee 的操作句柄,这些信息都可以通过参数传递。优化后的应用程序范例详见程序清单 9.79。

程序清单 9.79 应用程序实现(app_mvc_bool_main.c)

显然,只需要准备好 1 个 LED、1 个按键和一个 zigbee 资源(调用它们对应的实例初始化函数),然后调用 app_mvc_bool_main()函数即可,为了便于调用 app_mvc_bool_main()函数,将该函数声明在 app_mvc_bool_main.h 中,详见程序清单 9.80。

程序清单 9.80 应用程序入口函数声明(app_mvc_bool_main.h)

在主程序中调用 app_mvc_bool_main()函数即可启动应用,范例程序详见程序清单 9.81。

程序清单 9.81 启动应用程序(main.c)

注意,AM824ZB 板载的独立按键 KEY1 和 LED 均在系统启动时自动调用了实例初始化函数,因此,无需再次调用。默认情况下,LED0 的 ID 为 0,KEY1 的按键编码为 KEY_F1。

此时,若应用程序需要移动到其它硬件平台上运行,或相关资源的 ID 发生变化,则只需要完善“启动应用程序”这一部分代码即可,其往往就是根据实际情况调用各个硬件实例的初始化函数。将资源的“准备”工作(初始化)从原先的应用程序中分离出来,使得应用程序彻底的通用化了,与具体硬件实现了完全的分离,可以灵活的跨平台应用。

在公众号后台回复关键字【编程】,即可在线阅读《面向AMetal框架与接口的编程(上)》和《程序设计与数据结构》两本书。

书籍的淘宝购买链接如下,可复制到浏览器打开:

【广州致远电子官方企业店】,复制这条信息¥Ebic03xkccD¥后打开手机淘宝

公众号介绍

致远电子官方微信公众号,一个汇聚500名工程师的研发测试分享平台,为您提供电子行业领先的产品技术与解决方案。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180126B0AAUK00?refer=cp_1026

扫码关注云+社区