CAD/CAM 软件架构总结

2019-03-02

  2014年,我第一次参与了桌面端的软件开发项目。从惴惴不安到坦荡对待。2015年中,帮助设计了新版的CAD软件架构,那个时候,我们几个人都没有过架构大型桌面端软件的经验,参考着各种资料摸着石头过河。直到架构稳定下来,我们也废弃了不少原有的设计,不断的重构、重写花费了大半年的时间,不得不承认这中间有部分的时间是因为我们的经验不足、错误的设计导致的。今年我又参与了一个新的3D CAD项目,处于初始阶段,软件的架构需要重新实现。我也有更多的信心了。不过按照人月神话的作者经验,错误设计之后容易“过度设计”的。需要注意。我也需要对CAD/CAM或者说是桌面端软件架构设计做一下总结。
  CAD/CAM 软件实现过程中,难度最高的当属各种计算集合的算法了,而最复杂的,当属界面交互操作相关的设计。一旦UI层设计不好,将会把不良影响逐渐传递到底层。类似软件一般有几大模块:UI,3D 显示,三维交互,Undo/Redo,scripting,文件处理,报表生成,嵌入式设备通信与管理,各算法模块,Log。一般情况下,十余人甚至二三人十人的协同开发,分为四五个小组,大的项目如maya,估计有专职开发者几十人。不做好架构设计,组员就会遇到问题,架构上对各个组件的限制,可能组员经常就跑向组长,说因为系统设计的限制,做这个功能很麻烦,或者一个人进行功能开发,却容易改动另外一个人负责的功能。 UI模块
 好的UI lib是非常重要的。我一直推荐Qt的原因倒不是因为Qt的代码质量好、设计好,而是Qt是一门比C++更高级的语言啊(应该是可以这么说的)。语言表述能力的增强,才是解决问题的最高效途径。假如有一天,计算机程序构建可以接受自然语言输入,那么我们能简单的完成这些编程任务了。
2D、3D显示
  我们一般需要一个渲染引擎,二维显示中,这种需求不是很大,我们可以自己实现一个简单管理引擎。三维渲染引擎,一般基于OpenGL或者DirectX接口。但是,对于CAD/CAM这样的程序,选择OpenGL即可。可跨Windows/Linux平台,Mac OS将来只支持Apple自己的Metal接口。不需要同时支持DirectX接口。OpenGL的确很难以使用,但是,对于渲染团队而言,也并不是难点。OpenGL的下一代标准是Vulkan,接口繁杂,但是,整体上更容易理解并使用。对于三维引擎,我们一般有两种选择:1,选择OSG、OGRE这样的第三方引擎;2,自己写一个。对于新项目,我反对自己实现渲染引擎。对于已经稳定的项目,并且对于渲染算法有较多的定制,当然最好是使用自研的渲染引擎。我并不推荐封装很高层的lib。就如我一个同学的想法,机器学习也可以不用学习,到时候调用别人封装好的lib就可以了。当然可以,但是,代价是你将失去对底层的控制能力。
二维、三维交互
  对于二三维交互,我们一般需要拦截二三维窗口的事件。这一般不难,可以建立一个抽象的Operation层,多个Operation都能拦截二三维窗口的事件。二三维渲染线程,还有可能存在的算法线程,与UI层(其实就是Operation)的通信,就是最大的问题。这里也是MVC模式体现最明显的模块。 因为Qt的signal/slot能够非常的解决耦合、多线程异步通信问题,我们可以让Operation模块、渲染模块、算法模块都依赖于QtCore。这比自己实现多线程通信要简单、稳定的多。
算法模块
  因为算法模块的独立性较强,有算法团队专人负责,一般不需要担心对于模型的侵入。但结合到程序中时,为了数据访问、交换的方便、算法的高效性,可能选择直接使用主程序中基础的数据bean,这样就耦合了。这种选择需要结合算法的独立性、主程序对该算法模块的性能要求来决策。
Undo/Redo
  这个模块需要使用command设计模式。基本上是冗杂的工作,需要小心谨慎。command 的 execute()、unexecute()的配对操作,一定不要有所遗漏。command最好记录操作,而非记录之前的数据。我们一般不需要担心多次计算的精度问题。
脚本嵌入
  脚本嵌入中有两大问题:接口暴露、内存管理。与单纯的C++接口设计不同,跨语言的接口设计更需要小心设计。所幸,脚本模块对于原系统来说是非侵入性的。其他模块一般不对它产生依赖。对于脚本语言,不要使用过多的高级特性,尽量让接口足够底层,足够简单。因为,通过嵌入的脚本完成的工作本身不会很复杂。 推荐看一下《C++ API设计》这本书。关于API设计,特别是C/C++ API设计,以后单独总结。
文件处理
   对于标准交换格式文件的读写,最好使用第三方lib。需要定义好接口,底层读取写入算法实现可以随时被替换掉。对于项目所需要存储的自定义数据,我们一般有两种保存格式:文本格式;二进制格式。这便是序列化与反序列化。对于Java、Python这样的程序,对于对象的序列化有语言的直接支持。但是,对于C/C++,只能自己写代码完成。boost库支持C++ class 序列化,有入侵式与非入侵式。当然,最好是选择非入侵式的方式。避免其他模块对于boost产生依赖。序列化能够很好的解决数据bean 持续变动的问题。因为,我们基本上不可能一次性的设计好数据结构。而且,随着算法模块持续演进,必然需要对于数据bean做出修改。另外一种方式是采用文本方式保存数据bean。一般来说,我们最好设计好接口,二进制、文本方式都需要支持。
报表生成
  报表生成一般需要保存为word、excel、PDF、HTML、cvs、txt等格式。各个软件对于报表生成的要求不一,有的软件或许只需要输出一些cvs、excel即可,数据比较标准,有的软件可能要求输出PDF、HTML,甚至还需要截取三维显示的内容,光报表生成可能就需要两三人负责开发。但是,报表模块一般只读内部数据,在接口设计上,多使用const。对于多种报表生成器,需要定义好接口。对于数据成员的访问,使用好Visitor模式,避免为了报表从class中提取数据,而增加一些用途不大的接口,导致代码臃肿。
网络通信
  最好不要使用原生的接口来实现网络通信功能。因为我们是在实现应用程序,而非lib。支持跨平台的网络通信lib 是最好的选择。通信模块一般不是重点,也不会专门调人开发维护。使用lib的好处就是稳定,且有资料可查。对于公司内部项目而言,重要的上层通信协议。对于协议的设计,要做好不断迭代并拓展的准备。
Log
  log模块的作用是非常重要的。因为C++程序内部错误导致crash的情况比较多,且C++程序多交付给第三方使用,重现步骤就会变得比较困难。log是帮助修复bug的重要信息来源。对于简单的应用,可以使用同步log,但是,对于CAD/CAM程序,线程可能较多,就需要异步log了,避免计算的线程与磁盘读写的阻塞问题。对于所需日志较为详细时,可能需要通信模块支持,建立日志服务器,避免占用磁盘。

  关于错误。我们曾经犯过不少错误。编程相关的决策错误大抵是不会导致软件项目失败的,特别是在当前这种互联网公司持续运营项目的模式下,多数程序在服务端服务端运行。软件开发团队会持续的迭代项目。每次交付的内容也是阶段性成果,对外交付的是服务。不像二三十年前,软件交付并分发出去之后,迭代改进只能等到下个版本了。所以,对于我们软件开发者的压力也小了些。但是,对于需要对外交付的软件,则需要格外小心。
  关于UI lib的选择,不要选择MFC或者C#的WinForm或者WPF,或者其他小众的UI lib。最好需要Qt。因为,偏工业软件并不需要在UI 显示效果上有很高的要求,即使是Maya,2011年之后也是采用Qt技术实现UI的。用户最在意的是软件的核心功能,炫酷的样式或者交互只是锦上添花。
  对于,模块之间接口暴露程度。越是偏上层,越没有必要隐藏模块内的数据结构。需要隐藏的东西最好设置protected或者impl 实现。对外提供指针,而非id或者某种handle。之前吃过这个亏,非常大的亏。我从最开始时便反对这种方案,奈何没有作用,用handle代替指针给后续两年的开发过程带来非常多的bug。所谓的解耦,并不是把自己的模块隐藏起来,解耦的目的也不是为了将来有一天重新实现某模块,并替换掉老的模块。这不现实。解耦,是为了在后续开发过程中,发现某模块内设计不合理,能以很小的代价进行修改,对其他模块的影响也能尽量的少。
  对于,C++版本以及 std、boost是否选用问题。我觉得,对于新创项目,还是尽量选择新版本C++以及 std、boost,它们的质量比我写的代码质量好太多了。须知,编译器也是有bug的。之前项目中,我帮助发现了一个由vc12 foreach 语法糖导致的bug。而且,对于这种偏上层代码,我们不需要担心其他开发团队依赖于本项目的问题。lib带来的依赖冲突问题基本上不需要考虑。
  对于,脚本嵌入,最好选用Python。Lua真的不适合CAD/CAM项目。虽然Lua语言本身小巧,容易操纵,但Lua语言本身的表达能力偏弱,使用者较少,第三方lib的广泛性以及成熟程度都不及Python。我们在之前的项目就犯过这个错误。
  对于第三方lib的选择,非核心算法模块,或者将来自己的程序肯定会修改实现方式的功能,如果有第三方lib,就尽量使用。最大的原因在于自己写的代码,非常有可能在质量上比不过经过锤炼的第三方lib。所谓的学习成本,还是很低的。并且,一个人的学习,可以形成文档,第二个人完善文档,之后其他人的学习成本就会低很多。
  对于各种方便的、取巧的方法,如使用全局变量,需要保持警惕。须知,Nothing comes for free。使用起来简单,但是,用多了,就会导致忽略模型正确的class 层次关系,对重构模块或者项目带来很大的难题。不要为了避免重复构造关键对象而使用单例模式(基本上单例与全局变量总是一起出现)。不要过分担心软件设计里面的问题,还没有遇到耦合问题,就考虑对各模块解耦;还没有遇到性能问题,就考虑优化而对模型层次关系做出修改 ,这些做法都矫枉过正了。
  关于自动化测试,CAD/CAM软件一般难以进行自动化测试,因为对于二三维窗口的业务操作,很难录制脚本。而且,由于操作交互变化较多,即使实现了脚本录制的方案,录制的脚本有效期也不会持久。这都导致自动化测试工作难以开展。我们可以利用内置的脚本语言,对于主流程做简单的测试,避免录制交互操作,而是使用脚本替代完成,让脚本直接调用内部接口。这便要求各个模块尽可能多的暴露接口给脚本模块。
  在详细设计阶段,一定要做好UML图。并且需要专人负责持续修改。让团队成员对于项目的整体架构保持熟悉。强调一点,《道德经》有言”无名天地之始,有名万物之母“。对于命名,拥有决策权的各组长一定要控制好。尽量避免缩写,避免***Manager这样的模糊不清的表述,避免歧义。

P.S. 2018四月份开始写的东西,到现在才写完,真费劲。可能行文很杂乱吧,本来也不是一次成文,从笔记里面提炼出来的总结而已。

如果有任何意见,欢迎留言讨论。


[ 主页 ]
COMMENTS
本来是想找古希腊的那幅著名的know thyself的图画,却无意中发现你的博客。本人仍为在读本科生,虽不是学计算机相关领域的,只简单体验过一天的编程学习,很佩服,也觉得发现这个网站是今天很意外的一个惊喜。写作真的是一个很好的梳理自己的方式,btw不知道你有没有在其他的平台上发表这些内容,希望能继续坚持,在know thyself的路上不断前进。(到此一游的一点感受,与本文内容无关,对于非专业人士实在理解有难度,见谅哈)
一开始看到你面试erlang,然后点进来看,从12年开始,感触很深,好像跟着你一起成长,看你的经历,感想,你的描述,有一种娓娓道来的感觉。有乐观,也有悲观,看到了现在的自己,确实需要更加努力。
19年都过半了 你也没像17年发那么多文章 可惜了
POST A COMMENT

(optional)



(optional)