面向对象设计原则:不要STUPID,坚持GRASP和SOLID
不要STUPID,坚持GRASP和SOLID
听过SOLID编码吗?有人可能会说:这是描述设计原则的一个专业术语,由我们可爱的代码整洁之道传教者鲍勃(罗伯特C. 马丁)大叔提出,是一组用于指导我们如何写出“好代码”的原则。
在编程界充满了这样由单词首字母组成的缩略词。其它类似的例子还有DRY(Don’t Repeat Yourself! 不要重复你自己!)和KISS(Keep It Simple, Stupid! 让事情简单化,傻瓜化)。但是,这些条条框框好像有点多,太多,超级多……
所以,可不可以换个角度来解决这些问题呢?看看是什么原因导致我们写出“坏代码”。
抱歉,你的代码就是那么的STUPID
没有人喜欢听到别人评价他的代码很愚蠢。而且这样做也很容易冒犯别人。所以不要说出来。但平心而论:全世界中大部分代码都是不可维护的,因为它们都是乱糟糟一团的。
那些烂代码又有什么特点呢?是什么把代码变得如此STUPID?
- Singleton - 单态
- Tightcoupling - 紧密耦合
- Untestability - 不可测试
- Premature Optimization - 过早优化
- Indescriptive Naming - 胡乱命名
- Duplication - 重复代码
你同意上面的列表吗?是?好极。不?OK,我会在下面的内容中逐一解释每项观点,这样你就可以更好地明白为什么我会选用这些模式。
单态
<?php class DB { private static $instance; public static function getInstance() { if (!isset(self::$instance)) { self::$instance = new self; } return self::$instance; } final private function __construct() { /* something */ } final private function __clone() { } /* actual methods here */ }
上面的代码是一段典型的数据库访问实现,你可以从很多PHP教程中找到这样的代码。实际上,不久之前我也在使用与此风格类似的代码。
你可能会感到奇怪:这段代码怎么了?不论在哪,利用DB::getInstance()都可以很容易地访问DB啊,并且它还保证一次只有一个数据库连接。到底坏哪儿了?
嗯,很好,我之前也是这样想滴^^。“我只需要一个连接”。当应用程序规模变得稍微大一些的时候,实事会证明我还需要另一个数据库的连接。这就是混乱的开始。我把单态稍加修改,增加了一个->getSecondInstance()方法,这样,单态就变成了——“双单态”了。其实我本就应该意识到数据库连接不是一个简单的单态结构,压根就不该用它来作为实现方案。同样的单态用法你还可以找到很多。请求对象的确是单态!但听过子请求吗?日志就是!难道你不需要换个方式记录点别的什么?
上面描述的只是问题之一。还有一个重大的问题就是在代码中使用DB::getInstance()会把代码与类名DB强行绑定。也就是说,你无法对类DB进行扩展。假设我需要把查询性能数据以日志的形式写到APC(Alternative PHP Cache)中。但由于类名紧密耦合,根本无法实现这项功能。如果当初程序是采用依赖注入的方式实现的,我可以很轻松地对类型DB进行扩展,然后传入新的实例对象。但单态已经不允许我这样做了。现在我能做的就是用下面这种粗制的手段实现我的想法:
<?php // original DB class class _DB { /* ... */ } // extending class class DB extends _DB { /* ... */ }
一个字儿:丑。或许还有人会加入一些别的形容词,骇客、不可维护、大米共,或STUPID。
最后还有一点要考虑:还记得我之前说过的那句,“不论在哪,利用DB::getInstance()都可以很容易地访问DB”。不得不承认,其实这也是件糟糕的事情。一看到“无论在哪”,我们自然可以联想到“全局”,也可以理解为“单态就是一个具有特别命名的全局变量”。在你学习PHP的时候,你可能早就被告知使用关键字global是一个很坏的习惯。但殊不知使用单态和使用全局变量的影响是一样的:它们都创建了全局状态对象。这种方法创建的是非显式依赖,结果就是会让程序变得难以重用与测试。
紧密耦合
通过上面对单态问题的认识,你可能已经学会举一反三,把问题推向使用更为广泛的static方法和属性。不管在什么时候,只要编写Foo::bar()这样的代码,就是把代码和Foo类耦合在一起。这种耦合使得对Foo类的功能扩展几乎变得不可能,进而导致代码很难被重用和测试。
类似的情况还有包括普通类名的使用,它们也同样会带来代码臭味。其中包括new操作符的使用:
<?php class House { public function __construct() { $this->door = new Door; $this->window = new Window; } }
在上面的代码,你怎么替换房子中的门和窗呢?答案很简单:不可以。作为一个技法娴熟的开发者,你可能一眼就会看出怎么用些下流的骇客手法来替换门或窗。但是,或许下面的方式更为简单一些:
<?php class House { public function __construct(Door $door, Window $window) { // Door, Window are interfaces $this->door = $door; $this->window = $window; } }
采用这种方法可以很方便地把不同的门和窗加到房子里。另外,这份代码同时还具有良好的扩展性、重用性和可测试性。你还有什么可以奢求的吗?
上面的代码概括起来说就是使用了“依赖注入(DI=DependencyInjection)”。而一讲到DI,许多人就会把它和Symfony(一款PHP开发框架)这样的DIC(Dependency Injection Container=依赖注入容器)联系起来,而实际上DI的概念是非常简单的。
不可测试
单元测试很重用。如果你没有对你的代码进行过测试,你也就登上驶往破坏代码的战船。但即使是这样,还是有很多人没有很好地完成他们的测试。为什么?大多数原因可以归结于难以测试的代码。那又是什么原因使得代码难以测试呢?主要是列表中前一点内容:紧密耦合。单元测试——看似清晰明了——就是测试一个代码单元(通常是各种类)。但如果类与类之间紧密结合,又怎么可能针对每一个类进行测试呢?这时你可能会使用更多的骇客技术。但是,通常情况下大多数人都不会在此花费这么多力气,代码仍旧保持在原先无法测试的状态,并任它慢慢腐败。
每当你决定不编写测试用例时,多时会把原因归结于“没有时间”,而真正导致这个结果的原因,其实是你的代码里有太多垃圾。如果代码结构组织良好,测试不会花费你多少时间的。只有在代码杂乱无章的时候,单元测试才会成为负担。
过早优化
下面的代码片段源自于我之前编写的一个网站:
<?php if (isset($frm['title_german'][strcspn($frm['title_german'], '<>')])) { // ... }
猜一下它是干什么用的!
其实它只是检查德语标题中是否包含字符“<”或“>”,可以说下你用了多久才看明白的吗?你完全看明白了吗?
我来做一下解释:如果“<”和“>”都没有的话,strcspn会返回字符串的本身长度。所以这段代码可以简单地看成isset($str[strlen($str)])。由于字符串所允许的最大偏移量为字符串本身的长度减一,所以上面代码的结果就永远为false。假如目标字符串中包含前面所述的两个字符中的任何一个,函数就会返回一个小于字符串长度的数字,这样一来,整个表达式的结果就为true。
我为什么要写这么一段难以理解的代码呢?为什么不改用下面的方式呢:
<?php if (strlen($frm['title_german']) == strcspn($frm['title_german'], '<>'))) { // ... }因为之前我曾读到过isset要比strlen快许多……但这样写会使代码看起来很隐晦,因为它需要程序员必须精确了解函数strcspn语义(而大多数PHP程序员可能不是特别了解)。所以为什么不改写成:
<?php if (preg_match('(<|>)', $frm['title_german'])) { // ... }
因为我曾听说使用正则表达式有点慢……(这样说可能有谎话的嫌疑:实际上正则表达式要比我们想象的快得多。)
看看,除了令人费解的代码外,这些所谓的“优化”给我们带来的还有什么呢?一无是处。即便是现在,这个网站每月已经达到四千万左右的访问量(当然,在我刚写下上面代码的时候还远远没有达到这个数字),这个细小的优化基本上是微不足道的。因为这块根本就不是程序的瓶颈。而实际的瓶颈是访问最为频繁的控制器中的三重JOIN(你的程序可能也会有这样的瓶颈)。
从互联网上你可以找到很多这样的微优化(micro-optimization)。如“使用单引号,因为它们比较快一些”。别信这个。这样的建议中大部分都是错误的,即使没错,它也不会让你的代码运行速度有质的飞跃,反而只会浪费你的宝贵时间。
胡乱命名
还有一件事情需要提一下,你知道PHP的strpbrk函数是干什么的吗?不知道?你甚至没听过有这个函数?好吧,也没什么可奇怪的。没有人会去为了搜索字符串中的字符列表而去专门查找一个名为strpbrk的函数。那这个名字究竟从哪里来?其实它是继承于C语言,它的名字代表“string pointer break”。耶,真是太好了,我们在不支持指针的语言中找到指针了(我意思是PHP没有指针,不是指C)。
对了,在读前一节的代码时,你能一下子反应出函数strcspn是干什么的吗?不能?好吧,还是没有什么奇怪的。它是“stringcomplement span”的缩略形式,这样写是防止你搞不清楚它的含义。
到这里为止我们得到的教训就是:劳您大驾,在对类、方法、变量命名的时候,请多斟酌一下,尽量让别人知道您真正的意图。对$i这样的变量我不想争论什么,因为它们太短了,其中的含义不言而喻。真正的问题是出现在像上面那样命名的函数中。像函数strpbrk和变量$yysstk对于作者本人来讲可能很直观,但也就仅限于他本人了。
重复代码
我相信每个人都同意一种说法,短小精炼且直奔主题的代码都可以称为上等佳作(提一下,我说的不是语法风格像Perl/Ruby那样的“精简”)。换个角度来讲,冗长繁琐的代码自然就是丑陋不堪了。其实这也就是前面提到的DRY(不要重复你自己)和KISS(让事情简单化,傻瓜化)原则所教授我们的。
那么,重复代码从何而来?程序员们都是懒散的动物,所以少敲代码是它们的天性。这也就是为什么反复重复的代码至今还在盛行。
我个人认为,产生重复代码最常见的原因就是STUPID原则中的第二条:紧密耦合。如果你的代码彼此之间耦合的很紧,你就不可能重用它们。这就会导致重复性代码的出现。
不要STUPID,坚持GRASP和SOLID
那如何避免编写STUPID代码呢?简单,坚持GRASP和SOLID原则。
SOLID的解释为:
- Single responsibility principle
- Open/closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
GRASP代表GeneralResponsibility Assignment Software Principles(通用职责分配软件原则),它包括以下内容:
- Information Expert
- Creator
- Controller
- Low Coupling
- High Cohesion
- Polymorphism
- Pure Fabrication
- Indirection
- Protected Variations
编码快乐,新年快乐!
PS:如果你要问STUPID的出处:我可以告诉你,这些想法产自于StackOverflow的PHP聊天室,文章的每个组成部分都是由edorian、James和我搞出来的。而标题是由Gordon想出的。
译注: 原文连接:http://nikic.github.io/2011/12/27/Dont-be-STUPID-GRASP-SOLID.html GRASP:UML和模式应用 SOLID:http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod |