周立功:MVC 框架的应用
Posted ZLG致远电子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了周立功:MVC 框架的应用相关的知识,希望对你有一定的参考价值。
第九章为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 抽象视图类
即可按 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抽象类的定义如下:
即可按 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 个参数,其分别为指向视图的指针和指向模型的指针。
此外,在 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,其定义如下:
即可按 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 视图类
即可按 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 视图类
即可按 p_view_zigbee 的指向引用其它成员。
类似地,定义 zigbee 视图的初始化函数原型如下:
其中,p_view_zigbee 指向 zigbee 视图对象,zm516x_handle 是 zigbee 模块的实例句柄,可通过 ZM516X 模块的实例初始化函数获得。其调用形式如下:
在实现 view_zigbee_init()时,也需要先实现与其对应的显示函数,详见程序清单 9.74。
程序清单 9.74 zigbee 视图显示函数的实现
程序清单 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 模式常用于处理 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 发生变化,则只需要完善“启动应用程序”这一部分代码即可,其往往就是根据实际情况调用各个硬件实例的初始化函数。将资源的“准备”工作(初始化)从原先的应用程序中分离出来,使得应用程序彻底的通用化了,与具体硬件实现了完全的分离,可以灵活的跨平台应用。
书籍的淘宝购买链接如下,可复制到浏览器打开:
【广州致远电子官方企业店】,复制这条信息¥Ebic03xkccD¥后打开手机淘宝
以上是关于周立功:MVC 框架的应用的主要内容,如果未能解决你的问题,请参考以下文章
周立功can卡是如何运用在新能源汽车维修中的 他的控制逻辑是怎样的?
新书创作谈:周立功教授数十年之心血力作《程序设计与数据结构》