面向对象编程中的职责及美德

来源:互联网 发布:淘宝网商城夏装连衣裙 编辑:程序博客网 时间:2024/05/17 04:43
  对象和数据的主要差别就是对象有行为,行为可以看成责任职责(responsibilities以下简称职责)的一种,理解职责是实现好的OO设计的关键。“Understanding responsibilities is key to good object-oriented design”—Martin Fowler 。
  《对象设计:角色、责任和协作"(Object Design: Roles, Responsibilities, and Collaborations)》一书对对象的职责进行了完整阐述。
  对象过去一直被看成是被操作的数据,这也是失血贫血模式的来由,这还是一种将对象看成数据结构或集合的另外一种表现,对象是有自主行为的,对象是一种类似机器人的概念。
  职责的革命影响力
  在DDD领域驱动设计中,通常很多人会误将现实中实体完全映射到软件中领域实体,这是一种谬误,现实中实体不是一对一反映到软件中,这其中包括一个抽象过程。我们必须明白:软件领域中一个实体对象通常能够扮演多个现实中的实体角色。
  由于软件领域中对象根据不同场景可以扮演不同角色,对象的方法可以看成这些角色不同职责的表现,打个比喻:你在家是一个父亲,在单位是一个领导,如果我们建立两个实体父亲和领导就不合适了,没有经过抽象分析,其实只有一个领域模型实体,就是“你”,只不过在不同场景,你扮演不同角色,角色不同反映在角色的权利或职责行为不同,父亲的职责责任与领导的职责责任是不一样的。
  同一个领域对象通过不同方法扮演不同角色就带来实现上的一个问题,这就衍生出一种DCI架构。我们建模时,不可能将其扮演的所有角色行为都塞入实体对象中,而是应该根据不同运行场景来动态分配职责。所以,传统OO语言如Java, C#等有些难点,当然可以通过AOP的MIXIN方式变相达到,但是不是很优雅。Qi4J所谓面向组合概念实际就是这样,而Jdonframework则从EDA事件驱动方面婉转来通过事件消息来驱动不同职责响应,最新面向函数式语言如Scala这方面就优雅直接了。
  职责概念给OO世界带来巨大革命性变化,使得我们分析需求必须从职责驱动重新看待需求了。DDD一书中分析货运这个案例时,也未能从职责来审视。比如它为运输历史专门建立一个记录对象,如果从职责概念看,运输对象应该知道它自己过去的历史,这是它的职责。所以,运输历史获得应该是运输实体对象的一个方法,而不应该为其单独建立对象。
  DDD一书很多方面还是存在数据提取的影子,这是它的历史局限造成的,为了摆脱失血模式纯粹数据对象的影子,对象的职责上升到前所未有的高度。
  如何发现职责
  如果改变传统将对象看成是静止的实体概念,将实体对象看成是活的,你就很容易发现其行为职责,现实世界的对象可能是做事情或代表一些信息或东西,但是现实对象不做决定。软件中对象是活的,根据分配给他们的职责能做决定,类似智能机器人。
  职责来自于你的软件是如何工作,来自于软件的HOW。寻找和分配职责需要灵感,是一个创造性活动,是一个充满探索冒险发现新奇的乐趣活动,从下面几个方面寻找需求中职责:
  1. 来自用例分析中序列图消息发送。
  2. 构造invention、 约束表达、策略、算法、规格Specification和描述Description都可以成为职责。
  3. 系统要做的事情或要管理的信息
  4. 将实体对象看成一个演员(拟人化),扮演一个角色应该知道哪些事情knows something、会做那些事情do sth.,能够控制或决定什么事情。
  简明扼要,判断职责的主要就是:它是否知道know这些东西,它是否会做这些东西,或做一些判断决定等。
  职责分配
  将职责分配给对象,使得对象有形有态。
  按照高内聚原则分配。
  使用“如果没有这个职责,会怎样”。
  如果发现职责太广泛,不能分配到单个对象中,那么就切分职责,由这些小职责组合成更大职责。
  所谓高内聚原则:和DDD中的高聚合概念比较类似,关注类内部;一个类是否充分实现其职责目标?类中方法是否都是为实现这个职责服务的?高聚合代表鲁棒性 重用性和可理解性。
  总结
  一旦领域对象具有丰富的行为,变成富模型,或充血模型,它实际上就是一个Actor,Actor之间可以通过协作消息进行联系,而Scala的不同于多线程机制的Actor模型底层实现又更好地支撑了这样的富模型,这也是为什么Scala开始非常流行的原因之一。
  使用职责来分析需求,建立丰富对象,类似文学中的拟人化手法,将设计想象要赋予现实中静止不会说话的客观事物。当然,不能过,合适即可,目前最大问题是不够。
  函数组织到类中?恕我冒昧,这个观点是错误的。而且这是对面向对象编程中类的非常普遍的误解。类不是函数的组织者,对象也不是数据结构。
  那么什么是“合理的”对象呢?哪些不合理呢?区别又是什么?虽然这是个争论比较激烈的主题,但同时也是非常重要的。如果我们不了解对象到底是什么,我们怎么才能编写出面向对象的软件呢?好吧,幸亏Java、Ruby,还有其他语言,我们可以。但是它到底有多好呢?很不幸,这不是精确的科学,而且有很多不同的观点。下面是我认为一个良好对象应该具有的品质。
  类与对象
  http://www.amdcwangzhan.com/;http://www.esb1166.com/http://www.bbinwang.com/;http://www.188amdc.com/

  在我们谈论对象之前,我们先来看看类是什么。类是对象出生(也叫实例化)的地方。类的主要职责是根据需要创建新对象,以及当它们不再被使用时销毁它们。类知道它的孩子长什么样、如何表现。换言之,类知道它们遵循的合约(contract)。
  有时我听到类被称作“对象模板”(比如,Wikipedia就这样说)。这个定义是不对的,因为它把类放到了被动的境地。这个定义假设有人先取得一个模板,然后使用这个模板创建一个对象。技术上这可能是对的,但是在概念上是错误的。其他人不应该牵涉进来 —— 应该只有类和它的孩子。一个对象请求类创建另一个对象,然后类创建了一个对象;就是这样。Ruby表达这个概念要比Java或C++好多了:
photo = File.new('/tmp/photo.png')
  photo对象被类File创建(new是类的入口点)。一旦被创建后,对象可以自我支配。它不应该知道是谁创建了它,以及类中它的兄弟姐妹有多少。是的,我的意思是反射(reflection) 是个可怕的观点,我将会在接下来用一篇博客来详细阐述:) 现在,我们来谈谈对象以及它们最好和最糟的方面。

  1. 他存在于现实生活中 http://www.113ylc.com/;http://www.888yingfeng.com/http://www.ylconline.net/

  http://www.6188am.com/http://www.amdckaihu.com/;http://www.ylc6988.com/;http://www.hongsheng908.com/


  首先,对象是一个活着的有机体。而且,对象应该被人格化,即,被当做人一样对待(或者宠物,如果你更喜欢宠物的话)。根本上说,我的意思是对象不是一个数据结构或者一组函数的集合。相反,它是一个独立的实体,有自己的生命周期,自己的行为,自己的习惯。
  一名雇员,一个部门,一个HTTP请求,MySQL中的一张表,文件的一行,或者文件本身都是合理的对象 —— 因为它们存在于现实生活,即使当软件被关闭时。更准确来说,一个对象是现实生活中一个生物的表示(representative)。与其他对象来一样,它作为现实生活中生物的代理。如果没有这样的生物,显然不存在这样的对象。
photo = File.new('/tmp/photo.png')
puts photo.width()
  这个例子中,我请求File创建一个新对象photo,它将是磁盘上一个真实文件的表示。你也许会说文件也是虚拟的东西,只有电脑开机时才会存在。我同意,那么我把“现实生活”的重新定义为:它是对象所处的程序范围之外的一切事物。磁盘上的文件在我们的程序范围之外;这就是为何在程序内创建它的表示是完全正确的。
  一个控制器,一个解析器,一个过滤器,一个验证器,一个服务定位器,一个单例,或者一个工厂都不是良好对象(是的,多数GoF模式都是反模式(anti-patterns)!)。脱离了软件,它们并不存在于现实生活中。它们被创建完全是为了将其他对象联系在一起。它们是人造的、仿冒的生物。它们并不表示任何人。严格上说,一个XML解析器到底表示谁呢?没有人。
  它们中的一些如果改变名字可能变成良好的;其余对象的存在则是毫无理由的。比如,XML解析器可以更名为“可解析的XML”,然后可以表示我们程序范围外的XML文档。
  始终问问自己,“我的对象所对应现实生活中的实体是什么?”如果你不能找到答案,考虑下重构吧。
  2. 他根据合约办事
       http://www.bjlag.com/http://www.bjlbbin.com/http://www.bjldaili88.com/

  一个良好对象总是根据合约(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();
  }http://www.yule1144.com/ 、http://www.888lhj.com/、http://www.yuleworld.net/
}
  我可以创建很多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;
  }http://www.pk10pk10pk10.com/、http://www.888wwcp.com/、http://www.bazhussc.com/、http://www.pk1018.com/
  @Override
  public int read() throws IOException {
    return HttpURLConnection.class.cast(
      this.page.openConnection()
    ).getResponseCode();
  }http://www.zjh415.com/、http://www.zjhgame.net/、http://www.118zjh.com/、http://www.bjlluntan.net/
}http://www.919bjl.com/、http://www.2015bjlbbs.com/、http://www.laohu777.com/、http://www.777laohuji777.com/

  尽管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;
  }http://www.pkbjl110.com/、http://www.jiedu188.com/、http://www.gaopin1888.com/
http://www.pk10zhiboba.com/

}
  为什么这是错的?我们冒险破坏了整个父类的逻辑,因为重写了他的一个方法。记住,一旦我在子类重写了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;
  }
}
  确保该类实现了与原始类相同的接口:Status。HTTPStatus的实例将会通过构造函数被传递和封装给他。然后所有的调用将会被拦截,如果需要,可以通过其他方式来实现。这个设计中,我们把原始对象当做黑盒,而没有触及他的内部逻辑。
  如果你不使用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的 —— 而不是其他任何类型。
0 0
原创粉丝点击