草庐IT

随想007:模块化代码

研究是为了理解 2023-10-22 原文

你一定不止一次的听说过模块化代码

理想的模块化代码高内聚低耦合、逻辑清晰、经过严格测试,易于复用

嵌入式 C 编程界到处流传着它的大名。在学校、在公司、在各种技术书籍中,你总能找到它的身影。

它被描述的像是无所不能,仿佛只要使用了模块化代码,你就可以节省大把开发时间,项目就能化腐朽为神奇。

对公司而言,这极具诱惑力。

节省开发时间就可以快速推出新产品、及时实现新需求,这可以带来真金白银。

以至于有些人了解模块化代码的优点后,惊喜的发现找到了一个可以无视人员素质又节省开发时间的终极方法:强制所有开发人员使用模块化代码

现实真的就是这样吗?

在现实世界中,好像哪里有点不对劲:

  • 模块化推了几年总是不成功;

  • 开发人员防备心很重(多次掉坑里),宁愿自己重复造轮子,也不愿用别人的模块

  • 强制开发人员使用模块代码,阻力很大。人们怨气冲天,影响士气

这是怎么回事?

道理说起来简单,但只要实际操作起来,一线开发人员往往会直摇头:手中已有的所谓“模块”质量参差不齐、模块的开发者鱼龙混杂,很多模块别说出了问题找开发方负责维护,就是原作者是谁恐怕都找不到了。在这种情况下,谈论“强制使用模块代码”,简直就是天方夜谭,颇有几分“何不食肉糜”的傲慢

说白了,很多已有的“模块化代码”不好用,以至于开发人员对此失去了信任

我还记得这些“模块化代码”是怎么出现的:

  1. 某天大领导认识到模块化代码的好处,做出构建模块化代码库的指示,要求后续开发要使用库中的代码
  2. 模块化库中的代码从何而来?中层领导献策,可以将现有项目中的代码提取成模块化代码,形成模块化库。现有代码都是经过了现场考验的,肯定好用。
  3. 任务分配到各个项目组,开发人员全速开动,各种“模块化代码”迅速产出,然后收集到模块化库中
  4. 大领导视察模块化库成果,看着短短时间模块化库中多出的丰富文件夹,想着此后开发必将无往而不利,心情十分愉悦。
  5. 这些匆忙产出的代码质量如何?由谁管控?我将向谁反馈 BUG ?向谁提出新需求?没人在意这些,总之库已建成,一切必将好起来。

相信这个过程不会只发生在我的身边。

所以这些披着“模块化”外衣的代码被强制使用,一线人员一次又一次被这些代码伤害过之后,人与人、人与公司之间,再难有信任。
信任 这东西,看不到摸不着,重要却很脆弱。信任可以简化协作,这会在各个方面节省时间。但是信任需要长时间才能建立,只要一瞬间就可以失去

以至于后来发展到,提及模块化时,有些开发人员竟隐隐生出反感

这是一个不好的趋势,因为模块化代码确实有用,它是被工程实践证实的一种优秀的编程策略

假如我们有一些这样的模块化代码:

  • 我们信任这些模块代码,因此可以只关注使用而不需要理解模块的实现,这可以节省时间
  • 这些模块代码都不需要修改,因此可以拿来即用,这可以节省时间
  • 这些模块代码实现简洁明了,而且经过了严格测试和现场考验,因此错误少、维护少,这可以节省时间。

为了和烂大街的模块化代码区分开,我们称这些代码为合格的模块化代码,毫无疑问,使用合格的模块化代码,真的可以无往而不利,真的可以节省开发时间!使用的人越多,复用的越多,节省的时间也就越多!

比如写一套合格的 Modbus 协议栈模块需要 2 个月,然后其他人直接使用这个 Modbus 协议栈模块,可能只需要半天就可以实现 Modbus 通讯。所以我们不要再抱怨手中的模块化代码有多垃圾,我们应该将它们改进成合格的模块化代码,我们要产出合格的模块化代码

那么什么样的代码才是合格的模块化代码呢?

让我们来看一下合格的模块化代码具有的特性。

模块化代码一般由 .c.h 文件组成。

假如为菜单(menu)设计一个模块代码,模块一般包含 menu.cmenu.hmenu_cfg.h 这 3 个文件。

其中,menu.c 文件是模块的具体实现,menu.h 定义了模块的接口, menu_cfg.h 定义了默认配置和选项。

很多人不明白为什么要单独多出来一个 menu_cfg.h 文件,这个文件蕴含的思想很重要,对于需要更改配置或选项的模块代码,这个文件是必不可少的,后续将会提到。

合格的模块代码应该是只读的。如果不是只读的,使用者要把它修改成只读的

这是合格的模块化代码非常重要的特性!

很多人对此不解或充满疑问,但我们经常使用这种只读的模块代码,比如 C 标准库。

我们信任 C 标准库,我们不会修改 C 标志库,甚至我们都没有 C 标准库的源代码,我们无需理解它是如何实现的,我们只关注如何使用。

除了 C 标准库,Keil MDK 有一个 Manage Run-Time Environment 组件,如果你使用过其中的模块代码,就能发现这个组件提供的模块代码都是只读的(当然用户配置文件除外):

将模块代码设置为只读属性,显然是深思熟虑后的结果。这明确的告诉使用者:不需要也不能修改模块代码

不能修改代码???

要是我需要定制参数怎么办?要是你的代码有错误怎么办?

先暂且压一压心中的疑问,我们这样做是有充分理由的。

  1. 用户不需要修改模块代码

    用户使用模块化代码的目的是为了复用,为了节省开发时间。用户渴望的是拿来即用、用了还不出错的模块代码,所以用户并不想修改模块代码,甚至如果一个模块需要用户去修改才能用,那么可能会吓跑这个用户。

  2. 用户不能修改模块代码

    模块代码不是只给某个用户一个人用的。如果每个人都自由的修改模块代码,代码很快就会烂掉。

    模块也会不断升级,对用户而言,升级就是替换掉模块文件。但如果用户更改了模块内容,一段时间后又升级了模块,那么之前的更改会随着文件的替换而消失掉,依赖更改的代码会出现故障。特别是修改的人已经不在这个项目组,而接手的人又不能了解所有情况时。不要给人挖坑。

那么,那些因为不能修改模块代码而产生的疑问该怎么解决呢?

  • 模块通常都是可配置的,我需要定制参数怎么办?

菜单 模块为例。进入菜单后,如果长时间无任何操作,可自动退出菜单。那多长时间合适呢,不同的项目有不同的时间,这不要修改模块代码吗?

不需要。

对于定义良好的模块化代码,所有可配置项,他们的默认值定义在 module_cfg.h 文件中module_cfg.h 文件是模块的一部分。用户需要修改的配置项,定义在 app_module_cfg.h 文件中,同时 module_cfg.h 文件中定义的对应默认值失效。 app_module_cfg.h 文件是用户自定义的头文件。

具体到 菜单 模块为例,menu_cfg.h 文件中会给出默认的 超时退出时间,代码如下所示,这里超时退出时间是 30 秒。

#ifndef __menu_config_h__
#define __menu_config_h__

#include "app_menu_cfg.h"						//包含用户自定义文件

/*菜单无操作延时自动退出时间*/
#if !defined MENU_DELAY_AUTO_EXIT_TIME
#define MENU_DELAY_AUTO_EXIT_TIME       30      //单位:秒
#endif

#endif

只要使用 菜单 模块,用户就必须提供 app_menu_cfg.h 文件。如果要更改 超时退出时间,只需要在这个文件中重新定义宏 MENU_DELAY_AUTO_EXIT_TIME,同时,menu_cfg.h 文件中的相关宏失效。代码如下所示,这里将超时时间修改为 45 秒。

#ifndef __app_menu_cfg_h_
#define __app_menu_cfg_h_

#define MENU_DELAY_AUTO_EXIT_TIME       45

#endif

这并不是什么新鲜事,操作系统 FreeRTOS 中的 FreeRTOSConfig.h 文件,网络协议栈 lwIP 中的 lwipopts.h 文件,都是类似的用户参数配置文件。

  • 那模块中缺少我需要的功能怎么办?(新的需求)

  • 那模块中有错误怎么办?(修改错误)

这种情况下,我可以在模块中增加代码或者修改代码吗?

不可以。

正确的做法是将 BUG 或需求提交给模块维护者。

只有模块的维护者可以修改模块代码。不要图快捷而修改模块的任何代码!

模块维护者需要保证对模块化代码而言,除代码质量外,另一些生死攸关的特性:

  • 一致性
  • 简洁性
  • 向下兼容性

模块维护者更了解全局情况,他可能会因为需求不合理而拒绝你的请求,也可能会因为追求更通用性而扩展你的需求。他要考虑的东西更多,不会局限在某一个项目应用上。他要保证代码修改后运行不出问题,绝对不能出现用户更新代码后,程序编译就失败了,运行就不正确了

因此,从这方面来看,在公司层面推行模块化,这是一个系统工程。这里没有万能药水,也没有不劳而获,在享受模块化便利之前,必须要投入相应的资源:

  • 要有相应的制度:规定哪些部门负责发布维护模块、规定模块的组成、规定合格模块的标准……
  • 要有模块代码化发布和需求收集平台:方便的获取模块、方便代码更新、方便提需求、方便反馈 BUG……
  • 要有质量管控的部门:代码审查、第三方测试……

靠某个领导的意愿推行不了模块化,靠拼凑而来的代码推行不了模块化,它需要公司全方位的投入。

此外,它还需要人,能写出合格模块化代码的人。

只读,不仅是合格模块化代码的重要特性,这还是一个可以量化的特性

可量化 意味着可测量:是否达成指标可唯一确定。用户将模块代码设置为只读后,如果不能正常使用,那么这个模块代码就不具备只读特性,这是可以唯一确定的。

而合格模块化代码的其它特性,比如代码质量,则不具有可量化特性。满足什么样的指标才是好代码呢?没有精确的数据可以描述。我们简要说明一下这些特性:

  • 代码简单直接,明显没有错误

  • 高内聚,封装可以封装的一切,隐藏可以隐藏的一切,绝不暴露不该暴露的信息

  • 低耦合,模块自成一体,尽一切可能减少依赖关系

  • 谨慎的规划好接口,有一组严密、定义良好的 API

  • 严格遵循一致性、向下兼容性

  • 严格测试,先试用再推广,需要经历现场的考验

这些特性都是合格模块化代码的重要特性,然而它们也是所有麻烦的起点,这些特性不可量化,需要依赖特定的人。只有哪些长久苦思、有数年或数十年从业经验、经过无数尝试和错误经历的人才会真正明白、真正写出符合这些特性的代码。

编写模块化代码有技术门槛,这就是麻烦的原因。

编写出稳定可靠的生产代码已经很不容易,但编写合格的模块化代码要求更高,需要程序员站在更高的层次。

  • 第一层次:代码正确
  • 第二层次:处理异常,意识到现实世界并不是理想的,是有干扰的
  • 第三层次:代码清晰、可读性高,明白代码是给人看的、越来越重视自己使用的工具
  • 第四层次:理解抽象,开始抽象出数据结构来消除重复
  • 第五层次:绝不隐藏错误,开始理解防御性编程可能会隐藏错误,开始自动发现错误、主动避免错误
  • 第六层次:下意识思考如何降低复杂度。简单才能清晰,清晰就藏不住 BUG,就更容易让后来者理解。开始分解、重构,让代码越来越小越来越简单,同时也越来越容易扩展。把复杂的事情变简单,简单的事情便可靠。
  • 第七层次:抓住问题的本质,从整体抽象,隐藏细节、降低依赖、提供清晰简洁的接口,使得用户无需了解内部原理也能轻松使用,这样的代码可以作为模块化了。

如果一个好苗子没有辜负大学四年,当他走出新手村走向社会时,可以认为他的技术水平达到了20级。这个好苗子进入公司,经过一年的实践锻炼,他升到了50级,可以在老员工指导下更改需求了;又过了三五年,好苗子成了技术骨干,他升到了100级,可以独自干项目了;又过了几年,好苗子成了小组长,他升到了200级,可以做系统架构了。那多少级可以写出合格的模块化代码?可能至少需要500级!

这意味着并非所有人都能写出合格的模块化代码

这个结论多少有点让人沮丧,也多少会让那些一心降低人为因素影响的领导恼怒。但“并非所有人都能写出合格的模块化代码”并不代表“没有人能写出合格的模块化代码”,既然知道了模块化代码的好处,那就要努力编写出合格的模块化代码

如果一个人有了努力编写合格模块化代码的决心,也有进益求精的态度,那么他需要什么知识来支撑自己呢?

这需要细心、谨慎、并不断的学习,多看、多想、多练。

一个不好的消息是,这个过程没有捷径,时间、经验、态度缺一样都不行。

好消息是网上有很多优秀的开源代码可以参考,可以从开源项目 EmbedSummary 中找到这些代码。

在编写模块化代码的过程中,必须时刻提醒自己,模块代码是很多同事工作的基础,代码必须精益求精






读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)



有关随想007:模块化代码的更多相关文章

  1. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  2. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  3. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  4. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  5. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  6. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

    我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

  7. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  8. ruby - 获取模块中定义的所有常量的值 - 2

    我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c

  9. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  10. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

随机推荐