梦断代码--一个程序员的自白(七)

本文谢绝转载 http://www.weibo.com/0x2b

    大约是各方面对ADP的反响都不大好,ADP要想推销自己,必须找到新出路。最终,目标锁定到Protein,让Protein通过ADP来存储材质包。对于Protein项目,我至今也不知道最初的立项缘由。Protein后来被定位成一个内容(材质)管理库,为公司的所有产品提供统一的材质管理。最早,Protein是基于FBX(一个3D文件格式和SDK)来实现的。ADP同事加入的是Protein 2.0。由加拿大的一组同事设计了一套新的API,表面上隔离了FBX。O和G做了将ADP集成到Protein的所有关键工作。除了用一个单独的DLL用来作为胶合层外,ADP也为此大作修改,几乎重写了runtime部分。此时,大部分中国的同事,也包括我,是对这些工作一无所知的,或者说是漠不关心。我只是做一些常规性的工作,解决各种编译、移植、测试问题。随着ADP集成结束,我也转到了Protein项目。合作者也换成了加拿大的同事。


    在产品集成Protein 2.0的过程中,遇到了许多早就预见到的问题。我称之为,明知前面有坑,被推着眼睁睁往下跳。我早在ADP时就说过,即使把数据存成文本,依然需要制定存储和转换协议,并预言中的问题却一一变成现实。一个是本地化的问题。例如在德语中,千分位和小数点符号不同于英语,而是相反。因此,“3.142”这样的数,到了德语里面就被读取成3142,这显然是不能接受的。这个问题在项目早期很好解决,就是要规定外部文本的解析方式,比如统一使用C locale保存就很容易搞定。另外一个是精度问题。比如0.3这样的数。用户输入的0.3,下次再打开,显示的却可能是0.2999999999。其实对于明白原因的人来说,这再正常不过了,可是对某些人却是难以接受的。由于Protein内部同时使用float和double类型,double还好一点,float出现前面问题的机会非常大。于是一个凑合的方案出台了,就是全部改成使用double,显示的时候,截短显示位数,利用平台API的能力来达到显示为0.3的效果。这种做法是非常脆弱和幼稚的。我们需要通过制定数据规格,让用户理解,什么样的显示差异是有道理的,也是符合预期的,而不是凑一个让用户满意的结果。如果我们明确了这种差异和差异的原因,用户代码自然就有据可凭,去填补数据表示和显示之间的空隙,而不是一团糊涂地代劳。事实上,提高精度和利用平台API的方法,在用户做了一个float到double再到float的转换后,因为精度丢失,就失效了。精度问题在后来Protein 3.0引入限制数据范围的特性后变得尤为严重。从字面上看是合法的数据,但是代码执行中却可能是非法的。这是因为double转成float会四舍五入,所以舍入后方向并不确定的缘故。这种问题对某些软件来说完全不重要,对另一些软件来说,则是至关重要,因为它影响到数据正确性。我们公司的软件正是后者,多是用来做各种设计--也就是创造数据的。


    另一个我认为很重要的数据完整性(包括正确性)问题,则从未得到重视,或者说从未被理解过。在我看来一个很显然的事实是,对于ADP、Protein要处理的任何外部数据,都要持恶意假定,即可能是任何形式的坏数据。在此情形下,在处理到任何坏数据时,都不能出现崩溃或其他的致命错误(如内存耗尽,死循环等等)。即便是安全性错误也是要竭力避免的,如临时文件漏洞,缓冲区溢出等等。对于保存数据,ADP采用了创建--改名的机制,虽然很慢,很笨拙,却也有效地避免了数据完整性问题。就地修改的特性,且不采用回滚日志,最后也没做出来--幸好没做出来。时至今日,ADP、Protein在读取到坏文件时,几乎100%会发生故障,安全性问题更是没有任何评估。然而就这样的东西,现在居然有人将之部署到网站,用来处理用户提交的数据文件!这是打算引诱人犯罪吗?


    当被自己预言过的问题坑了的时候,我不会觉得自己英明,而是为自己的无能为力感到羞耻乃至愤怒--愤怒不过是无能的另一种表现形式罢了。乃至于到我快离开ADP时,还给了我一个几乎让我暴走的任务:为了避免阻塞在费时的ADP保存数据操作上,要让写出过程是异步的,用多线程解决。要知道,这是ADP的运行时太慢导致的,即使我异步处理,仍然要读ADP运行时数据,而且为了保持数据一致性,就必须要锁住ADP系统--ADP可没有快照机制--直到保存数据操作完毕,那么要异步何用?这个任务我拖了好几个星期,并且被告了一状。最后说我临时先写一个,将来会用一个多线程IO的东西将之替换掉。没办法,我只好去做那个有害而无益的任务。在后来的Protein中,我不知道有多少次遇到多线程IO上出现的bug,而且,每次定位和修复bug都要花费好几天的时间。唯一值得庆幸的是,我写的那个临时方案确实很快被替换掉了,我不必为此背负骂名。


    Protein团队的组织有点松散。我到了Protein长期处于无事可干的状态。在完成了Property和PropertyTable的实现之后,发了两次邮件请示工作无果后,我也就懒得关心,看看代码就是了。Protein由多个模块组成,我们的主要工作是其中一个dll,即提供对材质库的访问和管理,以及为材质对象建模。Protein API的风格应该是受Java或者.Net的极大影响,全部设计为接口,整个库导出一个唯一的全局函数(GetIAssetLibraryManager())作为入口点。因为没有导出任何实例类,所以一切的工作都要从这个library manager对象出发,逐步展开。对于这样风格的API,如果设计的好,未尝不可。但不管怎么说,这样的系统问题也是很明显的:
    1.难以获得语言的支持和配合。C++鼓励扁平化的类设计,为什么?说白了就是对非扁平化的设计支持不好。
    2.从依赖关系上来说,library manager依赖整个系统,任何使用了library manager的地点,都隐含地依赖整个系统,这回导致系统僵化,难以重构。
    3.无法直接创建任何一个接口的实例类,必须从library manager出发去创建。而library manager又不是factory,因此,任何一个组件都无法从系统中单独拿出来考察。这意味着组件隔离做不到,按需创建测试目标代价高昂,单元测试将非常困难和低效。
    
    除了两个给用户的扩展外,Protein的每一个接口,实际上只有一个实现类--就像Pimpl的接口和实现那样。然而,和Pimpl不同的是,Protein的接口和实现类的功能不是一一对应的,接口功能只是实现类的一个子集。这就导致一个很大的问题,在Protein的实现代码中,拿到一个接口指针,第一件事就是Downcast成其对应的实现类对象。这是典型的抽象不足的表现。然而,我们是缺乏API重构的权限的。事实上,大多数项目都缺乏此权限。我当然理解API变化会影响客户代码,但是我不明白,为什么在项目早期也不鼓励改进API呢?我坚信,没有人可以在不写实现代码的情况下就设计出足够好的API来。


    另外,因为完全依赖C++的接口,在C++中就不可避免地要返回接口指针,如何维护这些返回对象的生命周期就是要慎重对待的。当然,如果有统一的约定,情况还好些,可惜Protein并不统一,所幸问题到也不很严重。Protein的实现完全是构建在FBX之上的,因此了解FBX也是必要的。FBX当中有一些质量不错的东西,但是也有大量不可理喻的东西。我不了解FBX的历史,倘若FBX最初写于1995年前,我觉得这就是一个不错的东西,如果写于2000年,那就平庸得很了。这让我想起AutoCAD的2D引擎Heidi。很多人对Heidi表示不屑,但是如果考虑到那是在80年代一个人搞出来的东西,就不能不表示尊敬,Heidi达到了它那个时代业界的最高质量标准。即使放到今天来看,Heidi所表现出的简洁性、一致性、严密性仍然是值得称道的--尽管已不符合当今一些人的口味。


    除了接口,Protein的实现质量在我看来也是很糟糕的。各种special case--我就不知道有啥不包含特例的--对我这样的人来说,记忆这种逻辑不明的东西就是个噩梦。大量的代码逻辑复制粘帖自FBX,以至于读代码是完全不知道在干什么,性能也很糟糕,各模块的交叉依赖简直是做到了极致。另外,Protein还附带了一些渲染处理,界面工具,而非仅仅是一个数据模型和包管理。这种混乱的层次关系也让我对Protein很是不喜欢。Protein唯一值得表扬的就是,它确实做了一件用户需要的事情,用户也确实在用。因此,我认为无论如何,Protein比ADP强。特别地,当有人拿ADP和Protein做比较,说Protein质量差时,我都忍不住要反驳:Protein很烂,但是有人离不开它,他有用;ADP不那么烂,但是所有人都不想要,恨不得扔掉它。至少从我的角度来说,我希望ADP彻底消失。一来可以使用了它的那些软件免于腐蚀,二来可以掩饰我的失败。

    Protein所谓的材质(Material),实际上是一个属性集合对象(差不多是这样,Protein管它叫Instance)不同属性可能有不同的数据类型和值,某些属性还引用了另一个Instance。Instance中的属性值有些可以改变,有些不能改变。Instance不能添加或删除任何属性,因此,每个Instance都从一个模板--叫Definition--clone出来的。可诡异的是Definition没提供接口去访问其中的属性,要想查询,先clone一个Instance吧!想修改,没门。到了2011年,我们还搞了个叫Schema的东西,专门定义Defintition/Instance有哪些属性,属性的数据类型,默认值等等。这种三层的结构实在是让人头疼到爆。然而,我们无权更改API。

    

    我不想再去吐槽那些让人抓狂的错误设计,毫无营养可言,回忆它们也不能从中汲取任何有益的教训。我每次在代码上受到挫折时,就到那个ADPLite上写一点代码,舒缓一下心情。然而写不了几天,我就又要开小差了--我都不在ADP干了,写那玩意儿干啥--去尝试和验证各种新鲜的想法,甚至就是学习。然而,在这断断续续大半年的时间里,我发现也积累了一点代码。我厌烦了牵挂这么个东西,想做个了断。

你可能感兴趣的:(C/C++,笔记,manager,library,api,float,多线程,工作)