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

标签: 面向对象 设计 原则 | 发表时间:2013-04-19 09:57 | 作者:rcom10002
出处:http://blog.csdn.net

不要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

作者:rcom10002 发表于2013-4-19 9:57:40 原文链接
阅读:170 评论:0 查看评论

相关 [面向对象 设计 原则] 推荐:

面向对象设计的设计原则

- - 博客园_知识库
  在设计面向对象的程序的时,模式不是一定要套的,但是有一些原则最好是遵守. 这些原则已知的有七个,包括:开闭原则、里氏代换原则、依赖倒转原则、合成/聚合复用原则、迪米特法则、接口隔离原则,单一职责原则. Martin引入了SOLID的说法,包括了其中的五个原则. 另外两个,这里把他们算成额外的两个规则.

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

- - CSDN博客架构设计推荐文章
不要STUPID,坚持GRASP和SOLID. 有人可能会说:这是描述设计原则的一个专业术语,由我们可爱的代码整洁之道传教者鲍勃(罗伯特C. 马丁)大叔提出,是一组用于指导我们如何写出“好代码”的原则. 在编程界充满了这样由单词首字母组成的缩略词. 其它类似的例子还有DRY(Don’t Repeat Yourself.

面向对象设计原则和创建SOLID应用的5个方法

- - ImportNew
最近我听到了很多关于函数式编程(FP),受之启发我觉得也应该关注面向对象编程(OOP)和面向对象设计(OOD),因为在设计系统时这些仍然非常重要. 我们将以SOLID原则为起点开始我们的旅程. SOLID原则是类级别的,面向对象的设计理念,它们与测试工具一起帮你改进腐坏的代码. SOLID由程序员们最喜欢的大叔 Robert C.

Java程序员应了解的10个面向对象设计原则

- - ITeye博客
众所周知,Java编程最基本的原则就是要追求高内聚和低耦合的解决方案和代码模块设计. 查看Apache和Sun的开放源代码能帮助你发现其他Java设计原则在这些代码中的实际运用. Java Development Kit则遵循以下模式:BorderFactory类中的工厂模式、Runtime类中的单件模式.

面向对象编程的五大原则

- - 非技术 - ITeye博客
       高层的实现不应该依赖底层,(父类可以替换掉任何子类),具体说就是我们要针对接口抽象来编程,不要针对实现来编程,这样程序才能解耦. 一、“开-闭”原则(Open-Closed Principle,OCP). 1.1 “开-闭”原则的定义及优点. 1)定义:一个软件实体应当对扩展开放,对修改关闭( Software entities should be open for extension,but closed for modification.).

面向对象设计模式的核心法则

- tonytech - Michael`s blog
有本经典的书叫《设计模式》,讲了经典的21种设计模式,建议大家都看看. 就一个类而言,应该仅有一个引起它变化的原因. 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力. 这种耦合会导致脆弱的设计,当变化发生时,设计会遭到意想不到的破坏. 软件设计真正要做的许多内容,就是发现职责并把那些职责互相分离.

JavaScript 游戏中的面向对象的设计

- - 博客 - 伯乐在线
来源: IBM developerworks. 简介: 从程序角度考虑,许多 JavaScript 都基于循环和大量的 if/else 语句. 在本文中,我们可了解一种更聪明的做法 — 在 JavaScript 游戏中使用面向对象来设计. 本文将概述原型继承和使用 JavaScript 实现基本的面向对象的编程 (OOP).

精通面向对象分析与设计的秘诀

- - 移动开发 - ITeye博客
精通面向对象分析与设计的秘诀. 面向对象设计的一个重要经验:. 谁拥有数据,谁就对外提供操作这些数据的方法,. 谁具有这项属性,谁就提供操作这些属性的方法. 这个案例有三个对象:Person,Blackboard,Circle. 有一个动作画圆,画圆这个动作可以封装成一个方法draw(),那么,这个draw()方法应该封装在那个类中呢.

Android设计原则

- - 所有文章 - UCD大社区
译者按: 在 iOS HIG已经强大经典了N年之后,Android终于推出了一套比较系统的 HIG(大概是为了配合Android 4.0 Ice Cream Sandwich). 仔细比较两套HIG的“设计原则”部分,发现完全是截然不同的两种风格. iOS HIG走的是更专业型的路线,描述严谨且有不少的专业词汇(比如Metaphors、Consistency之类的).

面向对象的Shell脚本

- jejer - 酷壳 - CoolShell.cn
还记得以前那个用算素数的正则表达式吗. 编程这个世界太有趣了,总是能看到一些即别出心裁的东西. 你有没有想过在写Shell脚本的时候可以把你的变量和函数放到一个类中. 不要以为这不可能,这不,我在网上又看到了一个把Shell脚本整成面向对象的东西. Shell本来是不支持的,需要自己做点东西,能搞出这个事事的人真的是hacker啊.