重构的代码已经正式release一个版本了,从结果上来看,有超过自己预期的部分,也有不尽人意的地方。
一、 背景
在代码重构-1中写到的了面向对象的思想,以及如何更好的解耦。里面提到了面向对象的思维以及通过C语言来实现私有变量,这些东西在这次重构中都在大规模使用;基于uds(特指14229)的特性,还使用了较多C语言实现的模板函数,后面会提到。
1. 为什么要重构
一开始我的任务是优化现有的uds协议栈,然后我花了几天的时间看了一下代码,我发现的代码结构是这样的:
我看到后是崩溃的,doip和cantp的一个结构体从最底下直接传到上面,这其实严重的违背了“高内聚、低耦合”的思想了,在可预想的未来,如果后面需要在doip、cantp与uds之间加入新东西时,一定是痛苦的;后面并行刷写时,果然验证了我的想法。所以咋进行简单的评估后,我给领导的答案是没法优化,要么重构,要么就一直解决bug。我的直线领导爱国真的是一个很nice的人,自己简单看了代码之后,肯定了我的想法,随即给我定下来了重构计划,并取名浴火重生,项目过程中遭到PM的battle时也是力排众议的支持我。
2. 重构的结果(现在的状态)
现在重构后的uds已经正式的进入了正常的软件版本中,后面也会一直持续的迭代,目前看来的结果至少是超出我的预期,在提前一个版本release、并且只有一周多的部门内部测试的情况下,不仅没有出现以前发现的问题,并且本身有代码带来的问题更是极少。从目前结果来看,主要包含以下方面状态以及原因:
- 整个项目将近5万行代码,有将近4万行是完全重写的,uds service移植的现有代码,bug较少(050有22个 issue,其中大部分是需求遗漏问题),因为:
a. 因为我在软件划分了很多模块,大模块里面还有很多小模块,每个模块开发完,我都进行了测试,最后一个较大模块开发完后,又用开发后的模块来完成自测,基本保证了再模块开发时即保证提前发现bug
b. 自己提前使用测试团队会用的测试步骤、测试工具来完成测试,也就是本应该测试发现的问题,在开发过程中就以前解决了
c. 提前使用,由于旧的uds还需要增加功能,所以就提前把代码放到了实际的业务场景上使用了,这对发现尤其有帮助;所以我也一直在推广其他也用,主要是为了发现问题,让软件更稳定。
d. 严格follow规范,不做为了减少工作量而投机取巧的行为 - 解决bug迅速,虽然bug不多,但是即使出现了bug,很多测试刚发现,甚至还没来及建bug,我这边分析并且解决完了;主要依赖于对模块的解耦,出了问题了,能够很快能从大模块再定位小模块。
- 有一些偶发性的bug,不需要测试多次复现,可以依赖日志判断出问题所在,因为:
前面提到的模块解耦 - 日志,在很前期测试的同事就跟我说,日志需要能够反馈代码运行的轨迹,尤其是数据流的路径,所以在开发过程中,也尤其注意这点
- 需求遗漏导致问题,这也是issue的主要来源了,主要是以下原因:
a. 因为重构和原有的项目是在并行,需求分析还停留在了较早的阶段,后面没有持续的跟踪需求,导致需求遗漏,又加上提前了一个版本上线,所以最终问题在测试阶段才发现。
b. 提前一个版本上线,所以很多需求实现在了旧的代码上了,在移植的过程中遗漏了
总之,这次重构的结果是好的,至少我觉得没有辜负领导的信任和投入,真是要人给人,要资源给资源了。
二 、关于模块解耦
前面提到了很多解耦带来的好处,具体在项目实施过程中,是如何做的呢?我觉得模块划分是需要根据功能进行划分,如果开发的是一个在某个领域使用教广泛需求,那么很多时候规范就已经做好了较好的模块划分,此刻只需要根据实际需求进行必要的拓展即可。
1. 整体划分
如下图所示,首先根据iso规范上对整体协议的划分(协议整体),我整体的软件划分给出如下图2部分的设计,其中大部分是跟ISO给出的7层模型完全匹配。其中我拓展了三处:
doip,阅读13400的规范后,我得出的结论时,doip绝不止只会跟uds(或者类似图上的diagnostic router)交互,因为从13400提到的不少东西都是会由应用层来实现,比如路由激活中的验证(需要验签算法)以及确认(甚至需要UI实现),所以我在定义设计前期就确定了doip可以在诊断的应用层访问到
显式的增加了诊断路由模块,这个模块主要两个功能
- 完成doip↔doip, doip↔docan以及其他诊断传输协议的路由功能,并且能够很轻松的控制路由策略
屏蔽不同诊断协议的差异性,比如cantp最大是4095个字节(不包含拓展后的4G字节)基本都会等一个完整包收完才处理或者完整包准备好了才发送了,但是doip不是,它经常是一个大包,如果都是缓存下来会造成的很大延时以及内存浪费,所以最好能够边准备边处理。所以诊断路由这一个模块,我能将cantp、doip都按doip的方式来处理,以后其他的xxxtp也都能这样做。并且不管对cantp还是doip,我最终都是由一个id来标识逻辑通道,类似tcpip的socket id一样,这样站在uds来看,可以最大程度的不关心传输层的差异了,这就很符合uds的Unified了。
在主要实现14229-1的uds上,将server和client的差异放到了更高层(图中lib_server和lib_client),这样就能最大化的复用会话层以及表示层的代码了(图中uds部分)。
得益于之前的工作经历,在很短的时间内(不到一个月)就想清楚了每个模块的大概实现思路,实现了不少代码,并且给出了更为详细、准确的工作量。考虑到时间关系,最终选择了将淡蓝色框里的模块外包出去了。至此uds-stack的所有基础库都正式进入了开发过程。
2. 大模块里的小模块划分
上面只是较为简单的整体划分,针对一个大模块里面其实还应该有较小颗粒度的划分,这里就要充分应用面向对象的思维了,这里以上面较为复杂并且稳定性更加重要的doip为例。先看图,由于我们是网关,
1. 首先确认了doip中的三个小角色,equipment、node和gateway(均出自13400规范里的名字定义)
2. 另外为了能够更加广泛的应用,我把doip分成了socket adapter和doip core,这样我就能在doip core里专注于实现13400协议的实现,在socket adaptor里专注于socket控制以及数据收发
3. 将config模块独立出来,这样即使将来配置形式在变化,不影响其他两个模块。ps:中间还真重构过config模块
以上三个模块,均是以动态库的形式存在的,利用语言特性,充分保证以保证模块间的解耦。关于动态库为什么可以能够保证解耦,可以参考什么?你还不懂链接
在doip core模块中,我又拆分成了一个个小的模块,每一个小的模块都可以认为是一个对象,所以最终就是所有的对象匹配起来。在这个过程了,同时也定下来一个规则:一个doip进程最多可以由一个server+n个client组成(分析规范后定下来的基本规则,最终从doip socket adaptor贯穿到uds)。具体模块作用就不再详细展开了。但是有一点还是想说明下,在doip core中,可以看到有一个chanel,这个小模块的作用主要是将任何平台的socket通信都抽象成了channel(自定义的,没啥特殊含义),也就是最终doip核心实现部分都是跟channel交互了,而不会随着不通的平台,一直在变了。并且,通过语法层面的限制,最小化了 socket adaptor的内容传递到doip core其他模块。
在socket adaptor模块中,根据基础功能,较为简单的划分成了以下几个模块,这些都是比较好理解了。主要是udp、tcp client以及tcp server组成了,其中包含前面提到的doip=1个server+n个client,并且对这些东西都用endpoint来抽象替代,以更好的拓展;再加上doip最新规范中的tls。
这章介绍的主要是我在开发过程中,在拿到一个项目时,是如何根据规范划分成一个个模块,又如何应用面向对象的思维在模块里又抽象出一个个对象来,然后将一个大项目变成了更小颗粒度的对象,最终完成项目变成实现对象以及组合对象了。正是因为这些设计,保证了出现了bug时,能够很短的时间内就定位问题,并且解决问题,毕竟每个小模块的作用域都是有限的。并且每个小模块又是开发过程中经过测试的,最后组合起来的质量自然也不会太低。
三、关于实现上的技巧
由于c本身是面向过程的语言,所以为了能够更好的达到前面提到的效果,还需要巧用一些技巧。就我再项目里用到或者实现了的技巧聊一下。其中在代码重构-1中提到了,如何用c实现私有成员。
1. 指针永远的神
指针永远的神这个相信嵌入式的工程师就不用了,但是巧用void *能够更好帮助你解耦和实现功能。以上提到了doip上把socket通信变成了channel通信,但是socket adaptor上又是endpoint和channel进行通信,如果不将endpoint传给上层,就需要进一步适配;如果将endpoint传给channel,就将adaptor很多信息暴露给了channel,不利于解耦。此刻void *解千愁,我们可以:
在初始化时,将void *给到doip core,
void (*doip_channel_init)(struct doip_channel_s *dch, uint16_t remote_la, const void *ud);
在发送时,把地址原封不动传下来,doip core就实现了“万花丛中过,片叶不沾身”。
int adaptor_transmit(void *buf, int len, struct peeraddr_s peeraddr, const void *ud) ;
void *真的是个好东西,其实不止有这些用户,借助void *可以很容易在C语言中实现某种意义上、类似于c++的多态。
2. 模板函数
熟悉c++同学对模板可熟悉不过了,其优点也不再细说了;看过linux 内核代码或者商业classic autosar代码,一定会觉得上面宏根本不是给人看的。所以我们也能巧用宏了实现c++的模板函数。
为什么需要这么做?在最前面有简单提到过,基于14229协议的特性,我们使用模板函数,这是因为14229的每个服务的思想都是类似于的,无非就是service id+subfunction(如果有)+payload,那基于此特性,我们就能在某个层次使用模板来实现一个统一的函数。模板函数在实现14229时基本上算是大规模使用了,以下直接拿一个比较简单的实际代码出来分析。
#define UDS_CLI_SVC_IS_NULL(module, sid) \
if (connect == NULL) { \
LOG_ERROR("connect is null"); \
return UDS_SERVICE_RETURN_FAIL; \
} \
if (connect->svc_##module == NULL) { \
LOG_ERROR("connect of svc_%s is null", #module); \
return UDS_SERVICE_RETURN_FAIL; \
} \
uds_service_##sid##_t *service = &connect->svc_##module->svc_##sid; \
if (service == NULL) { \
LOG_ERROR("connect(ta=[0x%04x]) svc_%s is null, response data ignore", \
connect->remote_addr, #module); \
return UDS_SERVICE_RETURN_FAIL; \
} \
if (service->resp == NULL) { \
LOG_WARNING("connect(remote addr=[0x%04X]) svc_%s sid=[0x%02X], resp " \
"is null, maybe no req, but " \
"have resp)", \
connect->remote_addr, #module, sid); \
}
14229的实现也是沿用了doip的思路,充分对模块再进行划分,比如最终对所有service都划分成了dcm、dt、ioctl等模块(对应模板函数的module),每个模块都有若干service,但是服务具体在不在就需要判断。如果通过具体函数来做,要么需要实现多个函数(多少个模块多个函数,利于解耦);实现一个大函数,在里面进行判断(不利于各个module间的解耦)。但若实现成上述的模板函数,上面的两个问题都没有,connect可以在上下文中获取(宏展开后就是语句了),module和service都可以直接根据传入来判别,最终只要如以下形式调用就能够判别了。实现成模板除了有以上好处,还会变相要求你,在设计代码时能够更好划分以及实现“对象”,毕竟如果“对象”没划分好,是很难抽象出类似的模板函数的。
3. 选择合适的算法
很多人会觉得算法是没用的,但是如果你了解算法,你能够很巧妙的将算法在你的项目里应用起来。如下图所示,diagnostic router中一大功能就是路由,但是如何使你的路由能够更有通用性以及随着需求变化能够最小化改动,这是最为关键的问题。我们的ecu模型跟一颗普通树一模一样,借助算法能够很容易将模型在代码中实现,并且树的深度不限,再加上配置文件,最终能够很小很小改动就能应付需求。比如:
1. 要添加一个ecu路由,我只需要配置文件中,在对应的树中加上一个节点,
2. 要改变ecu的父节点,比如SA既可以通过aurix再转can来完成诊断通信,也可以直接通过imx使用doip来完成通信,此时我需要将SA分别挂在不同的父节点下就ok
四、日志
日志是很多人忽略的一点,在项目前期阶段,测试的同事一直在跟我强调日志的重要性,现在整个项目完成下来,我特别感谢测试同事(jun.kuang)反复提到日志的重要性,不然在解决那几个偶发性bug上,不会这么顺利的。在review供应商代码时,我发现他们这一块就做的不够好,所以产生问题时,查找起来较为费劲。项目总结下来,我认为好的日志必须是:
- 等级需要准确(fatal、error和warning一定不需要跟info、debug和trace混了),这样能够方便release时设置等级但关键信息不丢失,并且一定包含了所有异常,比如出现了异常,搜索fatal、error或者warning是一定能够搜的,否则就不是合格日志
- 由于这个项目主要还是数据处理,所以日志必须能够体现数据流的路径,比如作为server,收到request再到回复response,日志一定需要能够很好的展示数据都经过了哪些模块,模块处理结果是否符合预期;
- 关键分支也一定需要打印当前条件值,这对解决问题尤其重要
五、写在最后
重构暂时告一段散落了,因为最起码基础库是整体重构完成了,所以心里也算是石头着地了。但是由于时间关系,肯定还有不少实现还可以再优化、再改进,比如我就有一些记录,如下图。同时我觉得这次重构也算是一个半成品吧,uds service层是移植的现有代码,站在个人角度,uds的service真的特别是应用SOA的思路来完成,我们又刚好将common api的框架应用了起来,所以这部分我目前让大连那部分在重构了。还有就是,重构基础库一个目的就是希望能够更多的用户去使用它,基于当前pyclient的一些痛点,也希望能够应用重构后的基础库将python实现替换掉;另外就是也希望测试sdk也能用起这套来。
最后,真的很感谢这个项目中领导、同事的大力支持,领导们资源上给了最大的支持和信任,让我可以心无旁骛的一直在重构,其他同事也给了很多实际的帮助,在开发过程中,好多次都怀疑自己在et7这个车型上完不成重构了,都是同事们的支持一直让我不减激情的一直做下去。
A fascinating discussion is definitely worth comment. I do think that you ought to publish more about this topic, it may not be a taboo subject but typically people dont speak about such issues. To the next! Many thanks!!
I blog often and I truly thank you for your information. This great article has really peaked my interest. Maxwell Wainer
Hello mates, fastidious post and pleasant arguments commented here, I am actually enjoying by these. Benedict Stoops
Amazing things here. I am very happy to see your article. Erina Bruce Pendleton
Pretty! This has been an extremely wonderful article. Thank you for providing this information. Israel Bladt
You should be a part of a contest for one of the best blogs on the internet. Douglas Strathmann
Some genuinely prime articles on this web site , saved to favorites . Vance Ferrick
Itís difficult to find knowledgeable people about this subject, but you sound like you know what youíre talking about! Thanks
There is visibly a bundle to realize about this. I feel you made some good points in features also. Rosendo Schmandt
I was studying some of your content on this internet site and I conceive this website is real informative! Retain posting. Jeromy Suddarth
I truly appreciate this blog. Really thank you! Much obliged. Felipe Hoeger
I have read so many articles concerning the blogger lovers but this piece of writing is genuinely a good article, keep it up. Rodrick Tourtellotte
I pay a quick visit each day some web pages and information sites to read articles, except this web site presents quality based content. Ulysses Underhill
Way cool! Some very valid points! I appreciate you penning this post and the rest of the site is really good. Alvin Merz
What a material of un-ambiguity and preserveness of valuable experience regarding unexpected feelings. Val Nowack
Hi there friends, its impressive piece of writing concerning cultureand fully defined, keep it up all the time. Rhett Rodas
Reiciendis rerum ipsam sed repellendus et aliquid. Animi voluptas esse quis occaecati et et aut molestiae. Domingo Monteros
Good post. I learn something totally new and challenging on sites I stumbleupon everyday. It will always be helpful to read content from other writers and practice a little something from other websites.
אני מאוד ממליץ על אתר הזה כנסו עכשיו ותהנו ממגוון רחב של בחורות ברמה מאוד גבוהה. רק באתר ישראל נייט לאדי <a href="https://romantik69.co.il/">https://romantik69.co.il/</a>
Some truly nice and useful info on this site, as well I believe the style and design holds superb features. Marcel Surguy
Hi! Someone in my Facebook group shared this site with us so I came to taske a look. Andrea Jurisch
Im thankful for the post. Much thanks again. Keep writing. Jarrod Deur
Awesome article post. Really looking forward to read more. Cool. Ervin Siami
There is definately a great deal to find out about this subject. I like all the points you made. Michale Baxtor