URL
status
type
date
slug
summary
tag
category
password
icon
这本书是关于代码的组织方式。这里少谈代码,多谈代码组织。
软件架构
“只管写你的代码,架构会为你收拾一切。”
好的设计意味着当我做出一个改动时,就好像整个程序都在期待它一样。我可以调用少量可选的函数来完美地解决一个问题,而不会为软件带来副作用。
怎么调整架构
顺序为:检查代码 → 针对新问题提出解决方案 → 编写测试 → 组织新增代码 → 无缝集成
检查代码
在你打开编辑器添加新功能,修复bug或者由于其他原因要修改代码之前,你必须要明白现有的代码在做什么。当然,你不必知道整个程序,但是你需要将所有相关的代码加载到你的大脑中。
我们倾向于略过这一步,但它往往是编程中最耗时的部分。如果你认为从磁盘加载一些数据到RAM很慢的话,试着通过视觉神经将这些数据加载到你的大脑里。
肉眼检查代替编译 = 如果你认为从磁盘加载一些数据到RAM很慢的话,试着通过视觉神经将这些数据加载到你的大脑里,这是什么黑色幽默?
提出解决方案
一旦你的大脑有了一个全面正确的认识,则只需稍微思考一下就能提出解决方案。这观点值得反复斟酌,但通常这是比较明确的。一旦你理解了这个问题和它涉及的代码,则实际的编码有时是微不足道的。
编写测试
为一些游戏代码编写单元测试比较难,但是大部分代码是可以完全测试的。
你的手指游走于键盘间,直到右侧的彩色灯光在屏幕上闪烁时,你就大功告成了,是吗?还没有!在你编写测试,并将它发送给代码审查之前,你通常有一些清理工作要做。
重新组织
你在游戏中加入了一些代码,但是你不想后面处理代码的人花大量时间理解或修改你的代码。除非变动很小,通常都会做些重新组织工作来让你新加的代码无缝集成到程序中。如果你做得很好,那么下一个人在添加代码的时候就不会察觉到你的代码变动。
现在想想,流程图的环路中没有出口有点小惊悚。
解耦
虽然不是很明显,但我认为很多软件架构师还处于学习阶段。将代码加载到脑中如此痛苦缓慢,得自己寻找策略来减少装载代码的体积。这本书有一整章(第5章)是关于解耦模式的,许多的设计模式也有相同的思想。
好处
你可以用一堆方式来定义“解耦”,但我认为如果两块代码耦合,意味着你必须同时了解这两块代码。如果你让它们解耦,那么你只需了解其一。这很棒,因为如果只有一块代码和你的问题相关,则你只需要将这块代码装载到你的脑袋中,而不用把另外一块也装载进去。
对一切进行解耦,你就可以迅速编写代码。每一次变化意味着只会涉及某一个或两个方法,然后你就可以在代码库上行云流水地编写代码。
这种感觉正是为什么人们会为抽象、模块化、设计模式和软件架构感到兴奋的原因。一个架构良好的程序工作起来真的会令人愉悦,每个人都会更加高效。良好的架构在生产力上会产生巨大的差异。怎么夸大它带来的效果是如何深远都不为过。
对我来说,这是软件架构的一个关键目标:在你前进前,最小化你脑海中的知识储存量。
坏处
低耦合限制
当然,对解耦的另一个定义就是当改变了一块代码时不必更改另外一块代码。很明显,我们需要更改一些东西,但是耦合得越低,更改所波及的范围就会越小。
架构维护成本
(维护你的设计)需要特别注意。我曾见过许多程序在开始时写得很漂亮,但死于一个又一个“一个小补丁而已”。就像园艺一样,只种植是不够的。你必须要除草、修剪。
天下没有免费的午餐。良好的架构需要很大的努力及一系列准则。每当你做出一个改变或者实现一个功能时,你必须很优雅地将它们融入到程序的其余部分。你必须非常谨慎地组织代码并保证其在开发周期中经过数以千计的小变化之后仍然具有良好的组织性。
你必须要考虑程序的哪一部分应该要解耦然后在这些地方引入抽象。同样地,你要确定在哪里做一些扩展以便将来很容易应对变化。
人们对此非常兴奋。他们设想着,未来的开发者(或者是他们自己)进入代码库,发现代码库开放、强大,只等着被加些扩展。他们想象一个游戏引擎便可统治一切。
但是,事情就在这里开始变得棘手。当你添加了一个抽象层或者支持可扩展的地方,你猜想到你以后会需要这种灵活性,于是你便为你的游戏增加了代码和复杂性,这需要时间来开发,调试和维护。
新旧更替,进退维谷
有人杜撰了“YAGNI”一词(You aren’t gonna need it 你不需要它)作为口头禅,用它来与猜测未来的自己会想要什么这种冲动进行斗争。
如果你猜对了,那么你之前的辛苦就没白费,而且也无须再对代码进行任何修改。但是猜测未来是很难的,并且当模块最终没起到作用时,很快它就变得有害。毕竟,你必须处理这些多出来的代码。
当你过度关注这点时,便会得到一个架构已经失控的代码库。你会看到接口和抽象无处不在。插件系统、抽象基类、虚方法众多,还有各种的扩展点。
抽象作矛,解耦为盾
你将花费大量时间去找到有实际功能的代码。当你需要做出改变时,当然有可能有接口能帮上忙,但你会很难找到它。从理论上讲,解耦意味着在你进行扩展时仅需理解少量代码,然而抽象却增加了理解代码的难度。
像这样的代码库正是让人们反对软件架构尤其是设计模式的原因。对代码进行包装很容易,以至于让你忽视了你要推出一款游戏的事实。一味地追求可扩展性让无数开发者在一个“引擎”上花费数年却没有搞清楚引擎究竟是用来做什么的。
但是从长期的影响来说,带来的好处远大于坏处的,因此模块化仍然是最佳的架构选择。
性能和速度
你有时候会听到关于软件架构和相关概念的批评声,尤其在游戏开发中:它会影响到游戏的性能。许多模式让你的代码更加灵活,但是它依赖于虚函数派发、接口、指针、消息以及其他至少有一些运行成本的机制。
模版元编程
一个有趣的范例是C++模板。模板元编程有时可以让你获得抽象接口而没有任何运行时开销。
1.
在某些类中调用一个具体方法时,你相当于将这个类固定(很难做出改变)。
2.
使用一个虚方法或者接口时,被调用的类将直到真正运行起来才能被追踪到,这样的程序更具灵活性但是会增加额外的运行成本。
模板元编程介于两者之间。在模板元编程中,在编译期间你就能决定在模板实例化时调用哪个类。
灵活→抽象分析,优化→具体分析
很多软件架构的目标是使你的程序更加灵活,这样只需较少的代价便可对代码进行改变,这也意味着在程序中更少的编码。你使用接口,以便代码可以与任何实现这些接口的类进行工作,而不是使用具体类。你使用观察者模式(第4章)和通信模式(第15章)使得游戏的两部分互相沟通,而将来它们自身就会成为另外两个需要沟通的部分。
但是性能优化总是在某些假设下进行的。优化的方法在特定的条件下进行更好,比如下面的例子。
1.我们能肯定地假设永远不会有超过256个敌人吗?好极了,我们可以将ID打包成一个单字节。
2.在这里我们只会在一个具体类型上调用方法吗?好,我们就静态调度或者对它内联。
3.所有的实体都是同一个类吗?太好了,我们可以将它们做成一个很棒的连续排列(第17章)。
灵活与性能的取舍
这并不意味着它的灵活性很差!它可以让我们快速地进行游戏更新,开发速度是让游戏变得有趣的关键性因素。没有人,哪怕是Will Wright,可以在纸上设计出一个平衡的游戏。这需要迭代和实验。
你越快地对想法付诸实践并观察效果,你就能越多地尝试并越有可能找到一些很棒的东西。即便在你已经找到合适的技术之后,你也要用充足的时间来进行调整。一个细小的不平衡就会破坏掉游戏的乐趣。
这里没有简单的答案。将你的程序做得更具有灵活性,以便能够更快速地进行原型编写,但这会带来一些性能损失。同样地,对你的代码进行优化会降低它的灵活性。
根据我的经验,将一款有趣的游戏做得高效要比将一款高性能的游戏做的有趣更简单些。一种折中的办法是保持代码的灵活性,直到设计稳定下来,然后去除一些抽象,以提高游戏的性能。
从原型到落地
编写架构良好的代码需要花时间精雕细琢,但是在这之前,如果只是想验证一些游戏想法是否能够正确工作,那么对其精心设计架构就意味着在想法真正显示到屏幕并得到反馈之前需要花费更多时间。如果它最终没有工作,那么当你删除代码时,花费在编写优雅代码上的时间其实都浪费掉了。
原型(把那些仅仅在功能上满足一个设计问题的代码融合在一起)是一个完全正确的编程实践。然而,特别提醒下,如果你编写一次性的代码,那么你必须要确保能将之扔掉再扔,否则就要重复造轮子了。我不止一次看到一些糟糕的经理重演以下场景。
老板:“嘿,我们已经有想法了,准备尝试下。只是一个原型,所以不必感觉必须要做得正确。大概多久能实现?”开发:“嗯,如果我简化很多,不测试,不写文档,不管bug,我几天内就可以给你一些临时的代码。”老板:“太好了!”几天后……老板:“嘿,原型写得很不错。你能花几个小时清理下代码然后开始真枪实弹的干么?”
你需要确保这些使用一次性代码的人们明白这种一次性代码看起来能够运行,但是它却不可维护,必须被重写。如果可能,最终你也许会保留它们,但需要后续修改得特别好,否则架构就要乱了。
开发中的平衡
1.好的程序架构-长期开发速度
2.好的运行性能-游戏的执行速度
3.完成今日任务-短期开发速度
这些目标至少部分是相冲突的。好的架构从长远来看,改进了生产力,但维护一个良好的架构就意味着每一个变化都需要更多的努力来保持代码的干净。
最快编写的代码实现却很少是运行最快的。相反,优化需要消耗工程时间。一旦完成,也会使代码库僵化:高度优化过的代码缺乏灵活性,很难改变。
完成今日的工作并担心明天的一切总伴随着压力。但是,如果我们尽可能快的完成功能,我们的代码库就会充满了补丁、bug和不一致的混乱,会一点点地消磨掉我们未来的生产力。
这里没有简单的答案,只有权衡。从我收到的电子邮件中,看得出来,这让很多人头疼。特别是对于想做一个游戏的新手们来说,听到这样说挺恐吓人的,“没有正确答案,只是错误口味不同”。
但是,对于我而言,这令人兴奋!看看人们从事致力的领域,在这中心,你总能找到一组相互交织的约束。毕竟,如果有一个简单的答案,每个人都会这么做。在一周内便可掌握的领域最终是无聊的。你不会接触到在别人的杰出职业生涯中所挖掘出的东西。
这和游戏本身有很多共同点。就像国际象棋永远无法掌握,因为它是如此完美的平衡。这意味着你可以穷尽一生来探索可行的战略空间。设计不当的游戏如果用一个稳赢的战术一遍遍玩,会让你厌倦并退出。
代码精炼
原则
努力地尝试着编写最干净、最直接的函数来解决问题。这种代码在你阅读之后,就会明白它究竟做了什么,并且不敢想象还有其他可能的解决方案。
我致力于保持数据结构和算法的正确性(在这个顺序下),然后继续往下做。我觉得如果我能保持简单性,代码量就会变少。这意味着更改代码时,我的脑袋里只需装载更少的代码。
它通常运行速度快,因为根本就没有那么多的开销,也没有太多的代码要执行(这当然并非总是如此,你可以在小部分代码中进行很多的循环和递归)。
Blaise Pascal用了一句名言作为了一封信的结尾:“我会写一封更简短的信,但我没有足够的时间。”另一种引用来自Antoine de Saint- Exupery:“极臻完美,并非无以复加,而是简无可减。”
简单不是代码量少
但是,请注意,我并不是说简单的代码会花费较少的时间来编写。你会觉得最终的总代码量更少了,但是一个好的解决方案并不是更少的实际代码量,而是对代码的升华。
少些用例,多些逻辑
我们很少会遇到一个非常复杂的问题,用例反而有一大堆,例如,你想让X在Z的情况下执行Y而在A的情况下执行W,以此类推。换句话说,是一个不同实例行为的长列表。
最省脑力的方法就是只编写一次测试用例。看一下新手程序员,这是他们经常做的:为每个需要记住的用例构建大量的条件逻辑。
在那里面毫无优雅性,当程序有输入或者编码者稍微考虑得跟用例有些不一样时,这种风格的代码就最终会沦陷。当我们考虑优雅的解决方案时,浮现脑海中的就有一个:一小块逻辑就能正确地处理一大片用例。
你会发现这有点像模式匹配或解谜。它需要努力识破测试用例的分散点,以找到它们背后隐藏的秩序。当你把它解决时,会感觉很棒。
最后的建议
- 抽象和解耦能够使得你的程序开发变得更快和更简单。但不要浪费时间来做这件事,除非你确信存在问题的代码需要这种灵活性。
- 在你的开发周期中要对性能进行思考和设计,但是要推迟那些降低灵活性的、底层的、详尽的优化,能晚则晚。
- 尽快地探索你的游戏的设计空间,但是不要走得太快留下一个烂摊子给自己。毕竟你将不得不面对它。
- 如果你将要删除代码,那么不要浪费时间将它整理得很整洁。摇滚明星把酒店房间弄得很乱是因为他们知道第二天就要结账走人。
- 但是,最重要的是,若要做一些有趣的玩意,那就乐在其中地做吧。
- 相信我,在游戏发布前的两个月并不是你开始担心“游戏的FPS只有1帧”问题的时候。
- 作者:Cloud
- 链接:https://cloud09.xyz/article/c3bb2c7e-758f-4517-97cd-b14107c99490
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。