本文由 伯乐在线 – zer0Black 翻译,黄利民 校稿。未经许可,禁止转载!
英文出处:Sahand Saba。欢迎加入翻译小组。
有
一个健康的自我批评对于专业和个人成长是很重要的。对于编程而言,自我批评的意义就是需要能查明设计中、代码中、开发中和行为中的无效或反效果的模式。这
就是为什么反面模式对任何程序员都很有用的原因。基于我遇到它们的频率和解决它们花费的时间,本文讨论了我发现的反复出现的、粗略组织的反模式。
某些反模式讨论到了它们被普遍认知偏误的地方,也有的错误是直接由它们引起的。这提供了一些关于认知偏误的文章。维基百科也有一份认知偏误列表供你参考。
在我们开始前,请记住,教条式的思考阻碍了成长和创新。因此,把下面的列表作为指导而不是一成不变的规则。如果我没有写到某些你认为重要的内容,请在下面给我留言!
1 过早优化
97%的时间里,我们应该忘掉微不足道的效率:过早的优化是万恶之源。然后,在3%的决定性时刻,我们不应该错过优化 —— Donald Knuth
不假思索就动手,还不如不做。—— Tim Peters, 《The Zen of Python》
什么意思?
在你有足够的信息能确定在哪优化、如何优化之前,就展开优化。
糟糕的原因
想要知道实践中的确切瓶颈很困难。试图在得到实验数据之前就实行优化,可能会提高代码复杂度,并引发难以察举的bug。
如何避免
把整洁的、可读性强的、能运行的代码放在首位,使用已知的和测试过的算法和工具。当需要找到瓶颈和决定优化优先级时,使用分析工具。依赖于测量而不是臆想和推断。
例子和标志
在找瓶颈之前做缓存。使用复杂的、未经证实的“启发式”算法替代出名的、数学上正确的算法。选择一种新的、未测试的web框架,当你处于早期阶段时,你的服务器大部分时间处于闲置状态,那这种框架理论上可以降低高负载下的请求延迟。
棘手的部分
棘手的地方在于知道什么时候属于提前优化。提前规划对于增长而言是很重要的。选择易于优化和增长的设计和平台是关键。也有可能用“提前优化”作为代码糟糕的接口。例如:在有更简单的、正确的O(n)算法存在时,却选择一个O(n²)的算法,仅仅因为前者更难理解。
总结
优化之前分析。避免为了效率而牺牲简洁性,除非效率被验证了的确是有必要的。
2 单车车库
“每次我们一讨论封面的排版和颜色就会被打断。讨论之后,我们就被要求投票。我认为投给在之前的会议上讨论出的颜色是最有效率的,但事实证明我总是少数派!我们最后选择了红色。(讨论应是蓝色)” —— Richard Feynman, 《你在乎其他人的想法吗》
什么意思?
花大量时间来辩论和决定琐碎、太主观的问题的这种趋势。
糟糕的原因
这是在浪费时间。Poul-Henning Kamp 在这封邮件里进行了深入讨论。
如何避免
如果你注意到了,那鼓励团队成员意识到这种趋势,并且优先达成决定(投票、抛硬币等,如果你不得不这样做的话)。当这个决定有意义时(例如:决定两种不同的UI设计),考虑随后A/B的测试来回顾这个决定,而不是进一步的内部讨论。
Richard Feynman 不是单车车库的粉丝
例子和标志
花费数小时甚至数天来讨论你的app要用什么背景色,或者一个 UI 按钮应该放在左边还是右边,又或者写代码时用制表符缩进而不是空格。
棘手的部分
依我所见,单车车库相对于提前优化更容易被发现和制止。只要注意你用在做决定和合约上的琐碎问题的时间,如果有必要,就加以干涉。
总结
避免花费太多时间在琐碎的事情上。
3 分析瘫痪
只想要预见性,不情愿去做简单有效的事,缺乏清晰的思考,建议混乱……这些构成了历史上无休止重复的特点。—— Winston Churchill, 《国会辩论》
做也许好过不做。—— Tim Peters, 《The Zen of Python》
什么意思?
对问题的过度分析,阻碍了行动和进展。
糟糕的原因
过度分析会延缓进展,甚至彻底终止进展。在极端情况下,分析的结果到了要做的时候已经过时了,或者更糟的是,项目或许从来走不出分析阶段。当决定难以做出时,很容易想到,更多的信息将会有助于做出决定——参看 资讯偏误) 和 效度偏误。
如何避免
重申一下,意识是有帮助的。重点在于迭代和改进。伴随着更多有帮助的、有意义的分析得到的数据,每次迭代都会提供更多的反馈。没有新的数据点,更多的分析将变得越来越让人猜疑。
例子和标志
花费数月、甚至数年来决定一个项目的需求、新 UI、或数据库设计。
棘手的部分
棘手的地方在于要知道什么时候该从计划、需求收集和设计阶段转移到实施和测试阶段。
总结
宁愿迭代,也不要过度分析和猜测。
4 上帝类
简单胜过复杂。—— Tim Peters,《 The Zen of Python》
什么意思?
上帝类是控制很多其它类,以及有很多依赖类,也就有更大的责任。
糟糕的原因
上帝类增长到后期就会变成维护人员的地狱——因为它违反了单一责任原则,它们难以单元测试、调试和记录文档。
如何避免
通过把责任打散成单一的、清晰的、经过单元测试的、文档易编写的类,可以避免类变成上帝类。
例子和标志
寻找类名包含了“manager”、“controller”、“driver”、“system”、或“engine”的类。小心导入或依赖太多其他类,或操作太多其他类,或有很多处理不相关任务方法的类。
棘手的部分
随着项目年限、需求和工程师人数的增长,小型的且有着良好意图的类慢慢地变成了上帝类。重构这些类就变成了浩大的任务。
总结
避免有着太多责任和依赖的庞大的类。
5 新增类恐惧症
稀少胜于繁杂。—— Tim Peters, 《The Zen of Python》
什么意思?
认为更多的类必然使得设计更加复杂,导致对新增类或把大类分解为一些小类感到恐惧。
糟糕的原因
新增类可以明显降低复杂度。下面是一张大而乱的毛线团。当解开时,你将得到集团分开的毛线团。类似的,一些简单的、易于维护、易于记录文档的类,要远远好过于有着太多责任的、单一庞大的、复杂类(参看上面的上帝类的反设计模式)。
如何避免
注意,什么时候可以简化设计新增类,以及解耦代码中不必要的耦合部分
例子和标志
考虑下面一个简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Shape: def __init__( self , shape_type, * args): self .shape_type = shape_type self .args = args def draw( self ): if self .shape_type = = "circle" : center = self .args[ 0 ] radius = self .args[ 1 ] # Draw a circle... elif self .shape_type = = "rectangle" : pos = self .args[ 0 ] width = self .args[ 1 ] height = self .args[ 2 ] # Draw rectangle... |
现在对比下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
< class Shape: def draw( self ): raise NotImplemented( "Subclasses of Shape should implement method 'draw'." ) class Circle(Shape): def __init__( self , center, radius): self .center = center self .radius = radius def draw( self ): # Draw a circle... class Rectangle(Shape): def __init__( self , pos, width, height): self .pos = pos self .width = width self .height = height def draw( self ): # Draw a rectangle... |
当然,这是一个显而易见的例子,但是它表明了一点:依赖性强的或有复杂逻辑的大型类,可以分解,也应该被分解为更小的类。结果就是,代码将有更多的类,但类更简单。
棘手的部分
新增类不是魔法弹。通过分解大型类来简化设计需要深入的考虑分析责任和需求。
总结
类的数量多,并不能说明设计很糟糕。
6 内部平台效应
那些不理解Unix的人会因为他们的重复发明而遭到谴责。—— Henry Spencer
任何 C 或 Fortran 程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是 bug 的、运行速度很慢的 Common Lisp 实现。—— Greenspun 的第十法则
什么意思?
复杂的软件系统趋势在于重实现它们所运行的平台的特点,或平台所使用的语言,通常都比较烂。
糟糕的原因
像任务调度和磁盘缓冲区之类平台级别的任务不太容易做好。糟糕的设计方案容易导致瓶颈和漏洞,特别是系统规模变大以后。重新发明语言中可能已经存在
的非正规的语言结构会导致代码阅读起来困难,并且对刚接触代码的人来说,有更陡峭的学习曲线。它还限制了重构和代码分析工具的效用。
如何避免
学习使用你的操作系统或平台所提供的平台和功能。抵制住创建已有语言结构的诱惑(尤其是因为你不熟悉新语言而找不到你的旧语言的功能)。
例子和标志
使用你的 MySQL 数据库做为工作队列。重实现你自己的磁盘缓冲区机制而不是使用系统的。用 PHP 为你的 web 服务器编写计划任务。用 C 定义 Python 之类的语言结构的宏。
棘手的部分
在极少情况下,重新实现平台(JVM、Firefox、Chrome 等)的某些部分可能是有必要的。
总结
避免重新发明你的操作系统或开发平台已经做得很多的功能。
7 魔法数和字符串
明了胜于晦涩。—— Tim Peters,《 The Zen of Python》
什么意思?
使用未命名的数字或字符串字面量,而不是在代码里命名为常量。
糟糕的原因
主要问题是由于没给数字或字符串字面量一个描述命名或其他形式的注解,而导致它们的语义被部分或完全的隐藏了。这增加了代码理解的难度,并且如果必须要修改常量,寻找和替换或其他的重构工具会导致一些微妙的bug。看看下面的代码片段:
1
2
3
|
def create_main_window(): window = Window( 600 , 600 ) # etc... |
这两个数字是什么?假设第一个是窗口宽度,然后第二个是窗口高度。如果需要修改宽度为800,搜索和替换就会变得很危险,因为在这个例子中,它也将修改高度的值,或许还有代码库里其它出现数字 600 的地方。
字符串字面量似乎会产生的这类问题不多,但是代码里有未命名的字符串字面量,将使得国际化更加困难,并且会导致有着相同字面量却有着不同语义这种类
似的问题。例如,英语中的同义词可能会造成搜索和替换的问题;想想看有两个“point”值出现,其中一个是名词(比如“she has a
point”),另一个是动词(比如“point out the differences……)。
如何避免
使用命名的常量、资源检索方法或者注释。
例子和标志
上面是一个简单的例子。这种特定的反面模式非常容易检测到(除了下面提及的一些棘手的情况。)
棘手的部分
有一个狭窄的灰色地带,难以确定特定的数字是不是魔术数字。例如,从0开始的索引中的数字0。其他例子还有,用100来计算百分比,用2做奇偶校验等等。
总结
避免在代码中出现未注释、未命名的数字和字符串字面量。
8 数字管理
用代码行数来衡量开发进度,无异于用重量来衡量制造飞机的进度。—— Bill Gates
什么意思?
严格地依靠数字来做决定。
糟糕的原因
数字很棒。避免本文提及的两个反模式(提前优化和单车车库)的主策略是分析或做A/B测试,来帮助你根据数字优化或做决策,而不是光靠凭空想。然
而,盲目的信任数字也很危险。例如,模型无效了但数字还在,或者模型过期了不再能精准的代表现实。这就会导致一些错误的决定,尤其是如果它们完全自动化
时。请参考自动化偏误。
依赖于数字做决定(不仅仅是告知)带来的另一个问题是,策略过程可以随着时间来调整以达成期望的数字(请参见观察者期望效应)。
分数膨胀就是这种情况的一个例子。HBO 的节目《The
Wire/火线》是一个好例子(顺便说一句,如果你还没有看过,你一定要看!),它通过展示警察部门和后来的教育系统用数字游戏取代了有意义的目标来描述
依赖数字的问题。如果你喜欢图表,下面的图表展示了 30% 通过率的一场考试的分数分布,极好地说明了这个观点。
波兰高中毕业考试中通过率30%的分数分布。
如何避免
要理智地使用测量和数字,而非盲目。
例子和标志
使用代码行数、提交次数等来评判程序员的效率。通过员工呆在公司的小时数来测量他们的贡献。
棘手的部分
运营规模越大,需要做出决策的数字就越高,这意味着自动化和盲目依赖数字做决策开始蔓延到过程里了。
总结
用数字来得出你的决策,但不是用数字来做决定。
9 无用的(幽灵)类
要达到完美,不是没有东西可加,而是没有东西可减。—— Antoine de Saint Exupéry
什么意思?
无用类本身没有真正的责任,经常用来指示调用另一个类的方法或增加一层不必要的抽象层。
糟糕的原因
幽灵类增加了测试和维护时候的额外代码和复杂度,降低了代码可读性。阅读者首先需要明白幽灵类在做啥,当然, 通常是什么都不做,然后锻炼自己在心理上替换成实际处理事务的类。
如何避免
不要写无用的类,或者通过重构来消除。Jack Diederich 有一个很赞的演讲,题为《 Stop Writing Classes》,就和这个反模式相关。
例子和标志
多年前,我正忙于我的硕士学位,当时我是大一 Java 编程课的助教。在其中一个实验课上,我收到了实验材料,内容是关于使用链表来实现栈。我还收到了参考答案。下面是给我的答案,一个 Java 文件,几乎没做改动(限于篇幅我删除了注释):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import java.util.EmptyStackException; import java.util.LinkedList; public class LabStack() { private LinkedList<> list; public LabStack() { list = new LinkedList<>(); } public boolean empty() { return list.isEmpty(); } public T peek() throws EmptyStackException { if (list.isEmpty()) { throw new EmptyStackException(); } return list.peek(); } public T pop() throws EmptyStackException { if (list.isEmpty()) { throw new EmptyStackException(); } return list.pop(); } public void push(T element) { list.push(element); } public int size() { return list.size(); } public void makeEmpty() { list.clear(); } public String toString() { return list.toString(); } |
你可以想象当我看到这个参考答案时的困惑,试图搞清楚 LabStack
类是做什么的,以及学生应该从这个毫无意义的练习中学到什么。在本例中,这个类的错误不是太明显,但它没有意义!它只是通过实例化的 LinkedList
对象传递调用。这个类修改了很多方法的名字(比如把通用的 clear
换成 makeEmpty
),这只会让用户困惑。错误检查逻辑完全不必要,因为 LinkedList
里的方法已经做了同样工作(但是抛出了一个不同的异常,NoSuchElementException
,这是又一个可能困惑的地方)。直到今天,我还是无法想象当学生拿到这份实验材料时,作者会作何感想。当你看到和上例相似的类时,重新考虑一下,它们是否真的需要。
棘手的部分
这里的建议初看起来和“害怕新增类”的建议相矛盾。重要的是要明白,类在什么时候发挥着有价值的角色,然后简化设计,而不是无谓地增加复杂度却没有得到益处。
总结
避免没有真正责任的类。
转载请注明:学时网 » 每个程序员要注意的 9 种反模式