Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread一般讨论
【show-me-bad-code】改善既有代码的设计,谈谈我的真实案例
1.00
发布于 2021-10-25 15:56:13 浏览:1590
订阅该版
[tocm] # 1. 题外话 在本活动开始之前,非常荣幸地收到RRT小师弟的邀请, 看能否帮忙想想1024程序员节的论坛活动好点子,在此非常感谢对我的信任。 想起早年在浏览一些博文的时候,对国外代码社区举办的"烂代码比赛"印象深刻, 看到那些牛人提交的"眼花缭乱的**bad-but-work-well**的代码,简直是佩服得五体投地。 自然我们大部分的程序员还很难提供出这么**秀**的代码, 所以基于这个原则,结合我们的实际情况, 修改了下比赛规则,重在娱乐参与,旨在代码提升。 更多关于此次show-me-bad-code活动的介绍,请点击[这里](https://club.rt-thread.org/ask/article/3106.html)。 # 2. 我来提供一段代码 ## 2.1 代码背景 这段代码的原型来自于早几年在一家POS行业内头部企业供职时的真实案例代码,基于这套代码也是过了各种POS机的行业认证, 经过10多年的沉淀,基于该模板代码移植/改造而来的应用程序,罐装的POS机累计装机量应该在KW级别,可谓就是传说中妥妥的【**祖传代码**】。 早期代码的运行环境是嵌入式Linux系统,总内存高达128MB,还是非常富余的;后面才慢慢切为RTOS系统,内存也锐减到64MB,但即便这样,给到应用程序的内存也是非常充足的。 ## 2.2 代码功能 这段代码的主要功能就是在POS机触发交易时的界面(带LCD显示)下,能够自动识别出当前要使用的各种交易方式,其中包括: - **是否刷了磁条卡?(刷了磁条卡,走刷磁条卡的交易流程)** - **是否插入了IC卡?(插入IC卡,走银联的IC卡交易流程,行内称**PBOC**流程)** - **是否使用了IC卡做非接触交易(俗称:挥卡)?(挥卡交易,走银联的IC卡快速交易,行内称**qPBOC**流程)** - **是否扫描到了微信/支付宝这类的支付条码?(条码支付与标准银行卡交易是完全不一样的流程)** - **是否按下了【取消键】,用户主动退出了交易?** - **是否在规定的时间内(一般60S)没有任何的交易发生,导致超时?** 除这些功能外,因为POS机交易是需要保存最近N笔交易记录的,所以这个处理交易的流程中,还得维护交易记录的保存。而代码中用于保存交易记录的结构体也是非常非常非常的庞大,大到一个结构体就占用接近2KB,这是非常恐怖的。 ## 2.3 代码片段 为了仅说明情况,而不透露具体的技术细节,这里我仅提供伪代码,注意代码里的注释,是我为了辅助介绍而自己添加的,原来的代码中,注释比这少得可怜。 ```c /** * 保存交易记录的结构体定义 */ typedef struct _transaction_log_t { char time_stamp[32]; uint8_t trans_type; uint8_t trans_no; /* 各式各样的数据定义长达20-30个变量 */ uint8_t data1[64]; ... uint8_t datan[128]; } transaction_log_t; /* 全局的交易log变量,被各个处理流程的文件引用 */ transaction_log g_log_info; /** * 支持的交易方式的掩码 */ #define TRANSACTION_SWIPE_CARD 0x01 #define TRANSACTION_INSERT_CARD 0x02 #define TRANSACTION_TAP_CARD 0x04 #define TRANSACTION_CODE_PAY 0x08 /** * 处理交易的流程总入口 */ int all_transaction_process(int timeouts_ms, int transaction_flag) { int start_time, end_time; end_time = 0; start_time = get_local_time(); /* 循环判断需要支持的交易方式,直到触发了某种交易或者超时 */ while (1) { if (transaction_flag & TRANSACTION_SWIPE_CARD) { /* 判断刷卡:阻塞式的接口,带超时时间 */ ret = get_swipe_card(10); if (ret == ok) { /* 处理刷卡流程 */ ... return 0; } } else if (transaction_flag & TRANSACTION_SWIPE_CARD) { /* 判断插卡:阻塞式接口,带超时时间 */ ret = get_insert_card(10); if (ret == ok) { /* 处理插卡流程 */ ... return 0; } } else if (transaction_flag & TRANSACTION_SWIPE_CARD) { /* 判断挥卡:阻塞式接口,带超时时间 */ ret = get_tap_card(10); if (ret == ok) { /* 处理挥卡流程 */ ... return 0; } } else if (transaction_flag & TRANSACTION_SWIPE_CARD) { /* 判断条码支付 */ ret = get_code(10); if (ret == ok) { /* 处理条码流程 */ ... return 0; } } end_time = get_local_time(); if (start_time + timeous_ms == end_time) { /* timeout */ return -1; } /* 界面显示倒计时 */ lcd_update_timetous(); /* 循环延时 */ delay_ms(10); } } /** * main函数总入口 */ int main(int argc, const char *argv[]) { /* 应用初始化 */ system_init(); /* 处理备份及系统异常断电引发的冲正交易 */ handle_bakup(); /* main loop */ while (1) { /* 每10ms获取下当前有没有按键触发交易 */ key = get_key_ms(10); if (key1 == key) { /* 有触发交易需求 */ ret = all_transaction_process(60*1000, ); } else if (key2 == key) { /* 其他菜单操作 */ ... } else { /* 超时或未知按键输入 */ ... } /* 刷新显示在LCD上的时间 */ update _time_dispaly(); } return 0; } ``` ## 2.4 "烂"的理由 这里说上面的代码"烂"并不是说它工作得不行,相反,它的确工作得可以,从装机量和各种行业认证就可以看得出来,还是扛得住市场对它的考验。这里提出它"烂"的原因,主要考虑是从代码设计和代码维护的角度来看。 - **代码维护上:全局变量全盘引用,乱串于各个文件中,可读性非常差** - **代码设计上:数据结构没有规划设计,导致结构体定义太复杂,太过庞然大物,冗余空间浪费大** - **代码设计上:所有交易流程的判断处理,太过于死板,一个if-else判断到底,耦合严重,可扩展性非常差** - **代码设计上:LCD显示与核心业务流程跳转参杂在一起,没有解耦,往往改了业务功能的同时还要改UI实现** - **代码性能上:流程处理中,大量使用带超时时间的阻塞式函数,导致整个流程响应的时效性不是很理想** # 3. 改善既有代码的设计 ## 3.1 改善代码前的思考 大家都戏称**祖传代码,请勿随意改动**,但我觉得真正优秀的代码,是慢慢被迭代,被重构而来的,否则的代码就会被一步步地堆砌起来,直到有一天,没人能够再修改维护了,那一刻就如同一栋大厦轰然倒塌,这是非常糟糕的。 正是基于这些的考虑,当时我也是很大胆地向主管提出,我们应该从小面积开始做代码重构,逐渐改善一些有缺陷的代码设计。 比较遗憾的是,主管是相对保守的,没有接受后面大面积重构的实施,主要还是担心期间的风险,影响外面的程序升级。从商业的角度上,我是支持他的;但是,从代码的角度,我还是坚持我的观点;当然这并不影响我们的共事。 ## 3.2 我要如何改善这些代码 **先说下,主管答允我的小面积重构部分,我是怎么做的!** 【**代码维护上:全局变量全盘引用,乱串于各个文件中,可读性非常差**】 针对这一点,我重新整理了部分的C代码规划,明确要求非十分必要的设计,不允许全局变量在多个C文件中修改传递,尽量控制在一个C文件,同时关键的数据都封装成get/set接口,减少通篇修改数据的可能性。 在code review环境,着重对此类代码做重点审查,一经发现,务必打回重新修改提交。 【**代码设计上:数据结构没有规划设计,导致结构体定义太复杂,太过庞然大物,冗余空间浪费大**】 针对这一点,我的方法就是重新梳理,我们需要用到的必备数据,把所有非必须的数据全部删除,同时一些数据buffer的长度,再严格评估其真正的内存需求空间,而不是一上来就定义32字节/64字节/128字节。 还有一个方面,就是尽量考虑字节对齐的问题,定义结构体的时候,多考虑考虑。 最后的实践方案,省下了一半的空间。 **再说下,当时我提出的实施方案,但是被按下的重构方案!** 【 **代码设计上:所有交易流程的判断处理,太过于死板,一个if-else判断到底,耦合严重,可扩展性非常差** **代码设计上:LCD显示与核心业务流程跳转参杂在一起,没有解耦,往往改了业务功能的同时还要改UI实现** **代码性能上:流程处理中,大量使用带超时时间的阻塞式函数,导致整个流程响应的时效性不是很理想** 】 这三点,我认为都应该通过改善整体的代码架构来提升,下面简单说说的重构思路。 为了解决此类有业务处理流程,又有UI输入和UI显示的处理场景了,有一个比较成熟的软件模型,叫[MVC模型](https://baike.baidu.com/item/MVC%E6%A1%86%E6%9E%B6?fromtitle=MVC%E6%A8%A1%E5%9E%8B&fromid=23680545)。 MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。 - **Model(模型)** - 模型代表核心业务处理模块。它也可以带有逻辑,在数据变化时更新控制器。 - **View(视图)** - 视图代表模型包含的数据的可视化,即UI部分。 - **Controller(控制器)** - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。 ![img](https://www.runoob.com/wp-content/uploads/2014/08/1200px-ModelViewControllerDiagram2.svg_.png) 根据这个理论模型,应用到我的工程上就是: **Controller**:控制器,对应在这里就是,各式各样的事件监听器,把各种用户可能输入的方式抽象成一个个事件,而每个事件都有对应的监听器,当监听器识别到了对应事件的发生,立马触发事件给到模型层。 **Model**: 模型,这应该整个设计中代码最重的部分,主要是各个处理事件的处理,独立抽象成一个个模型,这些模型层互不干扰,也易于扩展,有新的事件需要处理就重新定义个模型即可。 **View**:视图,就是最简单的UI界面,它负责对Model提供的数据最UI界面的更新,比如刷新时间,比如显示倒计时,比如提示用卡信息等等。 它的整一个示意图如下所示: ![image.png](https://oss-club.rt-thread.org/uploads/20211025/ccf200da1ac8f17affd3159e1cab2c38.png.webp) 应用MVC框架的最大好处就是把M和V分离开来,数据和视图解耦,使得数据易于处理并存储,同时也易于扩展,这里的扩展包括视图扩展和模型扩展,从一变N变得更加容易。 另外很重要的一点,在性能上,处理的实时性大大提升了,而不是简单的阻塞、延时这种粗暴的方法,取之而来的各种异步监听,快速响应,这一点我觉得在涉及到UI的应用场景下,都是应该要着重考虑的部分。 # 4. 更多思考 代码是无止境的,没有最好的代码实现,但是往往有更好的代码实现。 很多奇奇怪怪的BUG,在代码设计和代码编写阶段其实已经埋下了隐患,只不过短时间没有暴露出来而已。 这也就要求我们这些代码工作者,在敲代码之前,多想想设计思路,尽量把你的思路,你的流程通过图表的形式表达出来, 再不济,也应该要形成文档,以便于随时可以复盘你的代码实现是否偏离了你原本的设计。 毫不夸张地说,一个在设计阶段就有缺陷的代码架构,再怎么修饰也将于事无补。 这也是我目前转入软件架构师岗位,得出的最直接的心得。 最后愿这个世界的程序越来越强,BUG越来越少; **不过,这样的话,那我们岂不是要失业了?**
9
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
李肯陪你玩赚嵌入式
2022年度和2023年度RT-Thread社区优秀开源布道师,COC深圳城市开发者社区主理人,专注于嵌入式物联网的架构设计
文章
47
回答
504
被采纳
82
关注TA
发私信
相关文章
1
有关动态模块加载的一篇论文
2
最近的调程序总结
3
晕掉了,这么久都不见layer2的踪影啊
4
继续K9ii的历程
5
[GUI相关] FreeType 2
6
[GUI相关]嵌入式系统中文输入法的设计
7
20081101 RT-Thread开发者聚会总结
8
嵌入式系统基础
9
linux2.4.19在at91rm9200 上的寄存器设置
10
[转]基于嵌入式Linux的通用触摸屏校准程序
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
freemodbus
PWM
flash
cubemx
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
flashDB
GD32
socket
中断
编译报错
Debug
SFUD
rt_mq_消息队列_msg_queue
msh
keil_MDK
ulog
C++_cpp
MicroPython
本月问答贡献
a1012112796
10
个答案
1
次被采纳
踩姑娘的小蘑菇
4
个答案
1
次被采纳
红枫
4
个答案
1
次被采纳
张世争
4
个答案
1
次被采纳
Ryan_CW
4
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
5
次点赞
YZRD
2
篇文章
4
次点赞
qq1078249029
2
篇文章
2
次点赞
xnosky
2
篇文章
1
次点赞
Woshizhapuren
1
篇文章
5
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部