不要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] view plaincopy
  1. <?php
  2. class DB {
  3. private static $instance;
  4. public static function getInstance() {
  5. if (!isset(self::$instance)) {
  6. self::$instance = new self;
  7. }
  8. return self::$instance;
  9. }
  10. final private function __construct() { /* something */ }
  11. final private function __clone() { }
  12. /* actual methods here */
  13. }

上面的代码是一段典型的数据库访问实现,你可以从很多PHP教程中找到这样的代码。实际上,不久之前我也在使用与此风格类似的代码。

你可能会感到奇怪:这段代码怎么了?不论在哪,利用DB::getInstance()都可以很容易地访问DB啊,并且它还保证一次只有一个数据库连接。到底坏哪儿了?

嗯,很好,我之前也是这样想滴^^。“我只需要一个连接”。当应用程序规模变得稍微大一些的时候,实事会证明我还需要另一个数据库的连接。这就是混乱的开始。我把单态稍加修改,增加了一个->getSecondInstance()方法,这样,单态就变成了——“双单态”了。其实我本就应该意识到数据库连接不是一个简单的单态结构,压根就不该用它来作为实现方案。同样的单态用法你还可以找到很多。请求对象的确是单态!但听过子请求吗?日志就是!难道你不需要换个方式记录点别的什么?

上面描述的只是问题之一。还有一个重大的问题就是在代码中使用DB::getInstance()会把代码与类名DB强行绑定。也就是说,你无法对类DB进行扩展。假设我需要把查询性能数据以日志的形式写到APC(Alternative PHP Cache)中。但由于类名紧密耦合,根本无法实现这项功能。如果当初程序是采用依赖注入的方式实现的,我可以很轻松地对类型DB进行扩展,然后传入新的实例对象。但单态已经不允许我这样做了。现在我能做的就是用下面这种粗制的手段实现我的想法:

[php] view plaincopy
  1. <?php
  2. // original DB class
  3. class _DB { /* ... */ }
  4. // extending class
  5. class DB extends _DB { /* ... */ }

一个字儿:丑。或许还有人会加入一些别的形容词,骇客、不可维护、大米共,或STUPID。

最后还有一点要考虑:还记得我之前说过的那句,“不论在哪,利用DB::getInstance()都可以很容易地访问DB”。不得不承认,其实这也是件糟糕的事情。一看到“无论在哪”,我们自然可以联想到“全局”,也可以理解为“单态就是一个具有特别命名的全局变量”。在你学习PHP的时候,你可能早就被告知使用关键字global是一个很坏的习惯。但殊不知使用单态和使用全局变量的影响是一样的:它们都创建了全局状态对象。这种方法创建的是非显式依赖,结果就是会让程序变得难以重用与测试。

紧密耦合

通过上面对单态问题的认识,你可能已经学会举一反三,把问题推向使用更为广泛的static方法和属性。不管在什么时候,只要编写Foo::bar()这样的代码,就是把代码和Foo类耦合在一起。这种耦合使得对Foo类的功能扩展几乎变得不可能,进而导致代码很难被重用和测试。

类似的情况还有包括普通类名的使用,它们也同样会带来代码臭味。其中包括new操作符的使用:

[php] view plaincopy
  1. <?php
  2. class House {
  3. public function __construct() {
  4. $this->door   = new Door;
  5. $this->window = new Window;
  6. }
  7. }

在上面的代码,你怎么替换房子中的门和窗呢?答案很简单:不可以。作为一个技法娴熟的开发者,你可能一眼就会看出怎么用些下流的骇客手法来替换门或窗。但是,或许下面的方式更为简单一些:

[php] view plaincopy
  1. <?php
  2. class House {
  3. public function __construct(Door $door, Window $window) { // Door, Window are interfaces
  4. $this->door   = $door;
  5. $this->window = $window;
  6. }
  7. }

采用这种方法可以很方便地把不同的门和窗加到房子里。另外,这份代码同时还具有良好的扩展性、重用性和可测试性。你还有什么可以奢求的吗?

上面的代码概括起来说就是使用了“依赖注入(DI=DependencyInjection)”。而一讲到DI,许多人就会把它和Symfony(一款PHP开发框架)这样的DIC(Dependency Injection Container=依赖注入容器)联系起来,而实际上DI的概念是非常简单的。

不可测试

单元测试很重用。如果你没有对你的代码进行过测试,你也就登上驶往破坏代码的战船。但即使是这样,还是有很多人没有很好地完成他们的测试。为什么?大多数原因可以归结于难以测试的代码。那又是什么原因使得代码难以测试呢?主要是列表中前一点内容:紧密耦合。单元测试——看似清晰明了——就是测试一个代码单元(通常是各种类)。但如果类与类之间紧密结合,又怎么可能针对每一个类进行测试呢?这时你可能会使用更多的骇客技术。但是,通常情况下大多数人都不会在此花费这么多力气,代码仍旧保持在原先无法测试的状态,并任它慢慢腐败。

每当你决定不编写测试用例时,多时会把原因归结于“没有时间”,而真正导致这个结果的原因,其实是你的代码里有太多垃圾。如果代码结构组织良好,测试不会花费你多少时间的。只有在代码杂乱无章的时候,单元测试才会成为负担。

过早优化

下面的代码片段源自于我之前编写的一个网站:

[php] view plaincopy
  1. <?php
  2. if (isset($frm['title_german'][strcspn($frm['title_german'], '<>')])) {
  3. // ...
  4. }

猜一下它是干什么用的!

其实它只是检查德语标题中是否包含字符“<”或“>”,可以说下你用了多久才看明白的吗?你完全看明白了吗?

我来做一下解释:如果“<”和“>”都没有的话,strcspn会返回字符串的本身长度。所以这段代码可以简单地看成isset($str[strlen($str)])。由于字符串所允许的最大偏移量为字符串本身的长度减一,所以上面代码的结果就永远为false。假如目标字符串中包含前面所述的两个字符中的任何一个,函数就会返回一个小于字符串长度的数字,这样一来,整个表达式的结果就为true。

我为什么要写这么一段难以理解的代码呢?为什么不改用下面的方式呢:

[php] view plaincopy
  1. <?php
  2. if (strlen($frm['title_german']) == strcspn($frm['title_german'], '<>'))) {
  3. // ...
  4. }

因为之前我曾读到过isset要比strlen快许多……但这样写会使代码看起来很隐晦,因为它需要程序员必须精确了解函数strcspn语义(而大多数PHP程序员可能不是特别了解)。所以为什么不改写成:

[php] view plaincopy
  1. <?php
  2. if (preg_match('(<|>)', $frm['title_german'])) {
  3. // ...
  4. }

因为我曾听说使用正则表达式有点慢……(这样说可能有谎话的嫌疑:实际上正则表达式要比我们想象的快得多。)

看看,除了令人费解的代码外,这些所谓的“优化”给我们带来的还有什么呢?一无是处。即便是现在,这个网站每月已经达到四千万左右的访问量(当然,在我刚写下上面代码的时候还远远没有达到这个数字),这个细小的优化基本上是微不足道的。因为这块根本就不是程序的瓶颈。而实际的瓶颈是访问最为频繁的控制器中的三重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
3楼 evencoming 4小时前发表 [回复]
不如说 滥用设计模式 让代码愚蠢
2楼 csfreebird 前天 23:12发表 [回复]
Singleton将代码变得stupid? 这里例子证明了不能用Singleton封装一个数据库连接,可视如果封装一个连接池呢?封装一个工厂类呢?用来表示全局的Application对象呢?
错误的运用任何经验总计的方法和模式都会引来stupid的嘲笑,但是嘲笑的只应该是错误的运用,而不能针对模式,方法本身。没有说这是万能灵药。

Re: rcom10002 昨天 18:41发表 [回复]
回复sheismylife:个人认为作者Singleton例子不是要从狭隘的角度来说明Singleton封装DB不好,而是要让人们慎用Singleton设计模式,不能滥用,否则这样的设计会为整个架构带来什么样的副作用。从设计角度说,封装一个连接和封装一个连接池没有什么区分,重点要看设计实现是否能够满足抽象,不依赖于具体,依赖要反转。

Re: csfreebird 昨天 20:49发表 [回复]
回复rcom10002:你的观点我赞同,但是文中似乎没有看到这种观点。我也没有细看。
1楼 zjwdmlmx 前天 22:29发表 [回复]
大标题写的很好,就是标题下的例子,不能马上让人懂
  • 面向对象设计原则:不要STUPID,坚持GRASP和SOLID

    evencoming:不如说 滥用设计模式 让代码愚蠢

  • 面向对象设计原则:不要STUPID,坚持GRASP和SOLID

    csfreebird:@rcom10002:你的观点我赞同,但是文中似乎没有看到这种观点。我也没有细看。

  • 面向对象设计原则:不要STUPID,坚持GRASP和SOLID

    rcom10002:@sheismylife:个人认为作者Singleton例子不是要从狭隘的角度来说明Singleto...

  • 面向对象设计原则:不要STUPID,坚持GRASP和SOLID

    csfreebird:Singleton将代码变得stupid? 这里例子证明了不能用Singleton封装一个数据库连接...

  • 面向对象设计原则:不要STUPID,坚持GRASP和SOLID

    zjwdmlmx:大标题写的很好,就是标题下的例子,不能马上让人懂

  • Struts源码粗略分析一:准备开发环境

    lxb_champagne:楼主好人。

  • 禁用Ubuntu Desktop的图形界面,只启动文本模式的命令行

    amanonly12:这篇文章很好。只能修改启动方式,而不能直接禁用/etc/X11/default-display-ma...

  • RTF转HTML,HTML转TXT(Java版)之威力加强版

    fanrenmaomaochong:厉害啊,不过太难找到你这个文章了!

  • 动态装载外部JavaScript脚本文件

    z2482193:谁说我不记得你了?早说是你呀,我就不紧张了!

  • 禁用Ubuntu Desktop的图形界面,只启动文本模式的命令行

    ruyizi:今天我也犯了一个同样的错误,我通过ubuntu挂载ubuntu把问题解决了,谢谢你的文章。

不要STUPID,坚持GRASP和SOLID相关推荐

  1. Ubuntu Nginx uwsgi django 初试

    /*************************************************************************************** Ubuntu Ngin ...

  2. Linux的Nginx八:服务器配置

    自己直播项目里面的 server{listen 80;#listen [::]:80;server_name l2.l2.l2 ;index index.html index.htm index.ph ...

  3. nginx php 配置

    /*************************************************************************************** nginx php 配 ...

  4. 架构师必须知道的架构设计原则

    目录 一.前言 二.软件设计原则 GRASP通用职责分配软件模式 1. 信息专家(Information Expert) 2. 创建者(Creator) 3. 低耦合(Low Coupling) 4. ...

  5. nginx php 没认,NginX没有执行PHP

    我已经经历了很多这个问题的潜在解决方案,但找不到任何有效的方法.基本上, PHP文件没有在我的NginX PHP_fpm Ubuntu 14服务器上执行.我有所有的包,他们正在运行.我已经清除了浏览器 ...

  6. 如何在云服务器使用docker快速部署jupyter web服务器(Nginx+docker+jupyter+tensorflow)

    如何在云服务器部署jupyter web服务器 jack lee 邮箱:291148484@163.com 如有错误可以法邮件给我纠正,希望本文对你有所帮助. 导读:如果你用过百度人工只能的在线提交代 ...

  7. 西科大 软件体系结构内容总结

    1.面向对象方法 (1)面向对象与面向过程的区别 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了:面向对象是把构成问题事务分解成各个对象, ...

  8. 怎么才能做好一套软件系统

    [i]偶然翻到电脑里早前写的一篇帖子,转过来[/i] [b]引子[/b] 本周二,xxx系统一天内三次故障,报警短信频传.星期三一早,下去协助ABC一起分析zzz系统的事故,此时的PMD老大已经连续在 ...

  9. 【绝对详细!不好使你顺着网线敲我!】Django3.1在Ubuntu16.04上的部署

    前言 转眼间Django版本更新已经到了3.1.7,在使用Django3.1时,使用以往版本的Django部署方法进行部署时许多已经都不起作用,但也能搜索到许多有帮助的技术性文章.于是特将整个部署过程 ...

  10. 【Ubuntu20安装aria2以及配置web端】

    Ubuntu20安装aria2以及配置web端 安装aria2c 创建配置文件 aria2c 封装为服务 安装Web UI 安装Nginx 效果图 安装aria2c root@aa:~# apt in ...

最新文章

  1. 钉钉 python调用审批信息_python之钉钉审批导出
  2. ecshop手机端html,ECSHOP手机版本的head标题的修改方法分享
  3. 配置单臂路由、三层交换技术以及动态路由
  4. 在颜值上,我 Bootstrap 真的没怕过谁
  5. 科技部成立新一代人工智能发展研究中心
  6. 如何提取幻灯片表格_如何查看对Google文档,表格或幻灯片文件的最新更改
  7. VO,BO,PO,DO,DTO的区别
  8. Python下使用optparse模块实现对多个文件进行统计【二】
  9. 一台服务器创建多个ssh_如何创建可用于生产的第一台安全服务器
  10. 案例:监听域对象的属性变更
  11. vim移动一行或一段代码
  12. 实现第一个JDBC程序(详细)
  13. 新裝win7虚拟机设置记录-20180909
  14. 修改ASP.NET MVC Ajax分页组件ASP.NET MvcPager一个小Bug并修该样式为自己所用(三)...
  15. 【IntelliJ IDEA】设置字体大小
  16. 错误排查:Cloudera Manager Agent 的 Parcel 目录位于可用空间小于 10.0 吉字节 的文件系统上。 /opt/cloudera/parcels...
  17. python 去掉空格_怎样去掉 sentence 前面的空格 python
  18. 全国青少年编程等级考试scratch二级真题2021年9月(含题库答题软件账号)
  19. Mybatis insert、update 、delete返回值
  20. 论做空工具体验对比,股票下跌可选择 期权?涡轮?CFD差价合约?牛熊交易获利

热门文章

  1. 网络工程师 第1章 计算机网络概述
  2. 导入Model为啥没有addAttribute()方法???
  3. 网络请求及各类错误代码含义总结(Errors Code)
  4. C#如何快速释放内存的大数组详解
  5. 网吧电脑装linux系统下载,网吧游戏服务器操作系统用的是LINUX系统
  6. Eclipse常用便捷设置
  7. 职场沟通10个小技巧 让你迅速融入团队
  8. ipa包上传itunes store失败
  9. 苹果,忘了发家的根本…
  10. 深度装N卡LINUX驱动 性能怎么样,讲解Deepin 20开源Nouveau和闭源NVIDIA驱动,附装闭源N卡驱动的方法...