周立功教授新書《面向AMetal框架與接口的編程(上)》,對AMetal框架進行了詳細介紹,通過閱讀這本書,你可以學到高度復用的軟件設計原則和面向接口編程的開發(fā)思想,聚焦自己的“核心域”,改變自己的編程思維,實現(xiàn)企業(yè)和個人的共同進步。經周立功教授授權,即日起,致遠電子公眾號將對該書內容進行連載,愿共勉之。
第九章為BLE&zigbee 無線模塊,本文內容為9.3 MVC 框架。
>>> 9.3.1 MVC 模式
模型-視圖-控制器(Model-View-Controller,MVC)模式是應用面向對象編程 SoC 原則的典型示例,模式的名稱來自用于切分軟件應用的三個主要部分,即模型部分、視圖部分和控制器部分。它是 Smalltalk 中的用戶界面框架,其目的是將模型從用戶界面解耦。因為 Model相對來說比較穩(wěn)定,而 View 和 Controller 相對來說容易變化,所以通過分層可以隔離變化。
而且視圖與模型的分離帶來的好處允許美工專心設計 UI 部分,程序員專心開發(fā)軟件,互相不會干擾。MVC 包括 3 類組件:
-
Model:模型代表應用信息,負責“內部實現(xiàn)”的具體功能,包含和管理(業(yè)務)邏輯、數(shù)據、狀態(tài)以及應用的規(guī)則,不依賴 UI;
-
View:通常在一個人機接口上呈現(xiàn) Model 信息的抽象視圖,即視圖是模型的外在表現(xiàn)——用戶界面的一部分,視圖只是展示數(shù)據,但不處理數(shù)據。視圖并非一定是圖形化的,文本輸出也是視圖;
-
Controller:將用戶輸入分配到模型與視圖中去,控制器也是用戶界面的一部分,定義用戶界面對用戶輸入的響應方式。
如圖 9.10 所示為 MVC 框架的示意圖,視圖和控制器合起來組成用戶界面,用戶界面包括輸入和輸出兩部分:視圖相當于輸出部分——顯示結果給用戶,控制器相當于輸入部分——響應用戶的操作。這 3 類組件通過交互進行協(xié)作,View創(chuàng)建 Controller 后,Controller 根據用戶交互調用Model 的相應服務。而 Model 會將自身狀態(tài)的改變通知View,View則會讀取Model的信息更新自身。比如,當用戶通過單擊(鍵入或觸摸等)某個按鈕觸發(fā)一個視圖時,視圖將用戶操作告知控制器。控制器處理用戶輸入,并與模型交互。模型執(zhí)行所有必要的校驗和狀態(tài)改變,并通知控制器應該做什么。控制器按照模型給出的指令,指導視圖更新顯示內容輸出。
圖 9.10 MVC 框架示意圖
通常 MVC 被認為是一種框架模式,而不是一種設計模式,因為框架模式與設計模式之間的區(qū)別在于,前者比后者的范疇更廣泛。其主要特征在于它能夠為多個不同的視圖提供數(shù)據,即同一個模型可以支持多個視圖,模型的代碼只需要寫一次就可以被多個視圖重用。假設在兩個視圖中使用同一個模型的數(shù)據,無論何時更改了模型,都需要更新兩個視圖,可以使用觀察者模式解決。
>>> 9.3.2 觀察者模式
觀察者模式定義了一對多的對象之間的依賴關系,當一個對象的狀態(tài)發(fā)生變化時,所有依賴于它的對象都會得到通知并自動更新,因此觀察者模式是一種行為模式,其適用于根據觀察對象狀態(tài)進行相應處理的場景。
在溫度檢測儀中,當溫度傳感器得到的值發(fā)生變化時,希望視圖的內容同步改變。雖然可以在溫度檢測代碼中附加更新顯示的功能,但在本質上更新顯示與溫度檢測是完全不同的兩種處理方法,因此相互之間形成了高度依賴性的關系。
觀察者模式就是一種避免高度依賴性的方法,構成觀察者模式的有兩個對象:發(fā)生變化的對象稱為 觀察對象(Subject),而被通知的對象稱為 觀察者(Observer)。如果觀察對象的狀態(tài)發(fā)生變化,則所有的觀察者都會收到消息,同步更新自己的狀態(tài),因此這種交互方式又被稱為“依賴”或“發(fā)布—訂閱”。雖然觀察對象是消息的發(fā)布者,但它發(fā)布消息時并不需要知道誰是它的觀察者,因此觀察者的數(shù)量是不限的,即觀察對象維護了觀察者對象的結合。現(xiàn)在的問題是,如果觀察者與觀察對象互相引用,它們變得互相依賴,這可能會對一個系統(tǒng)的分層和重用性產生負面影響。基于此,觀察者模式通過定義一個接口通知觀察對象發(fā)生了變化,從而將觀察者與觀察對象解耦,只依賴于觀察者和觀察對象的抽象類,從而保證了訂閱系統(tǒng)的靈活性和可擴展性。
圖 9.11 觀察者模式的實現(xiàn)結構
在如圖 9.11 所示的觀察者模式的結構圖中,觀察者類(Observer)、觀察對象類(Subject)、具體的觀察者類(ConcreteObserver)和具體的觀察對象類(ConcreteSubject)共同完成觀察者模式的各項職責,使用“添加、通知和刪除”的方法實現(xiàn)觀察者模式。
-
Observer(觀察者):Observer 角色負責接收來自 Subject 角色狀態(tài)變化的通知,即Subject 調用每個 Observer 的 update 方法,發(fā)消息通知所有的 Observer,從而將 Subject 和Observer 解耦。因此,當對象間有數(shù)據依賴時,最好用觀察者模式對它們解耦。
由于 Observer 角色是抽象的,雖然它聲明了 update 方法,但不提供任何實現(xiàn),該方法在子類中實現(xiàn)。
-
Subject(觀察對象):Subject 角色表示觀察對象,定義觀察對象必須實現(xiàn)的職責:管理(添加或刪除)觀察者并通知觀察者。從 Subject 指向 Observer 的箭頭線表明 Subject包含了 Observer 類型的實例,箭頭前面的實心圓圈表示多于一個實例。
當觀察對象的狀態(tài)變化時,由于它不知道該將消息發(fā)送給誰,因此 Subject 角色定義了一個可以存儲 N 個觀察者對象“列表”,以及添加(attach)、刪除(detach)和通知(notify)觀察者的抽象方法。
當觀察對象決定通知它的所有觀察者對象時,notify 遍歷觀察者對象“列表”,調用每個 Observer 的 update 方法,發(fā)消息通知所有的 Observer,告訴它們“我的狀態(tài)改變了,請更新顯示內容”。如果在 Subject 角色中注冊了多個 Observer 角色,誰先注冊就先調用誰的update 方法,不能改變調用順序。
-
ConcreteObserver(具體的觀察者):ConcreteObserver 角色表示具體的觀察者,當它的方法被調用后,就會去獲取具體的觀察對象的最新狀態(tài)。從 ConcreteObserver 指向ConcreteSubject 的箭頭線表明 ConcreteObserver 包含了 ConcreteSubject 類型的實例,由于沒有實心圓圈,則表示有且僅有一個實例。
-
ConcreteSubject (具體的觀察對象):ConcreteSubject 角色表示具體的觀察對象,其職責非常明確——誰能觀察,誰不能觀察。它不僅提供函數(shù)的實現(xiàn),而且提供獲取和管理它發(fā)布數(shù)據的方法。當自身的狀態(tài)發(fā)生改變時,它會通知所有已經注冊的 Observer 角色。除了要支持 Subject 角色外,根據業(yè)務的不同,可能還需要提供諸如 getState()、setState()這樣的函數(shù),用于具體的觀察者獲取或設置相關的狀態(tài)。
觀察者模式中的 Subject 和 Observer 接口是為了處理 Subject 的變化而設計的,因此當對象之間有數(shù)據依賴時,最好用觀察者模式對它們進行解耦。觀察者模式的適用范圍如下:
-
當一個抽象模型有兩個方面時,如果其中一個方面依賴于另一個方面,則只要將這兩者封裝在獨立的對象中,即可使它們各自獨立地改變和復用。
-
當改變一個對象需要同時改變其它對象時,卻不知道具體有多少個對象有待改變。
-
當一個對象必須通知其它對象時,而又無法預知其它對象是誰,而你不希望這些對象是緊耦合的。
注意事項:
-
在觀察者模式中,觀察對象通知了觀察者,而這個觀察者同時也是一個觀察對象,它會通知其它的觀察者。因而常常會產生過于復雜的設計,并且使調試變得更加困難。當遇到這種情況時,Mediator(仲裁)模式可能會幫助我們改進這類代碼。
根據經驗建議,最多允許出現(xiàn)一個對象既是觀察者也是觀察對象,即消息最多轉發(fā)一次(兩次),否則邏輯關系就會比較復雜且難以維護。
-
如果觀察者比較多,且處理時間比較長,雖然可以使用異步處理方式,但要考慮線程安全和隊列問題。
-
當觀察者與觀察對象的關系是一對多時,一是使用多線程技術(異步),不管誰啟動線程,都可以明顯地提高系統(tǒng)性能。二是使用緩存技術(同步),但需要足夠多的資源。
-
觀察對象可以自己做主決定是否通知觀察者,以達到減輕負擔的目的。
MVC 框架是一個典型的觀察者模式示例,Model 提供的數(shù)據是 View 的觀察對象,發(fā)布者是 Model,訂閱者是 View。Model 是指操作“不依賴于顯示形式的內部模型”,View 是管理 Model“如何顯示”的,通常一個 Model 對應多個 View。
下面將以 AM824ZB 開發(fā)板為載體展示 MVC 框架,當視圖觀察到模型生成的布爾類型value 值時,既可以通過 LED0 顯示,也可以通過 zigbee 發(fā)送出去,使得可以無線遠程監(jiān)控value 的值。其用例描述如下:
在初始狀態(tài)時,value 為 AM_FALSE, LED 熄滅,zigbee 發(fā)送“0”。當有鍵按下時,則 value 值為 AM_TRUE,LED 點亮,zigbee 發(fā)送“1”;當鍵再次按下時,則 value 值為AM_FALSE,LED0 熄滅,zigbee 發(fā)送“1”......如此周而復始。
其中,value 值對應于 Observer 模式中的模型, LED 和 zigbee 對應于 Observer 模式中的視圖,Observer 模式描述了基本數(shù)據和它可能為數(shù)眾多的用戶界面元素之間的關系。
-
每份數(shù)據都被封裝在一個 Subject 對象中;
-
與 Subject 對應的每個用戶界面元素被封裝在一個 Observer 對象中;
-
一個 Subject 同時可以有多個 Observer;
-
當一個 Subject 改變時,會通知它所有的 Observer;
-
Observer 也會從對應的 Subject 處獲取相應的信息,并及時更新顯示內容。
最終的信息存儲在 Subject 中,當 Subject 中的信息發(fā)生變化時,Observer 會及時更新相應的顯示內容。當用戶保存數(shù)據時,其保存的是 Subject 中的信息,而 Observer 中的信息不需要保存,因為它們顯示的信息來自對應的 Subject。
Observer 模式規(guī)定了單獨的 Subject 類層次和 Observer 類層次,其中的抽象基類定義了通知的協(xié)議,以及用于添加(attach)和刪除(detach)視圖的 Observer 的接口。ConcreteSubject子類實現(xiàn)特定的接口,為了讓具體的 Observer 知道什么東西發(fā)生了變化,它還需要增加相應的接口,同時 ConcreteObserver 子類通過它們的 update 操作指定如何對自己進行更新,從而以獨一無二的方式顯示它們的 Subject。
>>> 9.3.3 領域模型
1. 類模型
創(chuàng)建類模型的第一步就是從問題域尋找相關的對象類,類常常與名詞對應,不要精挑細選,要記下所有可能的每個類。因為我們的目的是捕獲概念,一方面并不是所有的名詞都是概念,另一方面概念也會在語句的其它部分中得到體現(xiàn)。比如,暫定類為 value(bool 值)、key(按鍵)、LED(發(fā)光二極管)和 zigbee。然后通過共性和差異化分析,將它們歸類到更廣泛的范疇內。
雖然 LED 和 zigbee 屬于不同類型的對象,且它們的顯示函數(shù)也不一樣,但它們共同的概念是“視圖”和“顯示函數(shù)”。LED 具有“編號(led_id)”屬性,比如,LED0 的編號“0”用 led_id 表示,而 zigbee 具有“實例句柄(zm516x_handle)”屬性,通過實例句柄即可進行數(shù)據的收發(fā)。因此將共同的概念用抽象類 observer_t 表示,其中的屬性通過具體類 view_led_t和 view_zigbee_t 實現(xiàn)。
雖然 value 是一個 am_bool_t 值,可以將它歸類到業(yè)務邏輯,但同樣要對它建模,創(chuàng)建相應的基類 model_t 和具體類 model_bool_t,而 value 是model_bool_t 的屬性。
下一步是尋找類間的關聯(lián),兩個或多個類之間的結構化關系就是關聯(lián),從一個類到另一個類的引用也是關聯(lián),因此 model_t 與 observer_t 是一對多的關系。接著使用繼承共享公共結構組織類,其相應的類模型詳見圖 9.12。
圖 9.12 類模型
2. 交互模型
顯然有了類,即可創(chuàng)建模型對象model_bool 與視圖對象 view_led0 和view_zigbee。由于視圖需要知道如何調用顯示函數(shù),因此將通過在類中定義方法表示這些職責。當模型對象的狀態(tài)變化時,需要調用視圖對應的顯示函數(shù) view_update 才能更新顯示內容。雖然不同視圖(LED 視圖、zigbee視圖)的顯示函數(shù)的實現(xiàn)不一樣(LED 亮滅、zigbee 發(fā)送“0”或“1”),但其共性是“顯示函數(shù)”,因此可以共用 pfn_update_view 函數(shù)指針調用顯示函數(shù)。
如圖 9.13 所示的類-職責-協(xié)作序列圖展示了 model_bool、view_led0 和 view_zigbee 對象之間的消息流和由消息引起的方法調用。
圖 9.13 類-職責-協(xié)作序列圖
當有鍵按下時,即可調用 model_bool_set()修改模型對象的 value 值。當模型對象 value值改變后,調用 model_notify()遍歷視圖對象鏈表通知所有的視圖,即調用視圖顯示函數(shù)pfn_update_view()。視圖對象在 pfn_update_view()函數(shù)的實現(xiàn)中,調用 model_bool_get()從模型對象中獲取 value 值,以便更新顯示內容。
>>> 9.3.4 子系統(tǒng)體系結構
集成通信圖是所有開發(fā)用于支持用例的通信圖的合成,其形象地描述了對象之間的相互連接以及所傳遞的消息。通常不同用例之間存在執(zhí)行的優(yōu)先順序,通信圖合成的順序應該與用例執(zhí)行的順序一致,MVC 框架的子系統(tǒng)接口圖詳見圖 9.14。
圖 9.14 子系統(tǒng)接口圖
-
按鍵
當 key1 鍵按下時,則布爾模型的值發(fā)生改變。首先通過 model_bool_get()得到當前的布爾值,接著將該布爾值取反,然后調用 model_bool_set()將取反后的布爾值重新設置到布爾模型中。
-
布爾模型
布爾模型負責維護一個布爾值,外界可以通過 model_bool_set()設置布爾值,也可以通過 model_bool_get()獲取布爾值。當布爾值發(fā)生改變時,布爾模型將依次調用各個視圖的顯示更新函數(shù) pfn_update_view()通知各個視圖更新顯示,各視圖根據自身功能決定顯示方式。
-
視圖
圖中包含了兩個視圖:view_led0 和 view_zigbee。
當 LED0 視圖(view_led0)接收到布爾模型發(fā)出的更新顯示通知時,首先通過模型接口 model_bool_get()獲取當前模型的布爾值。若值為 AM_TRUE,則調用 am_led_on()點亮LED0;若值為 AM_FALSE,則調用 am_led_off()熄滅 LED0。
當 zigbee 視圖(view_zigbee)接收到布爾模型發(fā)出的更新顯示通知時,首先通過模型接口 model_bool_get()獲取當前模型的布爾值,然后通過 am_zm516x_send ()函數(shù)將布爾值通過 zigbee 發(fā)送出去。
>>> 9.3.5 軟件體系結構
1. 設計模型類圖
由于觸發(fā)事件的模型對象無法預測訂閱該事件的所有視圖對象,因此要求將視圖添加到模型的列表中保存起來。雖然可以將與模型關聯(lián)的視圖對象存放在數(shù)組中,卻不利于在運行時動態(tài)地添加和刪除視圖,因此選擇單向鏈表。
圖 9.15 單向鏈表示意圖
單向鏈表是由一個 slist_head_t 類型的頭結點和若干個 slist_node_t 類型的普通結點“鏈”起來的,其示意圖詳見圖 9.15,鏈表的數(shù)據結構定義如下:
由于模型需要管理(添加、刪除和遍歷)存儲視圖的鏈表,因此模型需要“持有”整個視圖鏈表的頭結點。基于此,需要將鏈表的 slist_head_t 類型(slist.h)頭結點 head 包含在model_t 抽象模型類中作為數(shù)據成員:
其中,head 為鏈表的頭結點指針,指向存儲視圖的鏈表,其相應的數(shù)據結構示意圖詳見圖 9.16。由于視圖對象是存儲在單向鏈表中的一個結點,因此需要將鏈表的 slist_node_t 類型(slist.h)普通結點 node 包含在 observer_t 抽象視圖類中作為數(shù)據成員:
圖 9.16 模型數(shù)據結構圖
當需要增加視圖、刪除視圖或遍歷視圖時,將會用到與鏈表對應的 4 個接口函數(shù)。下面將逐一介紹,并在定義了 model_t 類型的模型對象 model 和 observer_t 類型的視圖對象observer 的前提下,展示了接口的調用形式。
-
鏈表初始化
鏈表初始化的函數(shù)原型如下:
其調用形式如下:
在初始狀態(tài)時,模型與視圖沒有任何關系,而添加和刪除視圖是調用 model_attach()和model_detach()實現(xiàn)的,這是分別調用插入鏈表結點函數(shù) slist_add_head()和刪除鏈表結點函數(shù) slist_del()實現(xiàn)的。
-
添加視圖
添加視圖的 slist_add_head()函數(shù)原型如下:
其調用形式如下:
-
刪除視圖
刪除視圖的 slist_del()函數(shù)原型如下:
其調用形式如下:
圖 9.17 模型內部狀態(tài)圖
如圖 9.17 所示在模型內部的鏈表中添加和刪除視圖的狀態(tài)圖,圖中的 attach/detach 省略了 model_固定前綴。當調用 model_attach()時,則模型關聯(lián)了一個視圖,即從初始狀態(tài)轉移到關聯(lián)一個視圖狀態(tài)。當再次調用 model_attach()時,則模型對象關聯(lián)了兩個視圖,即從關聯(lián)一個視圖狀態(tài)轉移到兩個視圖狀態(tài),以此類推。
在觀察對象的聲明周期中,如果不需要刪除視圖的功能,則不要實現(xiàn) model_detach()。如果不需要在運行時動態(tài)地添加和刪除視圖,即可在初始化時將視圖存儲在數(shù)組中,那么不再需要 model_attach()和 model_detach()函數(shù)。
-
遍歷視圖
當模型的狀態(tài)發(fā)生變化時,則需要調用 model_notify()遍歷保存在模型中的視圖鏈表,并調用每個視圖的 pfn_update_view(),才能通知所有的視圖更新顯示內容。而 model_notify()又是調用遍歷鏈表函數(shù) slist_foreach()實現(xiàn)的,遍歷視圖的 slist_foreach()函數(shù)原型如下:
其調用形式如下:
除了在序列圖中顯示了對象協(xié)作的動態(tài)視圖外,還需要設計模型類圖表示類定義的靜態(tài)視圖描述類的屬性和方法。由于鏈表屬于基礎設施領域的概念,不是業(yè)務邏輯領域的概念,說明復用級別是基于基礎設施域的,沒有基于核心域,因此存儲視圖的是數(shù)組、鏈表還是其它的容器,都不影響核心域的概念。這就是鏈表不會出現(xiàn)在分析工作流中,只有設計工作流中才考慮的原因。基于此,不僅需要將鏈表的 slist_head_t 類型(slist.h)頭結點 head 包含在model_t 抽象模型類中作為數(shù)據成員,而且需要將鏈表的 slist_node_t 類型(slist.h)普通結點node包含在observer_t抽象視圖類中作為數(shù)據成員。
圖 9.18 設計模型類圖
如圖 9.18所示為 MVC框架的設計模型類圖,圖中的“0..*”說明模型與視圖呈現(xiàn)一對多的關系。顯然基類的方法子類也有,由于 model_attach()、model_detach()和model_notify()是在model_t類的接口中實現(xiàn)的,而在 model_bool_t 子類中沒有實現(xiàn),因此在繪制 UML 圖時則不需要重復表示,但子類還是繼承了父類的方法。而 observer_t 基類的 pfn_update_view()抽象方法是在子類中實現(xiàn)的,因此在繪制 UML 圖時必須顯式地表示。
2. 抽象視圖/ 模型
(1)抽象視圖類
當模型對象的狀態(tài)變化時,所有視圖共用函數(shù)指針 pfn_update_view,調用各自對應的顯示函數(shù) view_update 更新顯示內容。update_view_t 類型定義如下:
在面向對象 C++編程中時,方法是通過一個隱式的 p_this 指針,使其指向函數(shù)要操作的對象,訪問自身的數(shù)據成員。而在面向對象 C 編程時,則需要顯式地聲明 p_this 指針。使其指向函數(shù)將要操作的視圖對象,訪問視圖對象的數(shù)據成員。
p_model 指向視圖觀察的模型,其目的是獲取模型的數(shù)據,使顯示數(shù)據與模型數(shù)據保持一致。由于 pfn_update_view()方法使用了模型類定義的指針變量 p_model,而在該函數(shù)的實現(xiàn)中調用了模型類的方法 model_bool_get(),即一個類使用了另一個類的操作,因此可以說視圖依賴于模型。
依賴是兩個元素之間的一種關系,其中一個元素變化,將會導致另一個元素變化。雖然依賴的同義詞就是耦合和共生,但依賴是不可避免的,重要的是如何務實地應付變化,這就是良性依賴原則。通常在 UML 中將依賴畫成一條有向的虛線,指向被依賴的類。
由此可見,良性依賴可以幫助我們抵御 SOLID 原則與設計模式的誘惑,以免陷入過度設計的陷阱,帶來不必要的復雜性。
由于鏈表的每個結點存儲都是視圖,因此遍歷視圖鏈表需要從頭結點 node 開始。這是一種常見的 is-a 層次結構,其共同的概念用抽象類表示,差異化分析所發(fā)現(xiàn)的變化將通過從抽象類派生而來的具體類實現(xiàn)。observer_t 抽象類的定義如下:
其中,node 為鏈表結點成員,pfn_update_view 指向與視圖對應的顯示函數(shù),比如,view_update,抽象類 observer_t 的類圖詳見圖 9.19。
圖 9.19 抽象視圖類
顯然,有了 observer_t 類,就可以定義相應的實例 view,當將對象 view 的地址傳給形參 p_this 后:
即可按 this 的指向引用其它成員。
雖然 pfn_update_view 看起來是一個“數(shù)據成員”,但從概念視角來看,其定義的是一個在具體視圖類中實現(xiàn)的抽象方法,其目的是將模型和視圖“解耦”。
在面向對象編程時,每個對象(類)都有一個用于對象初始化的“構造函數(shù)”,初始化成員變量等,而 C 語言則需要顯式地調用 view_init()初始化函數(shù)。其函數(shù)原型如下:
其中,p_this 指向視圖對象,pfn_update_view 指向與視圖對應的顯示函數(shù),其調用形式詳見程序清單 9.54。
程序清單 9.54 視圖初始化函數(shù)范例程序
在 main()中調用對象 view 的初始化函數(shù) view_init(),用 OOP 術語來說,這是給對象 view發(fā)送一條消息,通知它進行自我初始化。view_init()的實現(xiàn)詳見程序清單 9.55。
程序清單 9.55 view_init()初始化函數(shù)
在面向對象 C++編程時,雖然每個類都有“構造函數(shù)”,但有時候可能為空,因此不會將構造函數(shù)作為方法展示在類圖中。而面向對象 C 編程——雖然 view_init()看起來像抽象視圖類提供的接口,但其功能類似于“構造函數(shù)”,因此沒有呈現(xiàn)在相應的類圖中。
(2)抽象模型類
由于抽象模型僅需管理與之關聯(lián)的視圖,其本質上是管理了一個視圖鏈表,因此僅包含一個鏈表頭結。此外,還需提供增加、刪除、遍歷視圖的方法,model_t抽象類的定義如下:
其中,head 為鏈表的頭結點,其類圖詳見圖 9.20。顯然有了 model_t 類,即可定義相應的實例,當將 model 的地址傳給形參 p_model 后:
即可按 p_model 的指向引用其它成員。
圖 9.20 抽象模型類
類似地,需要初始化模型中的各個成員,其函數(shù)原型如下:
其中,p_this 指向模型對象,其調用形式如下:
model_init()模型初始化函數(shù)的實現(xiàn)詳見程序清單 9.56。
程序清單 9.56 model_init()模型初始化函數(shù)
初始化后,需要將視圖保存到鏈表中。當模型的狀態(tài)變化時,即可遍歷鏈表找到與視圖對應的顯示函數(shù),通知視圖更新顯示內容。其函數(shù)原型如下:
其中,p_this 指向模型對象,p_observer 指向視圖對象,其調用形式詳見程序清單 9.57。
程序清單 9.57 添加視圖范例程序
為了避免直接訪問數(shù)據,將通過接口函數(shù)和對象交互,model_attach()添加視圖函數(shù)的實現(xiàn)詳見程序清單 9.58。
程序清單 9.58 model_attach()添加視圖函數(shù)
如果觀察者只對某一事件感興趣,則可以擴展觀察對象的注冊接口,讓觀察者注冊為“僅對特定時間感興趣”,以提高更新的效率。
當不再使用某個視圖時,則將其從鏈表中刪除,其函數(shù)原型如下:
其中,p_this 指向模型對象,p_observer 指向視圖對象,其調用形式詳見程序清單 9.59。
程序清單 9.59 刪除視圖范例程序
model_detach()刪除視圖函數(shù)的實現(xiàn)詳見程序清單 9.60。
程序清單 9.60 model_detach()刪除視圖函數(shù)
當模型對象的狀態(tài)變化時,需要遍歷保存在模型中的視圖對象鏈表,并調用每個視圖對象的 pfn_update_view(),才能通知所有的視圖更新顯示內容。其函數(shù)原型如下:
其中,p_this 指向模型對象,其調用形式詳見程序清單 9.61。
程序清單 9.61 遍歷視圖鏈表范例程序
model_notify()通知更新顯示內容函數(shù)的實現(xiàn)詳見程序清單 9.62。
程序清單 9.62 model_notify()通知更新顯示函數(shù)
其中,slist_foreach()為遍歷視圖鏈表函數(shù),__view_process()回調函數(shù)依次處理各個鏈表結點(即視圖),“處理”就是調用視圖中的 pfn_update_view 函數(shù),其對應的函數(shù)原型為:
通常在調用 pfn_update_view 函數(shù)時,需要傳遞 2 個參數(shù),其分別為指向視圖的指針和指向模型的指針。
由于視圖的第一個成員為node,p_node為指向node的指針,其值為視圖首元素的地址,與視圖的地址相等,強制轉換即可得到指向視圖自身的指針:
此外,在 model_notify()函數(shù)調用 slist_foreach()函數(shù)時,將指向模型的指針作為回調函數(shù)的參數(shù),因此__view_process()函數(shù)中的 p_arg 為指向模型的指針。為了類型匹配,強制轉換即可得到指向模型的指針:
此前介紹的示例只是為了展示接口的使用,實際上 model_t 和 observer_t 并沒有提供具體的實現(xiàn),需要在應用中定義具體的視圖類和模型類,比如,針對 LED 顯示可以定義一個LED 視圖類,針對布爾模型可以定義一個布爾模型類。
為了便于查閱,程序清單 9.63 展示了 mvc.h 文件的內容。
程序清單 9.63 mvc.h 文件內容
-
對稱性
其實程序中處處充滿了對稱性,比如,model_attach()方法總會伴隨著 model_detach()方法,一組方法接受同樣的參數(shù),一個對象中所有的成員都具有相同的生命周期。識別出對稱性,將它清晰地表達出來,使代碼更容易閱讀。一旦閱讀者理解了對稱性所涵蓋的某一半,自然也就很快地理解了另一半。
程序中的對稱性指的是概念上的對稱,無論在什么地方,同樣的概念都會以同樣的形式呈現(xiàn)。在準備消滅重復之前,常常需要尋找并表示出代碼中的對稱性。
3. 具體模型/ 視圖
(1)布爾模型
雖然 model_bool_t 實現(xiàn)了 model_t 接口,但抽象模型中并沒有與應用相關的業(yè)務邏輯,所以要在布爾模型中增加相應的數(shù)據,因為視圖的核心就是觀察數(shù)據并實時同步顯示。
圖 9.21 布爾模型類
雖然作為示例布爾模型僅包含一個值為 AM_TRUE 或 AM_FALSE的布爾值,但是各個視圖都可以觀察這個布爾值并實時同步顯示。其職責是管理觀察對象的狀態(tài),實現(xiàn) model_bool_get()獲取布爾值和model_bool_set()修改布爾值的方法,以及在狀態(tài)發(fā)生改變時,調用基類的方法 model_notify()通知所有關聯(lián)的視圖更新顯示內容。布爾模型的類圖詳見圖 9.21,其定義如下:
其中,am_bool_t 是 AMetal 在 am_types.h 中自定義的類型,其值為 AM_TRUE 或AM_FALSE。由于有了布爾模型,即可定義相應的實例,當將 model_bool 的地址傳給形參p_this 后:
即可按 p_this 的指向引用其它成員。
value 的初值將通過參數(shù)傳遞給初始化函數(shù),其函數(shù)原型如下:
其中,p_this 指向模型對象,init_value 為布爾模型初值。其調用形式如下:
通常應該先初始化基類化,接著再初始化自身特有數(shù)據,model_bool_init()的實現(xiàn)詳見程序清單 9.64。
程序清單 9.64 model_bool_init()模型初始化函數(shù)
由于布爾模型維護了一個 am_bool_t 類型 value 值,因此需要提供設置和獲取 value 值的接口。model_bool_set()用于設置布爾模型當前的布爾值,比如,當有鍵按下時,可以使用該接口修改布爾模型的值。在設置布爾模型的值時,可能會使布爾模型的值發(fā)生變化。當value 值改變時,視為布爾模型的狀態(tài)發(fā)生變化,此時需要調用 model_notify()通知所有的視圖。如果布爾值未發(fā)生任何改變,則無需做任何實際動作。其函數(shù)原型如下:
其中,p_this 指向模型對象,value 為設置的當前值,設置布爾模型當前值 value 的范例程序詳見程序清單 9.65。
程序清單 9.65 設置布爾模型當前值 value 的范例程序
model_bool_set()函數(shù)的實現(xiàn)詳見程序清單 9.66。
程序清單 9.66 model_bool_set()函數(shù)
類似地,model_bool_get()用于獲取布爾模型當前的布爾值,比如,當布爾模型的值發(fā)生變化時,模型會通知所有的視圖更新顯示。此時,在視圖顯示函數(shù)中,則需要調用該函數(shù)得到當前最新的布爾值,同步更新顯示。獲取 value 當前值的函數(shù)原型如下:
其中,p_this 指向模型對象,p_value 為獲取當前值的指針,獲取布爾模型當前值的范例程序詳見程序清單 9.67。
程序清單 9.67 獲取布爾模型當前值的范例程序
model_bool_get()函數(shù)的實現(xiàn)詳見程序清單 9.68。
程序清單 9.68 model_bool_get()函數(shù)
為了便于查閱,程序清單 9.69 展示了 model_bool.h 文件的內容。
程序清單 9.69 model_bool.h 文件內容
(2)具體視圖
由于具體視圖類實現(xiàn)了 observer_t 接口,因此具體視圖還必須實現(xiàn)在抽象視圖 observer_t中定義的 update 方法,即要給抽象視圖中的 pfn_update_view 函數(shù)指針賦值,使其指向實際的 update 函數(shù)。
當布爾模型的數(shù)據發(fā)生變化時,視圖顯示函數(shù)需要調用 model_bool_get()獲取最新的布爾值。當獲取布爾值后,即可根據具體視圖的實際功能同步顯示相應的數(shù)據。
對于 LED 視圖來說,則將觀察到的 bool 值通過 LED 顯示出來。即 bool 值為 AM_FALSE時 LED 熄滅,bool 值為 AM_TRUE 時 LED 點亮;對于 zigbee 視圖來說,則將觀察到的 bool值通過 zigbee 發(fā)送出去,即 bool 值為 AM_FALSE 時 zigbee 發(fā)送“0”,bool 值為 AM_TRUE時 zigbee 發(fā)送“1”。
-
LED 視圖
LED 視圖繼承自抽象視圖,同時具有一個私有數(shù)據成員 led_id,用于表示 LED 燈的 ID號,LED 視圖定義如下:
其中的 led_id 為 LED 的下標編號。LED 視圖類 view_led_t 實現(xiàn)了 observer 接口,其對應的類圖詳見圖 9.22。
圖 9.22 LED 視圖類
顯然有了 LED 視圖,即可定義相應的視圖對象,當將view_led的地址傳給形參p_view_led后:
即可按 p_view_led 的指向引用其它成員。
接著初始化視圖對象,顯然只要將 view_led、led_id 值傳遞給 view_led_init()函數(shù),即可初始化 LED 視圖對象,其函數(shù)原型(view_led.h)如下:
其中,p_view_led 指向 LED 視圖對象,led_id 為 LED 的編號。其調用形式如下:
通常需要先初始化抽象視圖(基類),接著再初始化私有數(shù)據成員 led_id 等,初始化抽象視圖的函數(shù)原型(mvc.h)如下:
在實現(xiàn) view_led_init()時,需要先實現(xiàn)與其對應的顯示函數(shù),詳見程序清單 9.70。
程序清單 9.70 LED 視圖顯示函數(shù)的實現(xiàn)
基于此,LED 視圖初始化函數(shù)的實現(xiàn)詳見程序清單 9.71。
程序清單 9.71 LED 視圖初始化函數(shù)的實現(xiàn)
視圖在得到通知后,需要知道究竟是 model_t 類中哪個狀態(tài)發(fā)生了變化,發(fā)生了何種變化。通常在通知接口 pfn_update_view 中不傳送這些信息,而是在視圖得到通知后,再反過來調用 model_bool_get()查詢狀態(tài)的函數(shù)。然后視圖再決定自己應該做什么事情,這時pfn_update_view 的參數(shù)就是 model_t 類的指針。
當布爾模型的狀態(tài)發(fā)生變化時,為了實現(xiàn)自動調用 LED 視圖對應的顯示函數(shù),那么 LED視圖需要預先將自己添加到模型對象的鏈表中保存起來。比如:
為了便于查閱,程序清單 9.72 展示了 view_led.h 文件的內容。
程序清單 9.72 view_led.h 文件內容
至此,實現(xiàn)了一個具體模型(布爾模型)和一個具體視圖(LED 視圖),具有單個視圖的模型完整示例詳見程序清單 9.73。
程序清單 9.73 單個視圖的范例程序(main.c)
-
zigbee 視圖
zigbee 視圖繼承自抽象視圖,同時具有一個私有數(shù)據成員 zm516x_handle,其為 zigbee實例句柄,通過該句柄,即可使用相應的接口函數(shù)操作 zigbee 模塊,zigbee 視圖定義如下:
zigbee 視圖類 view_zigbee_t 實現(xiàn)了 observer_t 接口,其對應的類圖詳見圖 9.23。
圖 9.23 zigbee 視圖類
有了zigbee 視圖,即可定義相應的視圖對象,當將 view_zigbee 的地址傳給 p_view_zigbee 后:
即可按 p_view_zigbee 的指向引用其它成員。
類似地,定義 zigbee 視圖的初始化函數(shù)原型如下:
其中,p_view_zigbee 指向 zigbee 視圖對象,zm516x_handle 是 zigbee 模塊的實例句柄,可通過 ZM516X 模塊的實例初始化函數(shù)獲得。其調用形式如下:
在實現(xiàn) view_zigbee_init()時,也需要先實現(xiàn)與其對應的顯示函數(shù),詳見程序清單 9.74。
程序清單 9.74 zigbee 視圖顯示函數(shù)的實現(xiàn)
其中,am_zm516x_send()函數(shù)的作用是通過 zigbee 發(fā)送字符串至目標地址的節(jié)點(目標地址在初始化時配置),其函數(shù)原型(am_zm516x.h)為:
基于此,zigbee 視圖初始化函數(shù)的實現(xiàn)詳見程序清單 9.75,其除了初始化抽象視圖類外,還完成了 zigbee 模塊的地址配置,配置本地地址為 0x2001,目標地址為 0x2002。
程序清單 9.75 zigbee 視圖初始化函數(shù)的實現(xiàn)
為了便于查閱,程序清單 9.76 展示了 view_zigbee.h 文件的內容。
程序清單 9.76 view_zigbee.h 文件內容
由此可見,當新增加 zigbee 視圖后,雖然與 LED 視圖不一樣,但可以共用同一個模型。
且 LED 視圖和布爾模型都不需要做任何修改,同時也沒有一行重復的視圖代碼,說明“用戶界面與內部實現(xiàn)”真正做到了分離。
3. MVC 應用
在 MVC 模式中,其核心是視圖接收來自模型和控制器的數(shù)據并決定如何顯示,控制器捕捉用戶的輸入事件和系統(tǒng)產生的事件。當控制器檢測到有鍵按下時,它將外部的事件轉換為內部的數(shù)據請求,控制器決定調用模型的那個函數(shù)進行處理,然后確定用那個視圖來顯示模型提供的數(shù)據,詳見程序清單 9.77。
程序清單 9.77 MVC 模式應用范例程序(main.c)
至此,實現(xiàn)了具有 LED 和 zigbee 兩個視圖的 MVC 應用程序,為了驗證 zigbee 視圖,實現(xiàn)遠程“監(jiān)控”,需要使用另外一個 zigbee 來接收 MVC 應用中 zigbee 視圖發(fā)出的數(shù)據“0”或“1”。為便于觀察,使用另外一塊 AM824ZB 開發(fā)板來接收數(shù)據,當接收到“0”時,其LED0 熄滅,當接收到“1”時,其 LED0 點亮,范例程序詳見程序清單 9.78。
程序清單 9.78 新增 AM84ZB 板用以接收 zigbee 數(shù)據的范例程序
在 MVC 應用中,zigbee 視圖將 zigbee 的本地地址設置為 0x2001,目標地址設置為0x2002,。新的 AM824ZB 開發(fā)板為了能夠接收到其發(fā)出的數(shù)據,需要對應的將本地地址設置為 0x2002,目標地址設置為 0x2001。
實際上,MVC 模式常用于處理 GUI 窗口事件,比如,每個窗口部件都是 GUI 相關事件的發(fā)布者,其它對象可以訂閱所關注的事件。比如,當按下 A 按鈕時,會發(fā)布相應的“動作事件”。另一個對象對這個按鈕進行注冊,便于在此按鈕按下時,得到相應的消息,然后完成某一動作。
由此可見,觀察者模式背后的思想等同于關注點分離原則背后的思想,其目的是降低發(fā)布者和訂閱者之間的耦合,便于在運行時動態(tài)地添加和刪除訂閱者。模式在抽象的原則和具體的實踐之間架起了一座橋梁,其主要動機是將變化帶來的影響局部化。
局部化影響的必然結果就是“捆綁邏輯和數(shù)據”,如果有可能盡量將其放在一個方法中,至少要放在一個對象里,最起碼也要放到一個包下面。在發(fā)生變化時,邏輯和數(shù)據很可能會同時被改動。如果將它們放在一起,那么修改它們所造成的的影響停留在局部。
其次,觀察者模式的最大推動力來自于 OCP 開放閉合原則,其動機就是為了在增加新的觀察者對象時,無需更改觀察對象,從而使觀察對象保持封閉,這對于系統(tǒng)的擴展性和靈活性有很大的提高。顯然由繼承實現(xiàn)的 OCP,使設計模式成為應變能力更強的工具。
>>> 9.3.6 MVC 應用程序優(yōu)化
在整個布爾模型的應用中,使用的硬件外設資源有 1 個按鍵、1 個 LED 和 zigbee 模塊,這些資源都有相應的可以跨平臺的通用接口。雖然程序清單 9.77 中絕大部分程序都沒有與硬件綁定,可以跨平臺復用,但是唯一的不足之處在于在應用程序中調用了實例初始化函數(shù)am_zm516x_inst_init(),而實例初始化函數(shù)是與平臺相關的,不同平臺可能不同,因此若實例初始化函數(shù)修改,則應用程序必須進行對應的修改。
顯然,實例初始化函數(shù)是初始化具體實例的,而應用程序并不關心具體實例,其只需要使用具體實例提供的通用服務(如 LED、zigbee、KEY)。基于此,將實例初始化函數(shù)的調用從應用程序中“分離”出去,應用程序全部使用通用接口實現(xiàn)。使用 LED,需要 LED 對應的 ID 號,使用按鍵,需要按鍵對應的編碼,使用 zigbee,需要 zigbee 的操作句柄,這些信息都可以通過參數(shù)傳遞。優(yōu)化后的應用程序范例詳見程序清單 9.79。
程序清單 9.79 應用程序實現(xiàn)(app_mvc_bool_main.c)
顯然,只需要準備好 1 個 LED、1 個按鍵和一個 zigbee 資源(調用它們對應的實例初始化函數(shù)),然后調用 app_mvc_bool_main()函數(shù)即可,為了便于調用 app_mvc_bool_main()函數(shù),將該函數(shù)聲明在 app_mvc_bool_main.h 中,詳見程序清單 9.80。
程序清單 9.80 應用程序入口函數(shù)聲明(app_mvc_bool_main.h)
在主程序中調用 app_mvc_bool_main()函數(shù)即可啟動應用,范例程序詳見程序清單 9.81。
程序清單 9.81 啟動應用程序(main.c)
注意,AM824ZB 板載的獨立按鍵 KEY1 和 LED 均在系統(tǒng)啟動時自動調用了實例初始化函數(shù),因此,無需再次調用。默認情況下,LED0 的 ID 為 0,KEY1 的按鍵編碼為 KEY_F1。
此時,若應用程序需要移動到其它硬件平臺上運行,或相關資源的 ID 發(fā)生變化,則只需要完善“啟動應用程序”這一部分代碼即可,其往往就是根據實際情況調用各個硬件實例的初始化函數(shù)。將資源的“準備”工作(初始化)從原先的應用程序中分離出來,使得應用程序徹底的通用化了,與具體硬件實現(xiàn)了完全的分離,可以靈活的跨平臺應用。
-
ZigBee
+關注
關注
158文章
2270瀏覽量
242738 -
可編程邏輯
+關注
關注
7文章
515瀏覽量
44083 -
無線模塊
+關注
關注
12文章
622瀏覽量
48491
原文標題:周立功:MVC 框架的應用
文章出處:【微信號:ZLG_zhiyuan,微信公眾號:ZLG致遠電子】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論