目录
背景
引用Martin Fowler的观点
我的新视角
对象使用的视角
分离对象的创建
实际编程中的分离观点
总结
背景
这篇文章是我在Net Objectives工作时写的,我在那里的工作是指导人们编写有效的面向对象程序。本文将介绍一些实用但不同以往的观点,用来解决每天出现的设计问题。本文不关注对象做什么,而是对象的使用和对象的实例化。基于这些观点,可以大规模地简化和改进代码,以满足将来维护的需要。
引用Martin Fowler的观点
在《UML精粹》第三版中,Martin Fowler提出了对象设计的三个层面:概念层、规约层(Specification)、实现层
概念层,概念层中的对象是承担一定职责的实体,通常用抽象类和接口(Interface)来描述,这些实体之间以各种方式相互联系来实现应用系统的目标。
如果我是一个对象,在概念层我关心的是“我的职责是什么”。
规约层,规约层中的对象通过它的公共方法来实现它的职责,每个公共的方法都是对象按照指定的方式提供的服务。
如果我是一个对象,在规约层我关心是“别人如何使用我”
实现层,实现层是对象的代码,对象通过代码来实现它的职责。
如果我是一个对象,在实现层我关心的是“我如何完成我的职责”
对于系统中一个实体,只立足于一个概念层次来工作,有很有益处的。同样的,把自己的思维过程划分成三部分也是有利的:
首先,这样有助于减弱藕合。如果对象之间的关系保持在抽象层面上,后期实现的子类之间的藕合会更弱。这些意见是设计模式的作者“Gang of Four”他们提出的。他们认为设计者应该“面向接口作设计”。
其次,能使系统内聚性更好结构更清楚,因为我们能围绕对象的职责来编写实现细节,而不是其它的方式。这样,一个对象被明确的定义职责范围,而并不是包含一些无关的方法和状态(这对眼前的问题豪无帮助)。
最后,其它开发者能因此有一个清楚的认识过程,因为如果对一个问题,让一个人同时从多个层面去理解它,那么他将很容易泄气。
我的新观点
下面才是正题,我将推荐一些观点,这是些类似的特性,它能帮助我们实现灵活性和健壮性,而这正是我们一直在寻找的面向对象方法。我把我的这些观点概括为:对象创建VS对象使用。
我们看看下面这段代码:
这是一个SignalProcessor的例子,它使用ByteFilter的一个实现(HiPassFilter)来完成自己的一部分工作。基本上这是不错的作法,内聚性是比较好的,每个类是一个整体,通过和其它类协作来完成自己的任务。并且,对于ByteFiler可以提供多种实现,而不需要改变SignalProcessor的设计。这是一个“可拔插”的设计,并且很容易扩展。
概念上,SignalProcess是负责处理字节数组的信号。规约方面,SignalProcessor表现为一个返回字节数组的process()方法。
而SignalProcessor的实现的方面。我们看到,SignalProcessor调用ByteFilter的实例,在我们经过这里的时候,我们只需考虑它的规约(filter()方法),而不需考虑它的实现。这样很好,干净清楚。
但是,问题在于,SignalProcessor和ByteFilter之间的调用混合了两种不同的概念(创建的概念和使用的概念)。SignalProcessor掌控了HiPassFilter这个类的创建,同时它也使用这个实例来工作。
这看起来好象微不足道,并且实际中也会经常出现。但是,让我思考一下这两个职责,使用对象和制造对象,把它们看做相对独立的事情,并审视它们之间建立的藕合。
关于对象使用的观点
一个客户对象要使用服务对象,要通过服务对象提供的公共方法。如果服务对象被引用为“Object”这种类型,那么就只有一些通用方法可以调用。所以,如果客户对象要使用服务对象,就要满足以下三个条件之一。
l 知道这个服务对象的实际类型
l 知道这个服务对象实现的某一个接口
l 知道这个服务对象继承树上端的一个基类
为了尽可能的降低藕合,我们倾向于满足后两个条件之一,这样,将来根据需要,可以改变实际被使用的客户对象,而不需要改变客户对象的代码。
换句话说,理想情况下,客户对象应该依赖一个抽象,而不是具体现存的一个类,这样将来就可以自由的添加不同的服务类,而不需要维护客户类的代码。特别在服务类被大范围使用的情况下,这种设计原则显示犹为重要。
对象创建的观点
显而易见地,如果我们要避免客户对象知道Bytefilters的具体的实现和这个对象如何创建,这就是意味着,就要有另一个东西,在另一个地方知道这些信息。
因此,我提出一种独特的概念。使用和创建。同样的,客户对象不涉及对象的创建,对象的创建者也不会涉及对象的使用。我们通常称这种“创建者”为工厂类。
这种设计就意味着下面这种设计模型。
需要认真考虑的是创建和使用之间最自然的藕合,做到这一点,任何东西发生变化时都不会提心吊胆地去维护。
如果把ByteFilter的抽象和两个具体实现看作“多态性服务”(ByteFilter是一个服务类,但它有两个版本,而通过多态性这种机制来实现不同版之间的变化),而SignalProcessor这个类只以使用的视角关注这个服务,ByteFilterFactory则是负责不同服务类的创建。
而ByteFilter这个抽象类型(本质上也只是一种抽象)有一些做为接口的公共方法,这个抽象类型是SignalProcessor类和ByteFilter这个多态性服务之间的藕合。而SignalProcessor和两个具体类之间不存在任何的藕合,但前提是我们是一个好的OO程序员,不会给接口随意的添加方法。
ByteFilterFactory和ByteFilter多态性服务之间的藕合就是另一种情况了。这个工厂类和子类建立藕合,因为它必须通过“new”关键字来创建实例。因为这个工厂类很自然地和构造器之间存在藕合,同时也和ByteFilter存在藕合(在把值返回给SignalProcessor之前,需要创建这个类型的引用),但是工厂类却不关心ByteFilter的公共方法,因为工厂类的概念应该只是创建。工厂类创建对象,但绝不调用对象的方法。
这一切带来的结果就是,当客户对象或是工厂类需要改变时,维护工作的痛苦会减少很多。
如果具体的子类要改变,例如ByteFilter需要添加或移除不同的实现,或者某个实现的业务规则发生改变。同时,只要维护一下ByteFilterFactory的代码。而不需要影响SignalProcessor。
如果ByteFilter的接口发生改变,添加、移除或改变公共方法,然后就需要修改一下SignalProcessor,但不会影响ByteFilterFactory。
我们非常感兴趣一个问题,客户对象和工厂类之间有一个很薄弱的环节,那就是ByteFilter这个抽象本身,而不是它的接口。要特别强调这样一个事实,很多资深的设计员都认为正确的抽象是OO设计的关键问题。即使接口有问题,也好过错误的抽象概念。
实际工作中的观点
这是否就意味着你的设计中的每一个类都应该有一个“工厂类”,而其它类都必须实例化这个工厂类?当然,如果问题很简单,没什么变数,例如一个普通排序类,那就是杀鸡用牛刀了。
但这就经常会有问题,我们永远不知道将来会发生什么样的变化。很不幸,我们预知变化的能力历来都不高。有一个折衷的办法是:把构造器封装在自己的类里面。
只要简单地把构造器声明为private或是protected,然后添加一个静态方法,这静态方法返回一个实例。代码片段如下:
这里关键的不同在于,“封装了构造器”,通常客户对象的代码是这样写的“mySender = Sender.getInstance();”,而不是“mySender = new Sender();”。通过把构造器声明为private可以屏蔽后一种做法。
初一看,这好象没有什么意义,和普通做的写法没有什么不同。但是,这样我们封装了“new”这个操作符。在C#或是JAVA这样语言中,“new”是不能被重载的,且我们不能控制它的返回类型,你通过“new”后面跟一个类名来指定一个类型,而它总是返回这个类型。而getInstance方法就不同了,它能返回其它类型作为返回值。当Sender需要做一点改变时,这种价值就很明显了。我们可以把Sender变成一种多态性服务。
这里主要的好处就是,当变化来临的时候,客户对象不需要改变。当有很多客户对象使用这个服务时,这就更有价值了,因此出于将来的维护的考虑,这么作是非常有必要的。
但是这好象违反了概念划分的原则吧?毕竟,这里Sender即是概念抽象(抽象类)同时又是具体实现(实现了工厂类)。是的,在眼前看来,我们有时要更实用主义一些,象这样允许一些不可预料的变化。而不是为了所有可能的变化放置一些不必要的代码在里面。
在这个例子中,Sender的方法getInstance()作了一个简单的决策(决定那个子类被创建),并且可能一直都是这么简单。如果问题变复杂,我们将会分离出工厂类来负责创建Sender的子类。这是否意味着需要修改客户对象呢,把调用静态方法改为调整工厂类?完全不用,我们可以做一个这样的委托。
当然,这是个不错的选择。如果客户对象很少,并且有些时间来重构,我们也可以把客户对象改为直接调用工厂类。但这并不是必要的,而且有点钻牛角尖了。你有没注意到,SenderFactory的构造器也被封装了,Sender调用静态方法getInstance来创建SenderFacroty实例。也许有一天工厂类也会变成多态性服务,所以,把它也封装起来会更好吧!
另外,把工厂类做成Singleton(一种设计模式,它能保证一个类只有一个实例,并通过一个全局的访问点访问这个实例)是很常见的。在重构代码时,只需举手之劳就能把构造的过程封装成Singleton。
结论
在设计中,实体这个概念有不同的视角,每个视角表现为某一类操作的内聚。而内聚为认是设计的传统美德,因为强内聚的实体容易被理解,更少的藕合,更好的粒度,更容易测试。
如果我们努力使实体的操作呈现出更清晰的视角,我们就改进了内聚性,包括状态的内聚、功能的内聚、职责的内聚,这将给我们带来很多的好处。
划分使用视角和创建视角可以有效的提升内聚性。这么作也意味有一个内聚的实体负责创建工作,一般是工厂类,同样的,我们也不需要关心它如果和使用相结合,最后实现整体的功能。
实际上,在我们设计中,掌握这些原则意味着用各种使用关系来确定各种工厂类,工厂类则专注于实例的创建(可以参考一些知名的创建模式)
在《设计模式精解》一书中,Alan Shalloway and James Trott描述了这样一种观点“根据上下文设计”,其中,他们说,一个设计的某些方面形成了一段上下文,通过这样的上下文可以推导出设计的其它部分。这是一个比较宏观的概念,这个概念意味着模式在设计、分析和实现中所扮演的角色,不过论题的关键是这样的:
随后,他们总结一个更有力的设计原则:
划分创建和使用,就是有效的支持了这一原则,因此也带来了很多优势。系统的内聚性会更强,可扩展性和灵活性会更好,维护将会变的相当的简单。
经常会发现一项好的技术会改进另一项好的技术,形成强强联手。
例如,测试驱动开发期望编写的类有更好的可测试性(通过前期关注测试),这也意味更强的内聚性,更好的粒度,等等。。。分离使用和创建的原则可以带来更优的可测试性,你可以分别测试它们,你可以放入一个假的对象来消除依赖性,使测试工作更容易。
设计模式也是这种观点的支持者。其中最值得称道的是“通过抽象隐藏变化”和“开放封闭原则”这些都给我们带来了很多改善。
然而,使用对象不需要知道对象的类型(知道的仅仅是抽象),然后另外再关注创建的问题。这就产生了工厂类,这就很自然把客户对象从复杂问题中分离出来。
我们正在建立自己的专业特点,我们搜寻各种有价值的特性、原则、实践方法和模式。以些形成我们智慧的基础。
寻找这些思想,和我们现有的思想集成,我们将更成功。给我们的客户、公司文化和经济方面都能增添价值。这也能帮助我们更好的沟通和协作,建立一个鼓励创新的团队气氛。
最后,我们所作的工作会更有趣。我相信这是一条正确的道路。