对象设计中创建VS使用

日期: 2008-06-04 来源:TechTarget中国

  背景


  这篇文章是我在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”这种类型,那么就只有一些通用方法可以调用。所以,如果客户对象要使用服务对象,就要满足以下三个条件之一。


  知道这个服务对象的实际类型


  知道这个服务对象实现的某一个接口


  知道这个服务对象继承树上端的一个基类


  为了尽可能的降低藕合,我们倾向于满足后两个条件之一,这样,将来根据需要,可以改变实际被使用的客户对象,而不需要改变客户对象的代码。


  换句话说,理想情况下,客户对象应该依赖一个抽象,而不是具体现存的一个类,这样将来就可以自由的添加不同的服务类,而不需要维护客户类的代码。特别在服务类被大范围使用的情况下,这种设计原则显示犹为重要。


  对象创建的观点


  显而易见地,如果我们要避免客户对象知道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描述了这样一种观点“根据上下文设计”,其中,他们说,一个设计的某些方面形成了一段上下文,通过这样的上下文可以推导出设计的其它部分。这是一个比较宏观的概念,这个概念意味着模式在设计、分析和实现中所扮演的角色,不过论题的关键是这样的:



  随后,他们总结一个更有力的设计原则:



  划分创建和使用,就是有效的支持了这一原则,因此也带来了很多优势。系统的内聚性会更强,可扩展性和灵活性会更好,维护将会变的相当的简单。


  经常会发现一项好的技术会改进另一项好的技术,形成强强联手。


  例如,测试驱动开发期望编写的类有更好的可测试性(通过前期关注测试),这也意味更强的内聚性,更好的粒度,等等。。。分离使用和创建的原则可以带来更优的可测试性,你可以分别测试它们,你可以放入一个假的对象来消除依赖性,使测试工作更容易。


  设计模式也是这种观点的支持者。其中最值得称道的是“通过抽象隐藏变化”和“开放封闭原则”这些都给我们带来了很多改善。


  然而,使用对象不需要知道对象的类型(知道的仅仅是抽象),然后另外再关注创建的问题。这就产生了工厂类,这就很自然把客户对象从复杂问题中分离出来。


  我们正在建立自己的专业特点,我们搜寻各种有价值的特性、原则、实践方法和模式。以些形成我们智慧的基础。


  寻找这些思想,和我们现有的思想集成,我们将更成功。给我们的客户、公司文化和经济方面都能增添价值。这也能帮助我们更好的沟通和协作,建立一个鼓励创新的团队气氛。

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐

  • 细说SCA V1.0规范之component与实现

    在SCAV1.0装配规范中,相对较为独立的组件只有三个,粒度由小到大分别是Component、Composite和Domain,其他Service、Reference、Property、Wire等都是辅助性的元素……

  • 调整合适的Web服务粒度

    我读过很多关于Web服务方面的参考文献,文章中都强调设计服务的粒度是如何的重要,确实是这样的。但是实际上,针对如何使粒度合适,这些参考文献并没有提出……

  • 解决服务粒度挑战 构建正确服务组合

    解决服务粒度挑战构建正确服务组合,最近的一些对话都集中于如何构建”正确的” 服务。关于这个问题的一个关键答案是确信我们构建的服务是以合适的粒度水平……

  • 设计更好的 SOA

    面向服务的架构( Service-Oriented Architecture , SOA) 在概念上是简单的,但是它的价值还不是很明显,在分布式企业中实现共享服务基础架构仍然十分困难。构建正确的架构和采用实现共享服务前景所需的最佳实践可以使过渡变得容易,并有助于确保短期和长期内的成功。