一、什么是软件架构?
在一头扎进一堆设计模式之前,我想先讲一些我对软件架构及如何将其应用到游戏之中的理解, 这也许能帮你更好地理解这本书的其余部分。 至少,在你被卷入一场关于设计模式和软件架构有多么糟糕(或多么优秀)的辩论时, 这可以给你一些火力支援。
什么是软件架构?
如果把本书从头到尾读一遍, 你不会学会3D图形背后的线性代数或者游戏物理背后的微积分。 本书不会告诉你如何用α-β修剪你的AI树,也不会告诉你如何在音频播放中模拟房间中的混响。
相反,我们会告诉你在这些之间的代码的事情,与其说这本书是关于如何写代码,不如说是关于如何架构代码的。 每个程序都有一定架构,哪怕这架构是“将所有东西都塞到main()
中看看如何”, 所以我认为讲讲什么造成了好架构是很有意思的,我们如何区分好架构和坏架构呢?
我思考这个问题五年了。当然,像你一样,我有对好的设计有一种直觉,我们都被糟糕的代码折磨得不轻,你唯一能做的好事就是删掉它们,结束它们的痛苦。
少数幸运儿有相反的经验,有机会在好好设计的代码库上工作,那种代码库看上去是间豪华酒店,里面的门房随时准备满足你心血来潮的需求,这两者之间的区别是什么呢?
什么是好的软件架构?
对我而言,好的设计意味着当我作出改动,整个程序就好像正等着这种改动,我可以仅调用几个函数就完成任务,而代码库本身无需改动。
这听起来很棒,但实际上不可行“把代码写成改动不会影响其表面上的和谐”就好。
让我们通俗些。第一个关键点是架构是关于改动的,总会有人改动代码,如果没人碰代码,那么它的架构设计就无关紧要——无论是因为代码至善至美,还是因为代码糟糕透顶以至于没人会为了修改它而玷污自己的文本编辑器。 评价架构设计的好坏就是评价它应对改动有多么轻松,没有了改动,架构好似永远不会离开起跑线的运动员。
你如何处理改动?
在你改动代码去添加新特性,去修复漏洞,或者随便用文本编辑器干点什么的时候, 你需要理解代码正在做什么,当然,你不需要理解整个程序, 但你需要将所有相关的东西装进你的大脑。
我们通常无视了这步,但这往往是编程中最耗时的部分,如果你认为将数据从磁盘上分页到RAM上很慢, 那么通过一对神经纤维将数据分页到大脑中无疑更慢。
一旦把所有正确的上下文都记到了你的大脑里, 想一会,你就能找到解决方案,可能有时也需要反复斟酌,但通常比较简单。 一旦理解了问题和需要改动的代码,实际的编码工作有时是微不足道的。
用手指在键盘上敲打一阵,直到屏幕上闪着正确的光芒, 搞定了,对吧?还没呢! 在你为之写测试并发送到代码评审之前,通常有些清理工作要做。
你将一些代码加入了游戏,但肯定不想下一个人被留下来的小问题绊倒。 除非改动很小,否则就还需要一些微调新代码的工作,使之无缝对接到程序的其他部分。 如果你做对了,那么下个编写代码的人无法察觉到哪些代码是新加入的,简而言之,编程的流程图看起来是这样的:
二、有什么代价?
解耦帮了什么忙
虽然并不明显,但我认为很多软件架构都是关于研究代码的阶段。 将代码载入到神经元太过缓慢,找些策略减少载入的总量是件很值得做的事。 这本书有整整一章是关于解耦模式, 还有很多设计模式是关于同样的主题。
可以用多种方式定义“解耦”,但我认为如果有两块代码是耦合的, 那就意味着无法只理解其中一个。 如果解耦了它们俩,就可以单独地理解某一块。 这当然很好,因为只有一块与问题相关, 只需将这一块加载到你的大脑中而不需要加载另外一块。
对我来说,这是软件架构的关键目标: 最小化在编写代码前需要了解的信息。
当然,也可以从后期阶段来看。 解耦的另一种定义是:当一块代码有改动时,不需要修改另一块代码。 肯定也得修改一些东西,但耦合程度越小,改动会波及的范围就越小。
有什么代价?
听起来很棒,对吧?解耦任何东西,然后就可以像风一样编码。 每个改动都只需修改一两个特定方法,你可以在代码库上行云流水地编写代码。
这就是抽象、模块化、设计模式和软件架构使人们激动不已的原因。 在架构优良的程序上工作是极佳的体验,每个人都希望能更有效率地工作。 好架构能造成生产力上巨大的不同。它的影响大得无以复加。
但是,天下没有免费的午餐。好的设计需要汗水和纪律。 每次做出改动或是实现特性,你都需要将它优雅的集成到程序的其他部分。 需要花费大量的努力去管理代码, 使得程序在开发过程中面对千百次变化仍能保持它的结构。
你得考虑程序的哪部分需要解耦,然后再引入抽象。 同样,你需要决定哪部分能支持扩展来应对未来的改动。
人们对这点变得狂热,他们设想,未来的开发者(或者他们自己)进入代码库, 发现它极为开放,功能强大,只需扩展,他们想要有“至尊代码应众求”。(译著:这里是“至尊魔戒御众戒”的梗,很遗憾翻译不出来)
但是,事情从这里开始变得棘手,每当你添加了抽象或者扩展支持,你就是在赌以后这里需要灵活性,你向游戏中添加的代码和复杂性是需要时间来开发、调试和维护的。
如果你赌对了,后来使用了这些代码,那么功夫不负有心人,但预测未来很难,模块化如果最终无益,那就有害,毕竟,你得处理更多的代码。
当你过分关注这点时,代码库就失控了,接口和抽象无处不在。插件系统,抽象基类,虚方法,还有各种各样的扩展点,它们遍地都是。
你要消耗无尽的时间回溯所有的脚手架,去找真正做事的代码。 当需要作出改动时,当然,有可能某个接口能帮上忙,但能不能找到就只能听天由命了。 理论上,解耦意味着在修改代码之前需要了解更少的代码, 但抽象层本身也会填满大脑。
像这样的代码库会使得人们反对软件架构,特别是设计模式,人们很容易沉浸在代码中,忽略了目标是要发布游戏。 对可扩展性的过分强调使得无数的开发者花费多年时间制作“引擎”, 却没有搞清楚做引擎是为了什么。
三、架构性能和速度
架构性能和速度
软件架构和抽象有时因损伤性能而被批评,而游戏开发尤甚。 让代码更灵活的许多模式依靠虚拟调度、 接口、 指针、 消息和其他机制, 它们都会加大运行时开销。
一个有趣的反面例子是C++中的模板。模板编程有时可以带来没有运行时开销的抽象接口。
这是灵活性的两极。 当写代码调用类中的具体方法时,你就是在写的时候指定类——硬编码了调用的是哪个类。 当使用虚方法或接口时,直到运行时才知道调用的类。这更加灵活但增加了运行时开销。
模板编程是在两极之间。在编译时初始化模板,决定调用哪些类。
还有一个原因,很多软件架构的目的是使程序更加灵活,作出改动需要更少的付出,编码时对程序有更少的假设。 使用接口可以让代码可与任何实现了接口的类交互,而不仅仅是现在写的类。 今天,你可以使用观察者和消息让游戏的两部分相互交流, 以后可以很容易地扩展为三个或四个部分相互交流。
但性能与假设相关,实现优化需要基于确定的限制,敌人永远不会超过256个?好,可以将敌人ID编码为一个字节。 只在这种类型上调用方法吗?好,可以做静态调度或内联。 所有实体都是同一类?太好了,可以使用 连续数组存储它们,但这并不意味着灵活性不好!它可以让我们快速改进游戏, 开发速度对创造更好的游戏体验来说是很重要的。 没有人能在纸面上构建一个平衡的游戏,哪怕是Will Wright。这需要迭代和实验。
尝试想法并查看效果的速度越快,能尝试的东西就越多,也就越可能找到有价值的东西。 就算找到正确的机制,你也需要足够的时间调试,一个微小的不平衡就有可能破坏整个游戏的乐趣。
这里没有普适的答案,要么在损失一点点性能的前提下,让你的程序更加灵活以便更快地做出原型; 要么就优化性能,损失一些灵活性。
就我个人经验而言,让有趣的游戏变得高效比让高效的游戏变有趣简单得多。 一种折中的办法是保持代码灵活直到确定设计,再去除抽象层来提高性能。
糟糕代码的优势
下一观点:不同的代码风格各有千秋。 这本书的大部分是关于保持干净可控的代码,所以我坚持应该用正确方式写代码,但糟糕的代码也有一定的优势。
编写架构良好的代码需要仔细地思考,这会消耗时间。 在项目的整个周期中保持良好的架构需要花费大量的努力,你需要像露营者处理营地一样小心处理代码库:总是让它比之前更好些。
当你要在项目上花费很久时间的时这是很好的,但就像早先提到的,游戏设计需要很多实验和探索,特别是在早期,写一些你知道将会扔掉的代码是很普遍的事情。
如果只想试试游戏的某些点子是否可行, 良好的架构就意味着在屏幕上看到和获取反馈之前要消耗很长时间。 如果最后证明这点子不对,那么删除代码时,那些让代码更优雅的工夫就付诸东流了。
原型——一坨勉强拼凑在一起,只能完成某个点子的简单代码——是个完全合理的编程实践。 虽然当你写一次性代码时,必须 保证将来可以扔掉它。 我见过很多次糟糕的经理人在玩这种把戏:
老板:“嗨,我有些想试试的点子。只要原型,不需要做得很好。你能多快搞定?”
开发者:“额,如果删掉这些部分,不测试,不写文档,允许很多的漏洞,那么几天能给你临时的代码文件。”
老板:“太好了。”
几天后
老板:“嘿,原型很棒,你能花上几个小时清理一下然后变为成品吗?”
你得让人们清楚,可抛弃的代码即使看上去能工作,也不能被维护,必须 重写。 如果有可能要维护这段代码,就得防御性地好好编写它。
四、坏代码中的好代码
坏代码中的好代码
作为有几年工作经验的程序员,都会对 bad code 不满意。如何将烂代码变成好代码,本文将由浅入深、一步步带你理解重构的奥秘,让你对重构有个基本的了解。本文基于文章《The Simple Ways to Refactor Terrible Code》编译整理而成。
任何一个有几年工作经验的程序员都经历过这样的场景:回顾早期写的代码,会陷入深深的怀疑,这么烂的代码是我写的吗?相比起刚入行时候的你,这几年不管是自己发奋学习,如阅读《编程模式》、《重构:改善现有代码的设计》等圣经级著作,还是公司大神对你耳提面命,你对程序架构、编码规范的认识都有了很大的提升。还有一种情况是做项目的时候,或多或少存在赶工期的问题,很可能为了能在 deadline 之前交付,某个问题的解决方案并不够优雅。
以每一个有责任心(代码洁癖)的程序员都会去考虑重构的问题,重构代码有很多好处,正如作者在文章中提到的:后期修正 bug 更加容易、提高程序的可读性、改进程序原有设计,更重要的是当你在小组讨论会上,向同事展示代码时不会觉得丢人。以上这些好处是彼此联系的,比如当你接手一个遗留工程,前开发人员早已不知去向,不管是要增加功能还是修正 bug,你都需要读懂代码,这就需要提高程序的可读性,你能依靠的除了你堪比福尔摩斯的推理能力,就只有重构这把杀猪刀了。
虽然重构有这么多好处,为什么当我们准备开始的时候,却会反复纠结?作者大致提到以下原因:
1、担心破坏已有代码。这种情况在核心业务系统尤为普遍,比如电商平台,企业的 ERP 平台等,系统需要 7*24 运行,你的一个修改可能导致 10000 元的商品被 1 元钱买走了。作为普通人的我们,自然会抱着多一事不如少一事的心理,毕竟出了问题,会吃不了兜着走。
2、不能立即看到产出。在每日的 stand-up 会议中,当研发经理问我,你今天干什么了,我说重构代码,如果连续三天都是这个答案,估计研发经理就要发飙了。
3、没有时间去做。面对繁重的研发任务和日益逼近的 deadline,重构,真的不是一场说走就走的旅行。
说了这么多,读者朋友可能会有一个想法,是否有一些方法,能让我享受重构的好处,又能避免上面提到的风险。
幸运的是还真有!
下面我将从最简单、基本不会破坏已有代码、花费很少时间的重构方法入手,逐步深入,让大家对重构有一个基本了解,在对方法的介绍中,我将按照《InfoQ 编程语言 2 月排行榜结果出炉》中的调研情况,选取用户掌握最多的编程语言 Java,以及该语言使用最多的 IDE 环境 Eclipse,进行举例。
重构入门
为了消除恐惧,让我们从最简单的重构方法入手
1. 格式化代码
当你发现代码缩进层次不齐,代码块中缺少{}等问题时,就需要考虑代码格式化了,现在的 IDE 工具已经对格式化提供了很好的支持,以 eclipse 为例,选中要格式化的代码,点击以下菜单项就能完成代码格式化。
此外很多源代码管理网站,也提供了格式化工具,如图所示:
在团队开发中,为了保证开发代码样式统一,需要建立编码规范。我们并不需要重头建立编码规范,可以在大厂的编码规范基础上进行定制,比如在 Java 领域可采用阿里、华为、Google Java Style Guide 等编码规范。该编码规范可以与 IDE 进行结合,如在 eclipse 中,打开 Window->Preferences->Java->Code Style 导入编码规范:
重要的是,不管选择何种规范,要坚持下去,并让每个团队成员都用起来。
2. 注释
在代码开发中,好的注释可以提高程序的可读性,坏的注释可能会画蛇添足,甚至起反作用。作者提到好的注释要做到和代码相关、及时更新。很多时候,代码刚开始编写时,注释和代码是一致,后期因为间隔时间过长或其他人接手修改代码,没有对注释及时修改,就会造成注释和代码渐行渐远。
尽量减少不必要的注释,如很多函数或者类,如果设计架构清晰,通过命名就能知道他们做什么,注释不是必须的。还有一种情况是暂时不用的代码,很多人会觉得以后会用到,会加个注释,作者给出的建议是删掉它,如果你将来真的用到了,可以到 git(一种代码管理工具)的历史记录中查找。
对于逻辑混乱的代码,如在循环中随意使用 break,复杂的 if 语句嵌套等,你要做的是理清逻辑,重构代码,而不是让注释替你补锅。正如《重构》中提到的 “当你感觉需要撰写注释,请先尝试重构,试着让所有注释都变得多余。”
3. 废弃的代码
随着系统版本的不断迭代,有一些函数或类不再使用后,我们应该将它及时删除,否则随着时间流逝,会造成代码库臃肿,程序可读性变差。而且如果还发生人员的变动,慢慢会成为谁也不敢动的代码,因为都不知道有啥用和在哪用到。
多先进的 IDE 工具都对查找代码的调用提供了支持,以 eclipse 为例,查找函数是否被调用,可以使用调用层次图功能,或者直接使用高级搜索功能,如图所示:
在调用层次图(Call Hierarchy)中可以看到 getInstance() 函数被什么地方调用。
如果使用类似于 spring 的自动装配功能,在 xml 中定义了调用关系,可以使用高级搜索功能查看 xml 或 properties 文件中定义的同名函数进行筛选。
4. 变量命名
就像我们人一样,一个好名字对变量、常量、函数和类都很重要,一个好的名字会让其他开发人员很容易明白其功能是什么。以下是命名的一些注意事项:
1、类和文件名使用名词,但这个名词要有意义,比如 Data、Information 就意义不明显,不是好名字。
2、函数使用动词或短语命名,比如 isReady hasName。
3、长名字 vs 无意义名字:在长名字和无意义名字中选择时,请选择长且有意义的名字,比如 java 语言中使用最广的类库 spring 中的一个命名是:SimpleBeanFactoryAwareAspectInstanceFactory。
4、命名法则:常见的有驼峰命名法(camelCase)和蛇形命名法(snake_case), 比如文件名使用蛇形是 file_name,驼峰式 fileName。选择一种,所有的命名都按照这个规则,并将其作为编码规范的一部分,让团队成员都要遵守。
如果你要对已有代码中错误的命名方式进行修改,eclipse 提供了很好地支持:选择要修改的类、函数或变量,选择 Refactor——》Rename 可以同时修改该变量在声明和使用处的名称,如下图所示:
5. 常量命名
常量的命名除了要遵守上一小节提到的通用方法外,还有一类魔法数字(magical numbers)的情况,如使用 0,1 来代表男女。更恰当的做法是定义常量名来代替魔法数字,如在 Java 中:
final int static FEMALE=0,MALE=1;
6. 负值条件的重构
在条件或循环语句中,使用负值条件,会让代码难以理解、容易出错,比如判断是否为男性,条件写成了 “!
isNotFemale(gender)”
重构方法是将条件改成正值,并调换 if/else 语句代码块的顺序。
7. {} 作为单独的一行
正确的{}格式已经在 1 部分中提到,这里再强调下,如果你没有将括号作为单独的一行,如下所示:
catch (IOException e) { e.printstackTrace(); }
你得到的好处只是减少了一行代码,但是当你设置断点调试时,断点将不能精确定位到你想调试的部分。
8. 变量定义和使用距离太远
变量的定义和使用不要离得太远,一般不要超过 20 行,函数也类似。如果你意识到这个问题,但并不能缩短定义和使用的距离,那代表这是个大函数(big function),你需要对函数做拆分。
以上是对入门级重构方法的介绍,在进行重构时,最重要的规则是:每次只做微小修改,并保证测试能正确运行(小步快跑)。
重构进阶
现在我们对重构已经有了基本的了解,并建立了初步的信心。让我们下面关注一些稍微复杂的重构内容。
1. 重复代码
当你发现相同的代码块在三个地方都出现时,你就需要考虑重构代码了。对于同一个类中重复的代码块,可使用提取方法(extract method:将重复代码提取出单独的函数)来完成;对于一组相关类如父类、子类 A、子类 B 中的重复函数,通过上移方法(pull method:将子类中的方法移入父类中)和模板方法(template method:父类方法定义模板,子类编写不同实现)来完成。
Eclipse 提供了相关功能,如图所示:
2. 函数参数
开关参数的滥用(boolean parameters):函数的形参中有一个是 boolean 类型,函数体根据该参数为 true 或者 false 执行不同的代码块。这种方式会导致重构的另一个坏味道——大函数(big function)的形成,从而增加代码的复杂性。重构方法是:去掉这个开关参数,将函数拆分成两个函数。
在函数内修改了参数:参数用于外界向函数体传递信息,好的做法是参数对于函数是只读的。如果在函数内修改参数,会造成函数功能难以理解,如果函数内多次修改参数,这个函数会变成一座迷宫,重构方法是:将参数赋值给局部变量,对局部变量修改,如下代码所示:
原始的:
int fun(int val) { val=32; }
修改后:
int fun(int val) { int temp=val; temp=32; }
参数太多:函数的参数最多有三个是合理的,超过三个就需要提高警惕了。重构方法是:根据逻辑拆分函数;引入参数对象(parameter object:构造参数类,将原来传递的参数作为类的属性,调用方传入该类的一个对象)
3. 变量多余
当定义的变量没太多含义,而且没有赋值操作,如下代码:
double basePrice = anOrder.basePrice(); return (basePrice > 1000);
其中的 basePrice 完全是多余的变量,完全可以用函数本身来替代它,如下代码:
return (anOrder.basePrice() > 1000);
4. 缺少变量
某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。如果它们被赋值超过一次,就意味着它们在函数中承担了一个以上的职责。如果临时变量承担多个责任,它就应该被替换为多个临时变量,每个变量只承担一个责任。
重构方法:针对每次赋值,创造一个独立、对应的临时变量
5. 复杂条件
我们都见过由 && || 构成的复杂的多行条件。复杂条件可读性很差,调试和修改也很麻烦。
一个简单的重构方式是:将这块代码抽取出来,变成一个单独的判断函数,如下代码:
double fetchSalary(double money, int day)
{
if(money>10000 && day>30 ) { return money``day/365``0.2; }else { return money``30.0/365``0.1; }
}
其中的条件 money>10000 && day>30 可重构为:
boolean isHigherSalary(double money,int day) { return (money>10000 && day>30 ); }
老旧代码的重构
在进行代码重构时,需要考虑测试代码是否能覆盖重构的功能,如果没有,需要增加测试用例覆盖所做的修改,否则重构可能会破坏已有的功能。在前面的章节,作者假设已有足够的测试用例,并且重构完成后测试可以正确运行。
但是如何重构测试用例没有完全覆盖的代码呢,如老旧代码?作者的建议是只做必要的重构,如当需要修正 bug 或者增加新的功能,这种情况下,先为遗留代码编写测试用例,在理解的基础上重构代码,为代码修改做好准备,然后进行代码修改。
从这点上来说,你可以进行任何类型代码的重构:一次只做一步重构,从小的容易的重构做起,并频繁测试。
利用工具
重构代码需要花费时间,当项目工期很紧时,很难下定决心去做重构。为了让重构变得更容易,市面上提供了大量相关工具,如 pylint( Python 代码分析工具)、Checkstyle(代码规范工具)、Sonarqube(代码质量管理的开源工具)
此外,你要保证你的测试用例跑的足够快,否则你会没有耐心等待测试运行结果,或者直接就不运行了。
理想情况下,程序在构建后部署到测试环境前,可以借助 CI/CD(持续集成 / 持续部署)工具实现代码质量检查、代码样式检查、潜在 bug 监测等模块的自动化运行。
五、软件架构的寻求平衡
软件架构的寻求平衡
如果没有所谓的“Deadline”(最后期限),我们就不用担心架构设计的问题,因为我们有足够的时间去研究去学习找到最优的架构设计方案。然而,做梦是可以有的。那怎么寻找?相信大家都各有各的看法,基本分为这几派:
梭哈派,不要给我说什么架构设计、设计思维!老夫架构设计就是敲代码,边敲边设计,自然而然代码就是架构,想那么多还不如直接敲。
借鉴派,善于借鉴业界成熟项目的标准,都按成熟标准来就好。
灵活派,根据实际项目需求来做架构设计,最好看现场情况来判断。
文档派,都得写文档,写完整了文档,就能预防后续的问题和风险。
肯定不止上面的介绍的对于架构设计的看法,相信大家都有自己认同的看法,欢迎评论区留下“最强流派”。
那架构设计的最佳平衡点肯定会有架构大师做了研究的,下面将从专业的研究分析下。
架构设计对总工期的影响
参考Barry Boehm《Architecting: How Much and When?》书中项目工期的构成:开发、架构设计、返工(处理技术债:打补丁、改BUG、重构、重写)(见图2)。
可见,Boehm证明了随着架构设计时间的增加,开发和返工量都会减少。书中还详细地研究了系统规模影响最佳构架设计的平衡点,鉴于国内外的项目情况不同,本文就不作为参考了。但从中还是可以得出以下个人认为比较客观的结论:
1、游戏系统越大,前期做架构设计的获益越大,反之越小。
2、一千万行代码以上的大项目在总工期上架构设计时间占3到4成较好,不超过一万行代码的小项目则不超过1成。
3、前期架构设计做得不够,后期做好返工的心理准备。
架构设计的时间决定
上节内容用系统规模来评估架构设计的工作量似乎很符合“灵活派”的做法,因为可以根据项目需求很容易确定系统的规模。相信肯定有人会问:那复杂度不要考虑吗?大型系统可能很复杂,但有“借鉴派”的成熟解决方案就不必做太多架构设计。然而,也并非所有复杂度高的系统都很庞大。
因此,评估前期架构设计的时间,不能只按标准、经验、代码量、复杂度来决定。应该是按照风险来驱动架构设计(下篇文章再详细讲解),当然这也是Boehm做的研究(见《Using Risk to Balance Agile and Plan-Driven Methods》)。
六、软件架构的简单性
软件架构的简单性
在游戏开发过程中,有些因素在相互角力:
1、为了在项目的整个生命周期保持其可读性,需要好的架构
2、需要更好的运行时性能
3、需要让现在想要的特性更快地实现
这些目标至少是部分对立的,好的架构长期来看提高了生产力, 也意味着每个改动都需要消耗更多努力保持代码整洁。
草就的代码很少是运行时最快的,相反,提升性能需要很多的开发时间,一旦完成,它就会污染代码库:高度优化的代码不灵活,很难改动。
总有今日事今日毕的压力,但是如果尽可能快地实现特性, 代码库就会充满黑魔法,漏洞和混乱,阻碍未来的产出。
没有简单的答案,只有权衡,从我收到的邮件看,这伤了很多人的心,特别是那些只是想做个游戏的人。 这似乎是在恐吓,“没有正确的答案,只有不同的错误。”
但对我而言,这让人兴奋!看看任何人们从事的领域, 你总能发现某些相互抵触的限制。无论如何,如果有简单的答案,每个人都会那么做。 一周就能掌握的领域是很无聊的。你从来没有听说过有人讨论挖坑。
也许你会讨论挖坑,我没有深究这个类比。 可能有挖坑热爱者,挖坑规范,以及一整套亚文化。 我算什么人,能在此大放厥词?
对我来说,这和游戏有很多相似之处。 国际象棋之类的游戏永远不能被掌握,因为每个棋子都很完美地与其他棋子相平衡。 这意味你可以花费一生探索广阔的可选策略。糟糕的游戏就像井字棋,玩上几遍就会厌倦地退出。
简单
最近,我感觉如果有什么能简化这些限制,那就是简单。 在我现在的代码中,我努力去写最简单,最直接的解决方案。 你读过这种代码后,完全理解了它在做什么,想不到其他完成的方法。
我的目标是正确获得数据结构和算法(大致是这样的先后),然后再从那里开始,我发现如果能让事物变得简单,最终的代码就更少, 就意味着改动时有更少的代码载入脑海。
它通常跑的很快,因为没什么开销,也没什么代码需要执行。 (虽然大部分时候事实并非如此。你可以在一小段代码里加入大量的循环和递归。)
但是,注意我并没有说简单的代码需要更少的时间编写。 你会这么觉得是因为最终得到了更少的代码,但是好的解决方案不是往代码中注水,而是蒸干代码。
Blaise Pascal有句著名的信件结尾,“我没时间写得更短。”
另一句名言来自Antoine de Saint-Exupery:“臻于完美之时,不是加无可加,而是减无可减。”
言归正传,我发现每次重写本书,它就变得更短,有些章节比刚完成时短了20%。
我们很少遇到优雅表达的问题,一般反而是一堆用况。 你想要X在Z情况下做Y,在A情况下做W,诸如此类。换言之,一长列不同行为。
最节约心血的方法是为每段用况编写一段代码,看看新手程序员,他们经常这么干:为每种情况编写条件逻辑。
但这一点也不优雅,那种风格的代码遇到一点点没想到的输入就会崩溃。 当我们想象优雅的代码时,想的是通用的那一个: 只需要很少的逻辑就可以覆盖整个用况。
找到这样的方法有点像模式识别或者解决谜题,需要努力去识别散乱的用例下隐藏的规律,完成时你会感觉好得不能再好。
七、准备出发
准备出发
几乎每个人都会跳过介绍章节,所以祝贺你看到这里,我没有太多东西回报你的耐心,但还有些建议给你,希望对你有用:
1、抽象和解耦会让代码的扩展性和灵活性更加强,但会花费额外的实现时间
除非你觉得这样的灵活性有必要,否则没必要过度的去追求。
2、性能优化很重要,但是要注意时机
在整个开发周期中,最好先专注于实现基本需求,把那些可能限制到项目进度的性能优化尽量延后。
3、在整个开发周期中,灵活性和高性能往往不能兼得
我们可以保持代码的灵活性直到设计定下来,再抽出抽象层来提高性能。
4、在原型开发阶段,能尽快让你做出原型产品,最终让产品成功上线的最初的功臣,或许就是设计糟糕的烂代码
因为他们实现想法够快,不需要缜密的设计与架构。只是这些烂代码在经历了原型设计阶段之后,一定要被重写或者重构。
5、如果打算抛弃这段代码,就不要尝试将其写完美
“摇滚明星将旅店房间弄得一团糟,因为他们知道明天会有人来打扫干净。”
6、提倡去写出最简单,最直接的整洁代码
你读过这种代码后,完全理解了它在做什么,想不出其他完成的方法。“完美是可达到的,不是没有东西可以添加的时候,而是没有东西可以删除的时候。”
7、但最重要的是,如果你想要做出让人享受的东西,那就享受做它的过程