引言
在我們的嵌入式 C 開發中經常會面對這樣的一類需求:因為對接的設備支持的協議不同,自身的設備需要兼容這些協議,因此需要業務支持不同的協議解析方式。
比如有的協議用 tlv 的數據格式,有的用 xml 的格式,有些又用 json 這樣的格式,面對如此多差異化的需求,我們該如何解決呢?
一種常見的做法是將協議解析和邏輯實現做到一起,每種協議對應一套代碼,這種實現方式簡單,沒有什么設計可言,就是擼代碼。
但是每當對接一種新的協議就要從頭開發,這種重復造輪子的方式非常初級,代碼的可擴展性也非常差,有沒有一種更高級一點的方法呢?
答案是有的,也就是我們本文要講的嵌入式多態。
嵌入式多態原理
我們首先審視一下上面的需求,上述需求的特點是協議是有多種的數據承載格式(tlv/json/xml),需要針對不同的協議做解析,但是解析完成后的數據處理,也就是業務邏輯的部分卻可以保持不變。
因此我們完全可以使用一種模型將這兩者分離開來,將易變的協議解析部分單獨作為一個模塊,將不易變的業務邏輯作為一個模塊。
協議解析模塊主要負責生成數據,而業務邏輯模塊就主要對數據進行處理,中間鏈接這兩者的模型,我們暫且稱之為數據分發模型,負責將協議解析出的數據傳給業務邏輯部分。
這樣一來協議解析模塊就可以在自己內部做任何變化,可以對不同格式的協議進行解析,而這對于業務邏輯來說都是不感知的,代碼的擴展性更佳。
要想達到這種良好的可擴展性,我們引入了一個負責數據分發的模型,這個模型要怎么實現呢?
我們看到協議解析其實是有多種實現方式,而業務邏輯可以看作只有一種實現方式,這就是一種多對一的方式。
在數據分發模型中,負責將多種協議解析方式收斂成一種,我們想一下C語言中哪種方式可以實現這種多對一的收斂呢?沒錯,就是函數指針。
這是指針的一種非常高級的應用,它可以將函數的框架抽象出來,用指針的方式進行定義,我們知道指針可以初始化指向不同的地址,那函數指針就可以被指向多個不同的函數。
當我們調用函數指針時,就可以動態地切換到不同的函數實現上。想一下,雖然協議解析的方式不同,但是最終業務邏輯需要的數據是固定的,因此完全可以將函數的參數搞成一樣的結構。
在面向對象的編程語言中,這種實現方式稱之為多態,同一個接口(抽象級,沒有具體實現)可以支持不同的具體實現。在我們嵌入式C開發中需要自己寫這種多態的接口,但是也不難,筆者將一個模板寫在了下一節中,讀者可以參照這種寫法,靈活修改。
嵌入式多態偽代碼實現模板
定義數據分發的模型,抽象接口
1)偽代碼
?
/*?回調的函數鉤子?*/ typedef?int?(*hook_func)(void*,?int?,?void*); typedef?struct? { ?/*?初始化接口,定義諸如內存之類的問題?*/ ?void?(*init)(void); ?/*?注冊服務對應的處理函數,如協議解析后的業務邏輯處理映射?*/ ?int?(*register)(int?type,?hook_func?func); ?/*?將數據分發到對應的服務?*/ ?int?(*distribute)(void?*input,?int?len,?void?*ret); }demo_param_s;
?
2)代碼解析
在這個結構體中,我們定義了三個函數指針,分別是:
初始化接口,用來初始化內存之類,這個內存可以用來存儲我們提供的服務類型和回調函數;
注冊服務處理函數,主要用來建立服務類型和回調函數的映射,在我們的案例中主要將協議解析后,需要使用哪種業務邏輯處理,建立這樣的一個模型;
數據分發服務,主要將協議解析后的數據作為參數,在服務映射表中找到對應的回調函數,然后進行回調。
如下圖所示,可以非常詳細的描述這個過程。
數據分發模型的初始化接口
1)偽代碼
?
demo_param_s?*g_demo_param?=?NULL; void?demo_init(const?demo_param_s?*param) { ?g_demo_param?=?param; ?return; }
?
2)代碼解析
上面的這部分代碼非常簡單,首先是定義了一個全局變量,這個全局編碼是數據分發模型的類型,用來指向一個數據分發模型的具體實現,初始時,可以讓這個全局變量指向空。
接下來,就是定義了一個初始化接口,這個接口是來初始化我們定義的這個全局變量的,你會看到這個全局變量指向了一個輸入進來的參數,而這個參數就是數據分發模型中各個函數指針的具體實現函數,你將在下一節中看到。
每個函數指針的具體實現
1)偽代碼
?
void?init_impl(void); int?register_impl(int?type,?hook_func?func); int?distribute_impl(void?*input,?int?len,?void?*ret); demo_param_s?demo_param?=?{.init?=?init_impl, ??????????????.register?=?register_impl, ??????????????.distribute?=?distribute_impl}; demo_init(&demo_param);
?
2)代碼解析
這部分的偽代碼主要是對3個函數指針實現,并進行初始化。
首先是三個具體的實現函數被聲明和定義,第一個初始化函數,就是 malloc 出內存;第二個函數,建立服務和業務處理函數的映射關系;第三個函數,在需要某種服務時,通過查詢第二個函數建立的映射表,回調具體的業務;
其次就是定義了一個數據分發模型的局部變量,然后初始化其中的每個函數指針;
最后調用我們在前面定義的數據分發初始化接口,將上面定義的局部變量的值傳入,使其可以全局訪問,在后面我們就可以直接用全局變量 g_demo_param 加上其中的參數的方式調用每個接口。
通過抽象接口調用注冊和分發功能
1)偽代碼
?
//?分發 for(i?=?0;?i?//?注冊 int?type?=?0; int?logic_func(void?*input,?int?len,?void?*ret); ret=?g_demo_param.register(type,?logic_func);?
2)代碼解析
上面就是我們的注冊和分發的接口,其中注冊的部分是在業務邏輯中實現的,分發的部分代碼是在協議解析的部分實現的。
需要特別注意這個實現規則,因為只有這樣才能達到業務邏輯和協議解析的分離,協議解析只依賴我們的數據分發模型,業務邏輯也只依賴我們的數據分發模型,這兩者之間互不依賴,可以獨立的編譯或者打包。
首先我們看注冊,有類型和具體函數實現,我們可以使用 g_demo_param.register 去創建兩者的映射關系,將其保存在內存中。
然后當我們解析完協議,就來到我們的分發部分,當我們的類型在內存映射表中有存儲時,就可以使用g_demo_param.distribute 實現分發,調用我們的業務邏輯代碼。
小結
隨著嵌入式軟件變得越來越復雜,架構問題也變得越來越突出,市場上我們最多見的是對互聯網這類架構的介紹,鮮有專門針對嵌入式軟件的架構分析。本文通過一個案例淺談了嵌入式多態的實現方式,目的是幫助讀者在設計開發相關的場景時能夠拿來借鑒,使自己的代碼具有更好地擴展性。
審核編輯:湯梓紅
評論
查看更多