好对象的7大美德

标签: 对象 美德 | 发表时间:2015-04-19 21:56 | 作者:
出处:http://kb.cnblogs.com/

   英文原文: Seven Virtues of a Good Object

  Marin Folwer 说过

“库本质上是一组可以调用的函数,这些函数现在经常被组织到类中。”

  函数组织到类中?恕我冒昧,这个观点是错误的。而且这是对面向对象编程中类的非常普遍的误解。类不是函数的组织者,对象也不是数据结构。

  那么什么是“合理的”对象呢?哪些不合理呢?区别又是什么?虽然这是个争论比较激烈的主题,但同时也是非常重要的。如果我们不了解对象到底是什么,我们怎么才能编写出面向对象的软件呢?好吧,幸亏Java、Ruby,还有其他语言,我们可以。但是它到底有多好呢?很不幸,这不是精确的科学,而且有很多不同的观点。下面是我认为一个良好对象应该具有的品质。

  类与对象

  在我们谈论对象之前,我们先来看看类是什么。类是对象出生(也叫实例化)的地方。类的主要职责是根据需要创建新对象,以及当它们不再被使用时销毁它们。类知道它的孩子长什么样、如何表现。换言之,类知道它们遵循的合约(contract)。

  有时我听到类被称作“对象模板”(比如, Wikipedia就这样说)。这个定义是不对的,因为它把类放到了被动的境地。这个定义假设有人先取得一个模板,然后使用这个模板创建一个对象。技术上这可能是对的,但是在概念上是错误的。其他人不应该牵涉进来 —— 应该只有类和它的孩子。一个对象请求类创建另一个对象,然后类创建了一个对象;就是这样。Ruby表达这个概念要比Java或C++好多了:

photo = File.new('/tmp/photo.png')

  photo对象被类 File创建( new是类的入口点)。一旦被创建后,对象可以自我支配。它不应该知道是谁创建了它,以及类中它的兄弟姐妹有多少。是的,我的意思是 反射(reflection) 是个可怕的观点,我将会在接下来用一篇博客来详细阐述:) 现在,我们来谈谈对象以及它们最好和最糟的方面。

  1. 他存在于现实生活中

  首先,对象是一个 活着的有机体。而且,对象应该被 人格化,即,被当做人一样对待(或者宠物,如果你更喜欢宠物的话)。根本上说,我的意思是对象不是一个数据结构或者一组函数的集合。相反,它是一个独立的实体,有自己的生命周期,自己的行为,自己的习惯。

  一名雇员,一个部门,一个HTTP请求,MySQL中的一张表,文件的一行,或者文件本身都是合理的对象 —— 因为它们存在于现实生活,即使当软件被关闭时。更准确来说,一个对象是现实生活中一个生物的表示 (representative)。与其他对象来一样,它作为现实生活中生物的代理。如果没有这样的生物,显然不存在这样的对象。

photo = File.new('/tmp/photo.png')
puts photo.width()

  这个例子中,我请求 File创建一个新对象 photo,它将是磁盘上一个真实文件的表示。你也许会说文件也是虚拟的东西,只有电脑开机时才会存在。我同意,那么我把“现实生活”的重新定义为:它是对象所处的程序范围之外的一切事物。磁盘上的文件在我们的程序范围之外;这就是为何在程序内创建它的表示是完全正确的。

  一个控制器,一个解析器,一个过滤器,一个验证器,一个服务定位器,一个单例,或者一个工厂都 不是良好对象(是的,多数GoF模式都是反模式(anti-patterns)!)。脱离了软件,它们并不存在于现实生活中。它们被创建完全是为了将其他对象联系在一起。它们是人造的、仿冒的生物。它们并不表示任何人。严格上说,一个XML解析器到底表示谁呢?没有人。

  它们中的一些如果改变名字可能变成良好的;其余对象的存在则是毫无理由的。比如,XML解析器可以更名为“可解析的XML”,然后可以表示我们程序范围外的XML文档。

  始终问问自己,“我的对象所对应现实生活中的实体是什么?”如果你不能找到答案,考虑下重构吧。

  2. 他根据合约办事

  一个良好对象总是根据合约(constract)办事。他期望被雇佣是因为他遵循合约而不是他的个人优点。另一方面,当我们雇佣一个对象,我们不应该歧视它,并期望一个特定类的特定对象来为我们工作。我们应该期望任何对象做我们间的合约所约定的事情。只要这个对象做我们所需要的事,我们就不应该关心他的出身,他的性别,或者他的信仰。

  比如,我想要在屏幕上展示一张图片。我希望图片从一个PNG格式的文件读取。我其实是在雇佣一个来自 DataFile类的对象,要求他给我那幅图片的二进制内容。

  但是等会,我关心内容到底来自哪里吗 —— 磁盘上的文件,或者HTTP请求,或者可能Dropbox中的一个文档?事实上,我不关心。我所关心的是有对象给我PNG内容的字节数组。所以,我的合约是这样的:

interface Binary {
byte[] read();
}

  现在,任何类的任何对象(不仅仅是 DataFile)都可以为我工作。如果他是合格的,那么他所应该做的,就是遵循合约 —— 通过实现 Binary接口。

  规则很简单:良好对象的每个公共方法都应该实现接口中对应的方法。如果你的对象有公共方法没有实现任何接口,那么他被设计得很糟糕。

  这里有两个实际原因。首先,一个没有合约的对象不能在单元测试中进行模拟(mock)。另外,无合约的对象不能通过 装饰(decoration)来扩展。

  3. 他是独特的

  一个良好对象应当总是封装一些东西以保持独特性。如果没有可以封装的东西,这个对象可能有完全一样的复制品(克隆),我认为这是糟糕的。下面是一个可能有克隆的糟糕对象的例子:

class HTTPStatus implements Status {
private URL page = new URL("http://www.google.com");
@Override
public int read() throws IOException {
return HttpURLConnection.class.cast(
this.page.openConnection()
).getResponseCode();
}
}

  我可以创建很多 HTTPStatus类的实例,它们都是相等的:

first = new HTTPStatus();
second = new HTTPStatus();
assert first.equals(second);

  很显然,实用类(utility classes),可能只包含静态方法,不能实例化良好对象。更一般地说,实用类没有本文提到的任何优点,甚至不能称作”类”。它们仅仅滥用了对象范式(object paradign),它们能存在于面向对象中仅仅由于它们的创造者启用了静态方法。

  4. 他是不可变的

  一个良好对象应该永远不改变他封装的状态。记住,对象是现实生活中实体的表示,而这个实体应该在对象的整个生命周期中保持不变。换句话说,对象不应该背叛他所表示的实体。他永远不应该换主人。:)

  注意,不可变性(immutability)并不意味着所有方法都应该返回相同的值。相反,一个良好的不可变对象是非常动态的。然而,他不应该改变他的内部状态。比如:

@Immutable
final class HTTPStatus implements Status {
private URL page;
public HTTPStatus(URL url) {
this.page = url;
}

@Override
public int read() throws IOException {
return HttpURLConnection.class.cast(
this.page.openConnection()
).getResponseCode();
}
}

  尽管 read()方法返回不同的值,这个对象仍然是不可变的。他指向一个特定的Web页面,并且永远不会指向其他地方。他永远不会改变他的内部状态,也不会背叛他所表示的URL。

  为什么不可变性是一个美德呢?这篇文章进行了详细的解释: 对象应该是不可变的。简而言之,不可变对象更好,因为:

  • 不可变对象创建、测试和使用更加简单。
  • 真正的不可变对象总是线程安全的。
  • 他们可以帮助避免时间耦合(temporal coupling,[ 译者注]指系统中组件的依赖关系与时间有关,如,两行代码,后一行需要前一行代码先执行,这种依赖关系就是与时间有关的,对应的还有空间耦合/spatial coupling)。
  • 他们的用法没有副作用(没有防御性拷贝,[ 译者注]由于对象是可变的,为了保存对象在执行代码前的状态,需要对该对象做一份拷贝)。
  • 他们总是具有失败原子性(failure atomicity, [ 译者注]如果方法失败,那么对象状态应该与方法调用前一致)。
  • 他们更容易缓存。
  • 他们可以防止 空引用

  当然,一个良好的对象不应该有 setter方法,因为这些方法可以改变他的状态,强迫他背叛URL。换言之,在 HTTPStatus类中加入一个 setURL()方法是个可怕的错误。

  除了这些,不可变对象将督促你进行更加内聚(cohesive)、健壮(solid)、容易理解(understandable)的设计,如这篇文件阐述的: 不可变性如何有用

  5. 他的类不应该包含任何静态(static)的东西

  一个静态方法实现了类的行为,而不是对象的。假如我们有个类 File,他的孩子都拥有 size()方法:

final class File implements Measurable {
@Override
public int size() {
// calculate the size of the file and return
}
}

  目前为止,一切都还好; size()方法的存在是因为合约 Measurable,每个 File类的对象都可以测量自身的大小。一个可怕的错误可能是将类的这个方法设计为静态方法(这种类被称作 实用类,在Java,Ruby,几乎每一个OOP语言中都很流行):

// 糟糕的设计,请勿使用!
class File {
public static int size(String file) {
// 计算文件大小并返回
}
}

  这种设计完全违背了面向对象范式(object-oriented paradigm)。为什么?因为静态方法将面向对象编程变成“面向类”编程(class-oriented programming)了。 size()方法将类的行为暴露出去,而不是他的对象。这有什么错呢,你可能会问?为什么我们不能在代码中将对象和类都当做第一类公民(first-class citizens,[ 译者注]可以参与其他实体所有操作的实体,这些操作可能是赋值给变量,作为参数传递给方法,可以从方法返回等,比如int就是大多数语言的第一类公民,函数是函数式语言的第一类公民等)呢?为什么他们不能同时有方法和属性呢?

  问题是在面向类编程中,分解(decomposition)不适用。我们不能拆分一个复杂的问题,因为整个程序中只有一个类的实例存在。而OOP的强大是允许我们将对象作为一种作用域分解(scope decomposition)的工具来用。当我在方法中实例化一个对象,他将专注于我的特定任务。他与这个方法中的其他对象是完全隔离的。这个对象在此方法的作用域中是个局部变量。含有静态方法的类,总是一个全局变量,不管我在哪里使用他。因此,我不能把与这个变量的交互与其他变量隔离开来。

  除了概念上与面向对象的原则相悖,公共静态方法有一些实际的缺点:

  首先, 不可能模拟他们(好吧,你可以使用 PowerMock,这将成为你在一个Java项目所能做出的最可怕决定…几年前,我犯过一次)。

  再者,概念上他们 不是线程安全的,因为他们总是根据静态变量交互,而静态变量可以被所有线程访问。你可以使他们线程安全,但是这总是需要显式地同步(explicit synchronization)。

  每次你遇到一个静态方法,马上重写!我不想再说静态(或全局)变量有多可怕了。我认为这是很明显的。

  6. 他的名字不是一个工作头衔

  一个对象的名字应该告诉我们这个对象 什么,而不是它 什么,就像我们在现实生活中给物体起名字一样:书而不是页面聚合器,杯子而不是装水器,T恤而不是身体服装师(body dresser)。当然也有例外,比如打印机和计算机,但是他们都是最近才被发明出来,而且这些人没有读过这篇文章。:)

  比如,这些名字告诉我们他们的主人是谁:苹果,文件,一组HTTP请求,一个socket,一个XML文档,一个用户列表,一个正则表达式,一个整数,一个PostgreSQL表,或者Jeffrey Lebowski。一个命名合理的对象总是可以用一个小的示意图就能画出来。即使正则表达式也可以画出来。

  相反,下面例子中的命名,是在告诉我们他们的主人做什么:一个文件阅读器,一个文本解析器,一个URL验证器,一个XML打印机,一个服务定位器,一个单例,一个脚本运行器,或者一个Java程序员。你能画出来他们吗?不,你不能。这些名字对良好对象来说是不合适的。他们是糟糕的名字,会导致糟糕的设计。

  一般来说,避免以“-er”结尾的命名 —— 他们中的大多数都是糟糕的。

  “ FileReader的替代名字是什么呢?”我听到你问了。什么将会是个好命名呢?我们想想。我们已经有 File了,他是真实世界中磁盘上文件的表示。这个表示并不足够强大,因为他不知道怎么读取文件内容。我们希望创建更强大的,并且具有此能力的一个。我们怎么称呼他呢?记住,名字应该说明他是什么,而不是他做什么。那他是什么呢?他是个拥有数据的文件;但是不仅仅是类似 File的文件,而是一个更复杂的拥有数据的文件。那么 FileWithData或者更简单 DataFile怎么样?

  相同的逻辑也适用于其他名字。始终思考下他 是什么而不是他做什么。给你的对象一个真实的、有意义的名字而不是一个工作头衔。

  7. 他的类要么是Final,要么是Abstract

  一个良好对象要么来自一个最终类,要么来自一个抽象类。一个 final类不能通过继承被扩展。一个 abstract类不能拥有孩子。简单上说,一个类应该要么声称,“你不能破坏我,我对你来说是个黑盒”,要么“我已经被破坏了;先修复我然后再使用我”。

  它们中间不会有其他选项。最终类是个黑盒,你不能通过任何方式进行修改。当他工作他就工作,你要么用他,要么丢弃他。你不能创建另外一个类继承他的属性。这是不允许的,因为 final修饰符的存在。唯一可以扩展最终类的方法是对他的孩子进行包装。假如有个类 HTTPStatus(见上),我不喜欢他。好吧,我喜欢他,但是他对我来说不是足够强大。我希望如果HTTP状态码大于400时能抛出一个异常。我希望他的方法 read()可以做得更多。一个传统的方式是扩展这个类,并重写他的方法:

class OnlyValidStatus extends HTTPStatus {
public OnlyValidStatus(URL url) {
super(url);
}
@Override
public int read() throws IOException {
int code = super.read();
if (code > 400) {
throw new RuntimException("unsuccessful HTTP code");
}
return code;
}
}

  为什么这是错的?我们冒险破坏了整个父类的逻辑,因为重写了他的一个方法。记住,一旦我在子类重写了 read()方法,所有来自父类的方法都会使用新版本的 read()方法。字面上讲,我们其实是在将一份新的“实现片段”插入到类中。理论上讲,这是种冒犯。

  另外,扩展一个最终类,你需要把他当做一个黑盒,然后使用自己的实现来包装他(也叫 装饰器模式):

final class OnlyValidStatus implements Status {
private final Status origin;
public OnlyValidStatus(Status status) {
this.origin = status;
}
@Override
public int read() throws IOException {
int code = this.origin.read();
if (code > 400) {
throw new RuntimException("unsuccessful HTTP code");
}
return code;
}
}

  确保该类实现了与原始类相同的接口: StatusHTTPStatus的实例将会通过构造函数被传递和封装给他。然后所有的调用将会被拦截,如果需要,可以通过其他方式来实现。这个设计中,我们把原始对象当做黑盒,而没有触及他的内部逻辑。

  如果你不使用 final关键字,任何人(包括你自己)都可以扩展这个类并且…冒犯他:( 所以没有 final的类是个糟糕的设计。

  抽象类则完全相反 —— 他告诉我们他是不完整的,我们不能”原封不动(as is)”直接使用他。我们需要将我们自己的实现逻辑插入到其中,但是只插入到他开放给我们的位置。这些位置被显式地标记为 abstract。比如,我们的 HTTPStatus可能看起来像这样:

abstract class ValidatedHTTPStatus implements Status {
@Override
public final int read() throws IOException {
int code = this.origin.read();
if (!this.isValid()) {
throw new RuntimException("unsuccessful HTTP code");
}
return code;
}
protected abstract boolean isValid();
}

  你也看到了,这个类不能够准确地知道如何去验证HTTP状态码,他期望我们通过继承或者重载 isValid()方法来插入那一部分逻辑。我们将不会通过继承来冒犯他,因为他通过 final来保护其他方法(注意他的方法的修饰符)。因此,这个类预料到我们的冒犯,并完美地保护了这些方法。

  总结一下,你的类应该要么是 final要么是 abstract的 —— 而不是其他任何类型。

相关 [对象 美德] 推荐:

好对象的7大美德

- - 博客园_知识库
   英文原文: Seven Virtues of a Good Object.   Marin Folwer 说过:. “库本质上是一组可以调用的函数,这些函数现在经常被组织到类中. 而且这是对面向对象编程中类的非常普遍的误解. 类不是函数的组织者,对象也不是数据结构.   那么什么是“合理的”对象呢.

javascript对象转json

- - JavaScript - Web前端 - ITeye博客
把javascript对象转成json. 已有 0 人发表留言,猛击->> 这里<<-参与讨论. —软件人才免语言低担保 赴美带薪读研.

对象的消息模型

- loudly - 酷壳 - CoolShell.cn
[ ———— 感谢 Todd 同学 投递本文,原文链接 ———— ]. 话题从下面这段C++程序说起,你认为它可以顺利执行吗. 试试的确可以顺利运行输出hello world,奇怪吗. 其实并不奇怪,根据C++对象模型,类的非虚方法并不会存在于对象内存布局中,实际上编译器是把Hello方法转化成了类似这样的全局函数:.

jQuery的deferred对象详解

- 郑小东 - 阮一峰的网络日志
jQuery的开发速度很快,几乎每半年一个大版本,每两个月一个小版本. 今天我想介绍的,就是从jQuery 1.5.0版本开始引入的一个新功能----deferred对象. 这个功能很重要,未来将成为jQuery的核心方法,它彻底改变了如何在jQuery中使用ajax. 为了实现它,jQuery的全部ajax代码都被改写了.

我连对象都没有。。。

- 老五 - Lzhi&#39;s Views
两个黄鹂鸣翠柳,我连对象都没有. 劝君更尽一杯酒,我连对象都没有. 莫愁前路无知己,我连对象都没有. 借问酒家何处有,我连对象都没有. 停车坐爱枫林晚,我连对象都没有. 一枝红杏出墙来,我连对象都没有. 壮士一去不复还,我连对象都没有. 烈火焚烧浑不怕,我连对象都没有. 雌雄双兔奔地走,我连对象都没有.

js对象深拷贝

- - ITeye博客
在做一个前台页面你的时候用到了一个自己写的List对象,在进行深拷贝的时候参考了网上的代码:. //对象扩展,tObj被扩展对象,sObj扩展对象. Object.extend(a,b);//a获得了b的所有属性. 我自己定义的list中没有定义constructor,所以执行sObj.constructor == Array会报错,我就修改为:.

Java的对象驻留

- - Java译站
Java会将源代码中的字符串常量存储到常量池中. 这不只是说它俩的值是一样的,而是说就是同一个字符串对象. 用Java的话来说就是a==b的结果是true. 然而这个只对字符串以及小的整型或者长整型有效. 其它的对象是不会被驻留的,也就是说如果你创建了两个对象而他们的值是相等的,但他们并不是同一个对象.

方案对象管理

- - CSDN博客数据库推荐文章
方案是数据库用户拥有的数据库对象的集合,方案对象是直接引用数据库的逻辑结构,对象包括表、索引、序列、视图、同义词等结构.  这一章大部分内容,我们在SQL的第九章创建和管理表及约束和第十章其它常用对象都已讲过,做一些补充. 堆表就是普通表,也叫堆组织表. 之所以这样叫,是因为他使用数据结构中堆的算法来组织表.

jquery获取对象大全

- - CSDN博客Web前端推荐文章
1、JQuery的核心的一些方法 . $("Element").length; ‘元素的个数,是个属性 . $("Element").size(); ’也是元素的个数,不过带括号是个方法 . $("Element").get(); ‘某个元素在页面中的集合,以数组的形式存储 . $("Element").get(index); ’功能和上面的相同,index表示第几个元素,数组的下标 .

Hibrernate 数据对象状态

- - ITeye博客
Hibernate 中数据对象状态概念介绍. 一、 数据对象在Session中的状态:. 1、理解Session的缓存:Session接口是Hibernate向应用程序. 提供操作数据库的主要接口,它提供了基本的增、删、改、查 方法;. Session具有一个缓存,Hibernate的缓存包括Session的缓存和SessionFactory的缓存;.