低耦合高内聚 原则的应用

来源:互联网 发布:美工培训班学费多少钱 编辑:程序博客网 时间:2024/05/18 01:50


这次主要是分享对软件设计中的“低耦合、高内聚”原则的一些个人体会,通过lorawan代码等实例分析,让大家对这个设计思想有一些具象的理解。


本文作者twowinter,转载请注明作者。


前言


“低耦合、高内聚”,乍听一下特别有逼格,瞬间让我们这次培训高大上了不少。


在一些设计模式的书籍,以及一些面向对象的书籍中,常常会看到这个词。设计模式主要是软件工程领域,特别是面向对象编程这个领域,大家原本都用一些很笨的办法在写代码,后面慢慢有一些人发现一些小技巧,可以让代码更易读、更易维护,再后来一些偏学术型的大牛,把一些常见的设计思路提炼提炼出来,就成了我们现在听到的设计模式。


因此今天要讲的耦合coupling,最早就是来自于面向对象编程。


那它是什么意思呢?


说下概念


作为一个正经的培训,我们来看看书中对它的定义:


耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息


内聚性:又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。


所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。


对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。


举个通俗的例子


这个例子是《大话设计模式》中介绍的,我觉得还挺有意思。


三国时期,(卧槽,三国时期? 嗯,三国时期),曹操在灭掉北方势力之后,带领百万大军攻打东吴,眼看就要灭掉东吴,统一天下,于是大宴众文武。酒席间,曹操诗兴大发,不觉吟道:“喝酒唱歌,人生真爽。......”。众文武齐呼:“丞相好诗!”于是一臣子速命印刷工匠刻板印刷,以便流传填下。


样张出来给曹操一看,曹操感觉不妥,说道:“喝与唱,此话过俗,应改为‘对酒当歌’较好!”,于是此臣就命工匠重新来过。工匠眼看连夜刻版之工,彻底白费,心中叫苦不喋。只得照办。


样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说:“人生真爽太过直接,应改问语才够意境,因此应改为‘对酒当歌,人生几何?…………’!”当臣转告工匠之时,工匠卒…………



这里的刻板就是一个耦合体,所有的字都耦合在一块板子上。如果我们能单独对某个字进行修改,降低字与字之间的耦合,那就轻松多了。


喝酒唱歌,人生真爽。

对酒当歌,人生几何。


一大段的短歌行,这样只要改4个字。北宋的毕昇就是这样想的,于是活字印刷术诞生了。


怎么做


虽然说耦合性、内聚性是联系紧密程度的度量,但它是个挺虚的概念。我们只能想办法去尽量的实现“低耦合、高内聚”。


那究竟怎么做呢?


方法一 从总体结构上分解系统


这应该是最容易想到的办法,把复杂的系统“化整为零,各个击破”。功能上分解开了,一个模块实现一个独立的功能,自然就不耦合在一起了。


这在软件设计上,称之为 单一职责原则SRP(Single Responsibility Principle)。


比如我们早先的演示代码,就将各种业务功能与LoRa传输杂糅在一个模块里。


有一个我印象很深的例子,就是之前做的OTA升级工具。


原本它都没啥问题,但后来我们的信道配置指令不是调整了吗,因此这个工具就变得不可用了。

这个问题就是一个很典型的功能耦合。OTA升级工具,它的功能就是把程序文件传送到远端去,不应该和信道配置等功能耦合在一起。

所以,这次我让平台组帮忙做了调整,让它变得更简单,也避免以后不必要的升级维护。



另外还有一个例子,那就是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.


具体怎么讲呢?


这是传统的设计思路:


这是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 );

}

}


总结



今天这篇分享,主要从“总体结构”->“系统层级”->“模块间”这三个从大到小的层面,分享了代码设计上解耦的一些思路。


希望对大家有所启发。





原创粉丝点击