这次主要是分享对软件设计中的“低耦合、高内聚”原则的一些个人体会,通过lorawan代码等实例分析,让大家对这个设计思想有一些具象的理解。
本文作者twowinter,转载请注明作者:http://blog.csdn.net/iotisan/
“低耦合、高内聚”,乍听一下特别有逼格,瞬间让我们这次培训高大上了不少。
在一些设计模式的书籍,以及一些面向对象的书籍中,常常会看到这个词。设计模式主要是软件工程领域,特别是面向对象编程这个领域,大家原本都用一些很笨的办法在写代码,后面慢慢有一些人发现一些小技巧,可以让代码更易读、更易维护,再后来一些偏学术型的大牛,把一些常见的设计思路提炼提炼出来,就成了我们现在听到的设计模式。
因此今天要讲的耦合coupling,最早就是来自于面向对象编程。
那它是什么意思呢?
作为一个正经的培训,我们来看看书中对它的定义:
耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息
内聚性:又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。
所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。
对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分。这样有利于修改和组合。
这个例子是《大话设计模式》中介绍的,我觉得还挺有意思。
三国时期,(卧槽,三国时期? 嗯,三国时期),曹操在灭掉北方势力之后,带领百万大军攻打东吴,眼看就要灭掉东吴,统一天下,于是大宴众文武。酒席间,曹操诗兴大发,不觉吟道:“喝酒唱歌,人生真爽。……”。众文武齐呼:“丞相好诗!”于是一臣子速命印刷工匠刻板印刷,以便流传填下。
样张出来给曹操一看,曹操感觉不妥,说道:“喝与唱,此话过俗,应改为‘对酒当歌’较好!”,于是此臣就命工匠重新来过。工匠眼看连夜刻版之工,彻底白费,心中叫苦不喋。只得照办。
样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说:“人生真爽太过直接,应改问语才够意境,因此应改为‘对酒当歌,人生几何?…………’!”当臣转告工匠之时,工匠卒…………
这里的刻板就是一个耦合体,所有的字都耦合在一块板子上。如果我们能单独对某个字进行修改,降低字与字之间的耦合,那就轻松多了。
喝酒唱歌,人生真爽。
对酒当歌,人生几何。
一大段的短歌行,这样只要改4个字。北宋的毕昇就是这样想的,于是活字印刷术诞生了。
虽然说耦合性、内聚性是联系紧密程度的度量,但它是个挺虚的概念。我们只能想办法去尽量的实现“低耦合、高内聚”。
那究竟怎么做呢?
这应该是最容易想到的办法,把复杂的系统“化整为零,各个击破”。功能上分解开了,一个模块实现一个独立的功能,自然就不耦合在一起了。
这在软件设计上,称之为 单一职责原则SRP(Single Responsibility Principle)。
比如我们早先的演示代码,就将各种业务功能与LoRa传输杂糅在一个模块里。
有一个我印象很深的例子,就是LoRaWAN的协议文档。原本协议框架及命令等,是和各个国家的地区参数一起发布的,后来由于LoRaWAN逐步应用过程中肯定会有很多新区域加进来,为了不影响旧有协议文档主体,所以从V1.0.2版本开始,联盟把地区参数这块内容单独出来。这就是一个解耦的例子。
依赖倒转原则DIP(the Dependency Inversion Principle DIP),这个原则是 Martin, Robert C 在1996年提出来的。
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
具体怎么讲呢?
这是传统的设计思路:
In conventional application architecture, lower-level components are designed to be consumed by higher-level components which enable increasingly complex systems to be built. In this composition, higher-level components depend directly upon lower-level components to achieve some task. This dependency upon lower-level components limits the reuse opportunities of the higher-level components.
这是DIP原则的实现方式:
DIP中提出了一个抽象接口。抽象接口是对低层模块的抽象,低层模块按要求来实现这个抽象接口。高层模块不直接依赖低层模块,而是依赖这个抽象接口。
所以,原本是高层依赖低层的情况,通过这个抽象接口操作,反而变成了低层模块要向上依赖这个抽象接口。这就是依赖倒转。
这样做的好处就是,高层模块和低层模块的耦合降低,高层模块的复用性极大的增强了。
事实上,这个思路大家应该都清楚。特别是我们嵌入式界,面对茫茫多的硬件设备,提炼出稳定的硬件抽象层,就显得特别重要。这样很多模块都可以复用。
这个方法在LoRaWAN协议栈中有一些运用。
关于这个Radio的用法,据我所知,物联网OS排行榜的第一名contiki也是这样定义的。
第三种方法,也是大家常会遇到的。
假设A是上层,B是它的下层,A依赖B。假如B也直接依赖A,那就可能造成循环依赖。比如说编译A模块时需要包含到B模块的文件,而编译B时同样要直接包含到A的文件。这种情况下,A和B的耦合就比较严重了。
单向依赖,就是说A模块可以调用B模块暴露的API,但B模块绝不允许调用A模块的API。
比如刚才提到的LoRaMac就有这种情况,当发数据时,是MAC传递给Radio,但接收数据时,是Radio传回给MAC。
这种情况下,就变成MAC和Radio循环依赖,这样子耦合就变得很严重。如果我们要换一个MAC,比如不走LoRaWAN的协议,那Radio中原来MAC的接口也要相应的变化。
我们最常见的办法是设置回调,这个例子中,MAC把接收函数以回调形式通过注册函数注入到Radio中,这样MAC还是依赖Radio的注册函数。
如下,MAC把接收函数注入到Radio中。
void LoRaMacInitialization(void)
{
RadioEvents.TxDone = OnRadioTxDone;
RadioEvents.RxDone = OnRadioRxDone;
RadioEvents.RxError = OnRadioRxError;
RadioEvents.TxTimeout = OnRadioTxTimeout;
RadioEvents.RxTimeout = OnRadioRxTimeout;
Radio.Init( &RadioEvents );
}
Radio 接收数据后,处理回调函数
void SX1276OnDio0Irq( void )
{
if( ( RadioEvents != NULL ) && ( RadioEvents->RxDone != NULL ) )
{
RadioEvents->RxDone( RxTxBuffer, SX1276.Settings.FskPacketHandler.Size, SX1276.Settings.FskPacketHandler.RssiValue, 0 );
}
}
今天这篇分享,主要从“总体结构”->“系统层级”->“模块间”这三个从大到小的层面,分享了代码设计上解耦的一些思路。
希望对大家有所启发。