正文
void
SetTo
(
Mesh
*
mesh
);
}
我还了解到,注释也被看作为好代码的标志,这导致我写出这样的「瑰宝」:
D3DXVECTOR3 GetCenter();
// Gets the center of the mesh
这个类还存在更严重的问题。Mesh 的概念是一个令人困惑的抽象,在现实世界没有参照物。尽管是我写出来的,但我也对它感到困惑。这是一个容纳顶点、索引和其他数据的容器吗?这是一个用于从磁盘加载和卸载数据的资源管理器吗?这是一个将数据发送到 GPU 的渲染器吗?它代表了所有这些东西。
如何改进
Mesh 类应该是一个「普通的旧数据结构」。它应该没有「智能」,这意味着我们可以安全地将所有无用的 getters 和 setters 丢弃,并将所有的字段都设为 public 属性。
然后,我们可以将资源管理和渲染分离为独立于惰性数据的系统。是的,是系统,而不是对象。当另一种抽象更合适时,就没必要将每个问题都转化为面向对象的抽象。
关于注释问题的修改,大多数时候,删除就可以了。由于注释不受编译器检查,容易过时,这是造成误导的主要因素。我认为不应该对代码进行注释,除非他们属于以下情况:
-
注释解释的是
why
,而不是
what
。这些注释是最有用的。
-
用几句话来解释下面的大块代码是什么。这些注释有助于指导和阅读代码。
-
对声明的数据结构进行注释,说明每个字段的意义。这些注释往往是不必要的。但有时,字段与内存中的概念的映射关系不能够直观显示,就有必要通过添加注释来描述这种映射关系。
2007-2008 年
这段时间,是我的「PHP 黑暗岁月」。
2009-2010 年
此时,我正在上大学。我做了一个基于 Python 的第三人称多人射击游戏《 Acquire、Attack、 Asplode、 Pwn》(简称 A3P)。关于此项目,我没有任何理由为自己辩解。
形势真的越来越尴尬了,这个项目带了一个侵犯健康权益的背景音乐。
(注:这个视频搬运失败,请看这里:https://youtu.be/qdt2ixQSjZo )
当我写这个游戏的时候,新学到的经验是全局变量被认为是糟糕代码的标志。全局变量提高了代码的耦合度。它们允许 A 函数通过修改全局变量进入完全不相关的 B 函数。 全局变量无法跨线程使用。
然而,几乎所有的游戏代码都需要访问整个 world 状态。我通过将所有内容存储在「world」对象中,并将「world」传递到每个单独的函数中来「解决」这个问题。 再也没有全局变量了!我认为这是「出色的实现」,因为理论上我可以同时运行多个、独立的「world」。
在实践中,「world」作为一个事实上的全局状态容器。多个「worlds」的想法当然是不需要的,也没有经过测试,但我相信,如果没有进行重大的重构,这也不会有效。
一旦你加入了清理全局变量的奇特「宗教团体」,你会找到很多有创意方法用以欺骗自己。最糟糕的莫过于单例:
class
Thing
{
static
Thing
i
=
null
;
public
static
Thing Instance
()
{
if
(
i
==
null
)
i
=
new
Thing
();
return
i
;
}
}
哇,魔术啊! 看不到一个全局变量!然而,单例比全局变量更糟糕,原因如下:
-
全局变量的所有潜在缺陷仍存在于单例中。 如果你认为单例不是一个全局变量,你只不过是在自欺欺人罢了。
-
在最好的情况下,访问单例只是给你的程序增加了昂贵的分支指令。 在最坏的情况下,这将会是一个完整的函数调用。
-
你不知道一个单例会在什么时候被初始化,直到该程序被真正地运行。这是程序员简单地将本该在设计时应该做出的决策留给程序自己去处理的另一个例子。
如何改进
如果某个变量必须要全局化,就让它全局化好了。在定义全局变量时,请结合整个项目进行考虑。有些经验可以借鉴。
真正的问题在于代码之间相互依赖。全局变量,容易使不相关的代码之间创建不可见的依赖关系。组合相互依赖的代码,并入到内聚的系统中,以最小化这些不可见的依赖关系。实现它的一个好方法,就是将与系统相关的所有内容都放到该系统自己的线程中,并强制其它的代码通过消息传递与该系统通信。
布尔参数
你可能写过像这样的代码:
class
ObjectEntity
:
def
delete
(
self
,
killed
,
local
)
:
# ...
if
killed
:
# ...
if
local
:
# ...
在这里,我们有四个不同的但又极度相似的「删除」操作,它们的差异仅仅在于两个布尔参数。看起来似乎完全合理。现在,让我们来看看调用这个函数的客户端代码:
obj.delete(True, False)
可读性很差,不是吗?
如何改进
这是个案。然而,Casey Muratori 提供的一条建议适用于此:
先写客户端代码
。我敢肯定,任何一个有理智的人,都不会写出上面这种客户端代码。 相反地,你可能会这样写:
obj.killLocal()
然后
,写出
killLocal() 函数的实现代码。
命名
对命名如此多地关注,可能看起来很奇怪。但就像老笑话一样,这是计算机科学中尚未解决的两个问题之一。另一个是缓存失效和差一 错误(off-by-one errors)。
看一下这些函数:
class
TeamEntityController
(
Controller
)
:
def
buildSpawnPacket
(
self
)
:
# ...
def
readSpawnPacket
(
self
)
:
# ...
def
serverUpdate
(
self
)
:
# ...
def
clientUpdate
(
self
)
:
# ...
显然,前两个函数是相互关联的,最后两个函数也是相关的。但是它们没有通过命名来反映这个事实。 在 IDE 中,这些功能将不会在自动完成菜单项中相邻显示。
一种更好地命名方式是,以相同的方式开始,并以不同的方式结束。如下所示:
class
TeamEntityController
(
Controller
)
:
def
packetSpawnBuild
(
self
)
:
# ...
def
packetSpawnRead
(
self
)
:
# ...
def
updateServer
(
self
)
:
# ...
def
updateClient
(
self
)
:
# ...
自动补全对话框在显示这些代码时也更加易于理解。
2010-2015年
有了 12 年的编程经验后,我才已完成了一个完整的游戏项目。
虽然,到目前为止我已经学习了很多编程知识,但这个游戏却是我所犯下的一些重大错误的特辑。
数据绑定
当时,「响应式」UI 框架编程之风刚刚兴起,像微软的 MVVM 和 Google 的 Angular 。现在,这种风格的编程主要集中在 React 中。
所有这种类型的框架都基于相同的基础 promise 库。它们向你展示一个 HTML 文本字段,一个空的
标签元素和一行绑定二者的脚本代码。在文本字段中键入,然后「嘭」!
标签中的内容不可思议地更新了。
在游戏的上下文中,它看起来像这样:
public
class
Player
{
public
Property
<
string
>
Name
=
new
Property
<
string
>
{
Value
=
"Ryu"
};
}
public
class
TextElement
:
UIComponent
{
public
Property
<
string
>
Text
=
new
Property
<
string
>
{
Value
=
""
};
}
label
.
add
(
new
Binding
<
string
>
(
label
.
Text
,
player
.
Name
));
哇,现在,UI 会根据玩家的名字自动更新!我可以保持 UI 和游戏代码完全独立。这是很吸引人的,因为我们通过游戏的状态推导出 UI 状态,从而消除了游戏中的 UI 状态变量。
然而,这里仍有一些危险信号。 我不得不将游戏中的每一个字段都转换成一个 Property 对象,该对象包含依赖于它的绑定列表:
public
class
Property
<
Type
> :
IProperty
{
protected
Type
_value
;
protected
List
<
IPropertyBinding
>
bindings
;
public
Type
Value
{
get
{
return
this
.
_value
;
}
set
{
this
.
_value
=
value
;
for
(
int
i
=
this
.
bindings
.
Count
-
1
;
i
>=
0
;
i
=
Math
.
Min
(
this
.
bindings
.
Count
-
1
,
i
-
1
))
this
.
bindings
[
i
].
OnChanged
(
this
);
}
}