首页 > 代码库 > 实例说明什么是代码的坏味道,如何重构
实例说明什么是代码的坏味道,如何重构
所谓优雅的代码,或者恶心的代码,很多时候是见仁见智的。也同时是看个人喜好或者习惯的。当经验不足,看的和写的代码还不够多的时候,我们可能会追捧某个大神或者奉某本经典为圭臬。然后跟学校的学弟们说,有空多看看《重构》和《设计模式》吧。
在我看来,优雅的代码并不是说这个代码写的有多神,多么让人惊叹。能够让人清晰的去阅读去理解就是好的代码。代码并不是艺术,更多的是严谨的表达出自己的思路。在这个过程中代码的易读性是第一位的,然后是正确性,然后是运行效率。
让人感到恼火的代码也并一定是写的多么凌乱。它们可能非常的中规中矩,甚至是以大量高精尖的设计模式为基础的。但是相信我,阅读这样的代码绝对不是一件轻松的事情。比如ACE,它封装socket后的代码量甚至比操作系统实现socket还要大,而且大很多。我需要你封装后能够简化概念,让我使用某个功能变得更加容易、更加健壮,让我不用操心底层细节,但是ACE的结果是让你知道了更多的概念,让你需要注意更多的细节,让你的网络功能更加脆弱。这样的封装意义何在?
当然,凡事皆有例外。有很多库是代码几乎没法看,但是功能强大到无以复加。并且它们几乎是不可替代的。比如freetype,比如curl,比如stl...... 这有点像一个人长的丑是缺点,但是如果有才华的话,那么缺点就不再是缺点,如果有大才的话,那么缺点反而会成为特点。这些不是我辈可以效仿和学习的,只能膜拜。
最近我一直在为光效编辑和光效渲染发愁(可恨没有使用Unity,原本这些不应该我来操心的)。仔细钻研了Particle Universe的代码。按理说,它的代码风格是沿袭了OGRE的代码风格,曾几何时我还是相当膜拜OGRE的设计思路和框架的。但是现在我非常讨厌绕来绕去的代码,举个例子,相比起一个文件3w行代码,我更加无法忍受的是3w行代码分到300个文件中。
Particle Universe中有这么几个核心的组件,一个是实际起作用的组件如Emitter、Affector等,一个是其对应的解析器如EmitterToken、AffectorToken,第三个是其对应的属性窗口,如EmitterPropertyWindow、AffectorPropertyWindow。
如上图所示,我要添加一个新的属性,要修改三个文件、N个地方,如发射器类、解析脚本、写入脚本、属性窗口初始化、属性窗口赋值、属性修改等等。这就是代码的坏味道。它不利于扩展,不利于维护,不利于阅读。从这里也可以看出写编辑器,语音支持反射是多么重要的一件事情。比如Unity使用C#,QT花了很大力气实现了属性功能。
那么我会如何重构它呢?回答是,我暂时不会去动它。因为重构的首要前提是维持功能不变。这套东西是代码的基础,一动就是伤筋动骨,在没有详细测试和特效做支撑的情况下,学会习惯和忍受就是最好的重构。我突然想到了cocos2dx,为什么明明它的功能愈发强大了,但是我对它的印象越来越差?作为3D引擎它连入场的资格都没有,而作为2D引擎,它越来越庞大、越来越厚重,像是老年迟暮一样。而且最恶心的是,它每次升级版本都会有不向下兼容的修改,cocos2dx和cocostudio也互不兼容。比如我使用cocosudio1.6,那么你最好不要使用v3的cocos2dx,乖乖使用v2的版本,因为不互相兼容。最新的cocos2dx使用FlatBuffer来替代ProtoBuffer,那么你的cocostudio也必须同步更新,否则无法加载配置,反之亦然。
虽然我暂时不会去动它,但是想法还是有的。维护一个统一的表,比如excel,里面描述好一个字段ID,显示的属性名字,值类型是什么,默认值是什么。然后程序加载这个表的数据来生成属性窗口和解析脚本内容。这样添加一个字段的时候只要在一个地方匹配好字段对应的表中字段ID就可以了。很眼熟不是吗? 其实游戏中的GUI都是这么玩的,通过一个配置生成UI内容。代码中注册回调来处理UI响应。我们这里是属性窗口,UI更加统一和一致,所以处理起来更加简单。
最后再提一个它蹩脚的设计,引以为戒。 它有一个enumToken的枚举,同时还有一个tokens的字符串数组,其顺序严格对应。程序中使用字符串的地方都使用tokens[枚举值]来索引。这个设计最蹩脚的地方在于字段数目很多的时候(现在有300个左右),你很难知道一个枚举具体对应哪个字符串。
修改方式,如果是c#中,直接定义一个类,其内部是public static readonly XXX = "xxx"。或者更近一步,直接使用枚举,C#中的枚举是可以转换为字符串的,当然这样需要枚举名就是写入脚本的字段名。
在c++中更加简单,直接使用#define XXX "xxx" 简单清晰无污染。
实例说明什么是代码的坏味道,如何重构