1.
Welcome to Design Patterns -设计模式介绍
现在我们要住进对象村(Objectville),我们刚刚开始接触设计模式…每个人都在使用它们。一会我们将去参加Jim和Betty的星期三晚上的模式聚会!
有人已经解决了你的问题。在这章里,你将学习到为什么(和怎么样),你将学习到那些幸存下来的开发者的智慧和教训,他们都曾经历过类似的设计问题。在我们做之前,我们将先看看设计模式的用途和好处,再看一些关键的面向对象设计原则,并且再通过一个实例了解模式的工作方式。使用模式最好的方法就是把它们装入脑袋里,然后在你设计和现有的应用程序里找到你能够应用它们的地方。相对于代码重用,使用模式你获得了经验的重用。
从一个简单的模拟鸭子程序开始
乔为一个制造非常成功的模拟鸭子池塘游戏(SimUDuck)的公司工作。这个游戏可以展示很多种鸭子的游泳方式和呷呷叫声。系统最初的设计者们使用了标准的面向对象技术,他们创建了一个Duck基类供所有其它类型的鸭子继承。
去年,竞争者们给公司带来了越来越多的压力。经过历时一周在高尔夫球赛场上的集体讨论,公司的经理们都觉得该是进行一次大改革的时候了。他们需要在下周在毛伊岛举行的股东大会上展示一些真正给人深刻印象的东西。
但是我们现在需要鸭子可以飞
经理们确定会飞的鸭子就是模拟器需要的用来击败其他竞争者的东西。当然,乔的经理告诉他们,对于乔来说在一周内迅速搞定这些根本不是问题。“毕竟”,乔的上司说,“他是一个面向对象的程序员…那些有什么难的呢?”
乔想:我仅仅只需要在Duck类里增加fly()方法,然后所有其他鸭子就都可以继承它了。现在是展示我真正的面向对象才华的时候了。
但是有些事情严重错误了…
乔的上司:乔,我正在股东大会上。他们刚看完演示,很多橡皮鸭子在屏幕上四处乱飞。这是你在开玩笑吗?…
发生了什么事?
乔没有注意到并不是所有Duck的子类都需要fly()方法。当乔给Duck基类增加新行为的时候,他也同时给那些不需要这些行为的Duck的子类增加了。现在他的SimUDuck程序里有了会飞的不存在的东西。
局部的代码更新导致了非局部的效果(会飞的橡皮鸭子)!
乔想:好吧,我的设计有一点小缺陷。我不明白为什么他们不能只部分调用它。…
他在想为什么在维护系统的时候无法使用继承来实现重用的目的。
乔在考虑继承…
乔想:我可以总是在橡皮鸭子里覆盖fly()方法,同样的方式对于quack()方法…
乔想:但是当我们在系统里增加木头鸭子的时候会怎么样呢?它们既不会飞也不会呷呷叫…
下面那些是使用继承来给Duck增加行为的不利条件?(可多选)
A. 代码在子类间被复制
B. 很难在运行时改变行为
C. 我们不能让鸭子跳舞
D. 很难得到所有鸭子的行为
E. 鸭子不能在同一时间飞和呷呷叫
F. 变动会无意间影响其他鸭子
利用接口(interface)怎么样?
乔认识到继承或许并不是办法,因为他刚得到消息说经理们现在想每六个月更新一次产品(他们还没有想好更新什么)。乔知道那样意味着不断变化,并且他将被迫检查所有将来增加到程序里的Duck的子类,可能还要覆盖它们的fly()方法和quack()方法。
乔想:我可以把fly()从Duck基类里拿出来,然后创建一个有fly()方法的Flyable()接口。这样,只有那些需要飞的鸭子才会通过实现这个接口来获得fly()方法…并且,我想最好再创建一个Quackable接口,因为并不是所有的鸭子都会呷呷叫。
你觉得这个设计怎么样?
如果你是乔,你会怎么办?
乔的经理:这是你提出过的最糟糕的主意。你怎么能说“复制代码”呢?如果你能想到被迫覆盖一些方法是不好的,那么你为什么不考虑一下当你需要对飞行行为做一点小的改动的时候会怎么样
… 对于所有48个能够飞行的Duck的子类来说?!
我们知道并不是所有的子类都有飞行和呷呷叫的行为,所以继承不是正确的方法。但是,尽管让子类实现Flyable或者Quackable解决了部分问题(不会再有会飞的橡皮鸭子),但是这种方法彻底破坏了行为的重用,所以它只是制造了另一个维护上的噩梦。当然,鸭子可能有一种以上的飞行行为…
此刻你可能正等待着有一个设计模式骑着白马来拯救世界。但是,那会是什么?不,我们将使用传统的方式来找到解决方案
– 使用优秀的面向对象软件设计原则
美女在想:如果一种软件开发方法,使我们可以用一种对现有代码影响最少的方式来修改软件,那不是在做梦吧?那样我们就可以花很少的时间来修改软件而有更多的时间给程序增加更酷的功能…
软件开发的一个不变的真理
好的,什么是你在软件开发中经常要注意的事情?不论你在那工作,你在建造什么,或者你在使用什么开发语言,什么东西一直在伴随着你?
不论你设计的程序有多好,随着时间的推移,一个程序都会改变的,或者它会死掉。
很多事情都会促使变化。在列表上写出一些你认为不得不改变你程序的原因(我们写下了一些我们的原因给你开个头)。
·我们的客户或使用者决定他们需要其他一些东西,或者他们想要新的功能。
·我的公司决定跟另一个资源库供应商合作,而他也是从其他供应商那里购买使用不同格式的数据。
问题零位调整…
我们知道使用继承并不能很好的解决问题,因为鸭子的行为在子类里持续不断地改变,并且让所有的子类都拥有基类的行为是不适当的。Flyable和Quackable接口开始听起来挺有希望的
– 只有那些真正会飞的鸭子才会实现Flyable接口,等等 –只是Java的接口里不能有实现代码,所以就没有代码重用。那意味着不论何时你需要修改一个行为,你就被迫要在所有定义了这个行为的不同子类里循环修改它,这种方法可能会引入新的错误!
设计原则 识别你的应用程序里不稳定的部分,并且把它们与稳定的部分隔离开。
换句话说,如果你已经发现你的代码的某部分正在改变,考虑每个新需求,然后你就知道你发现了一个需要同所有稳定部分隔离开的行为。
从另一个角度考虑这个原则:找到变化并且封装起来,稍后你就可以在不影响其他部分的情况下修改或扩展封装的变化部分。
尽管概念很简单,但是它几乎是所有设计模式的基础。所有模式都提供了使系统里变化的部分独立于其它部分的方法。
好的,是把鸭子的行为从Duck类里拖出来的时候了!
拿走变化部分并封装它,所以它将不会影响你代码的休息。
这样做的结果?使你的系统只会因代码改变带来的少许影响而有更大的灵活性。
分开变动和不变动的部分
我们从那里开始?在我们知道的范围内,除了fly()和quack()的问题,Duck类工作的很好,它没有其他表现出要变化或频繁改变的地方。所以,除了一些微小的改变,我们很恰当的留下Duck类自己。
现在,从那些保持不变的部分分离出变化的部分,我们将创建两组类(完全从Duck分离出来),一组给飞行一组给呷呷叫。每组类都将拥有它们各自行为的实现。例如,我们可能有一个实现呷呷叫的类,另一个实现吱吱叫的类,还有一个实现保持沉默的类。
我们知道fly()和quack()都是Duck类在鸭子里不断变化的部分。
通过从Duck类分离出这些行为,我们将把两个方法都从Duck类里拖出来并且创建一组新的类来表示每个行为。
设计鸭子的行为
那么,我们将怎么样设计那组实现了飞行和呷呷叫行为的类呢?
我们愿意保持事物的灵活性;毕竟,那些行为首先在鸭子的行为里是不灵活的,给我们带来了麻烦。我们知道我们想给Duck的实例分配行为。例如,我们可能想要实例化一个新的MallardDuck(野鸭)实例并且给它初始化一个特殊类型的飞行行为。那么,如果我们那么做了,为什么不确信我们可以动态地改变一个鸭子的行为呢?换句话说,我们将在Duck类里包含行为设置方法,所以我们可以说在运行时改变MallardDuck的飞行行为。
为了这些目标,让我们看看我们第二个设计原则:
设计原则 面向接口而不是面向实现编程。
我们将使用一个接口来表示每个行为 – 例如,FlyBehavior和QuackBehavior –每个行为的的执行者都将实现其中一个接口。所以现在是Duck类实现飞行和呷呷叫接口的时候了。另一种方式,我们可以制造一组完全为了表示行为而存在的类(例如,“squeaking”),并且使用行为类胜于使用Duck类来实现行为的接口。
这是跟我们之前做法大不相同的方式,行为不是来自于一个基类的具体实现,而是在子类自身提供了专门实现。在两个例子里我们都依赖于一个实现者。我们受制于使用特殊的实现者并且没有机会改变行为(除了写更多的代码)。
在我们的新设计里,Duck基类将使用一个接口(FlyBehavior和QuackBehavior)来表示行为,所以行为(换句话说,具体行为是编写在实现了FlyBehavior接口和QuackBehavior接口的类里)的实际实现者将不再受制于Duck基类。
从现在开始,Duck类的行为将实现在一个单独了类里 - 一个实现了特殊行为接口的类。
另一个方面,Duck类不再需要知道太多关于它们自己实现的细节。
女侠:我不明白你为什么非要使用FlyBehavior接口呢?你可以使用一个抽象类来做同样的事。难道所有的目的不都是为了使用多态吗?
“面向接口编程”等价于“面向基类编程”
接口(interface)这个词在这里被赋予了太多的意思。这里有一个接口的概念,但是还有Java里的接口结构。你可以面向接口编程,并不需要使用Java里的接口。要点是在面向基类编程的时候要使用多态,使得在实际运行时的对象不受制于代码。我们可以改述“面向基类编程”为“声明基类类型的变量,通常是一个抽象类或接口,赋值给变量的对象可以是任何基类的具体实现,意思就是申明它们的类并不是必须知道实际对象的具体类型是什么!”
这对你来说也许不是什么新鲜事,但是却明确了我们说的是同一件事,这里有一个使用多态的简单例子 – 设想有一个抽象类Animal(动物),它有两个具体实现,Dog(狗)和Cat(猫)。
面向实现编程将会:
Dog d = new Dog();
d.bark();
声明一个Dog类型的变量“d”(一个Animal的具体实现),强制我们面向一个具体的实现编码
但是,面向接口或基类编程将会:
Animal animal = new Dog();
animal.makeSound();
我们知道它是一条狗,但是我们现在可以使用animal变量引用更多的东西
还有更好的,比起在子类的实例里硬编码,在运行时分配具体实现对象的方法更好。
A = getAnimal();
A.makeSound();
我们不知道实际的animal子类型是什么…所有我们关心的只是怎么样调用makeSound()方法。
实现Duck的行为
这里我们有两个接口,FlyBehavior和QuackBehavior连同相应实现了每一个具体行为的类。
使用这个设计,其它类型的对象也都可以重用我们的飞行和呷呷叫行为,因为这些行为不再隐藏在我们的Duck类里了!
并且我们可以在不修改任何我们已有的行为类和不接触任何使用类飞行行为的Duch类的情况下增加新的行为。
所以我们在没有任何继承的负担的情况下获得了重用的好处。
(没有蠢问题)
Q:我需要总是先实现我的应用程序,看看那里会变化,然后在返回来分离并且封装那些变化吗?
A:不总需要;通常当你设计系统的时候,你会预见到那些可能会变化的地方,然后你会在代码里灵活的处理它。你将会发现原则和模式可以在开发生命周期的很多地方得到应用。
Q:我们将会把Duck也制造成接口吗?
A:在这里不会。就象你将看到的一样,我们已经把所有的东西都挂在了一起,我们通过持有具体类和特殊类,就象MallardDuck类一样,继承公共的属性和方法,得到了很多好处。现在我们已经从Duck的继承结构中移除了变化的部分,这个结构没有其他问题了。
Q:那些只有一个行为的类总让人感觉有些怪异。难道类不是用来表示事物的吗?难道类不会有既有状态又有行为吗?
A:在一个面向对象系统里,是的,类表示的事物一般都是既有状态(实例变量)又有方法的。而在这个例子里,事物恰巧只有一个行为。但是,甚至一个行为也可以同时有状态和方法;一个飞行行为可能会有一个表示飞行行为属性(每秒煽动翅膀的次数,最大高度和速度,等等)的实例变量。
①使用你的新设计,如果你需要增加象火箭一样飞行的行为到SimUDuck系统中,你将怎么做?
②你能想到一个不是鸭子还可能想要使用呷呷叫行为的类吗?
整合Duck的行为
关键是现在Duck类把它的飞行和呷呷叫行为委托出去,来替代定义在Duck类(或者其子类)中的呷呷叫和飞行方法。
① 首先我们将在Duck类里增加两个叫做为flyBehavior和quackBehavior的实例变量,它们都声明为接口类型(而不是一个具体的类实现类型)。每个鸭子对象都将会使用各种方式来设置这些变量,以引用它们期望的运行时的特殊行为类型(使用翅膀飞行,吱吱叫,等等)。
我们还将把fly()方法和quack()方法从Duck类(或者其子类)里移除,因为我们已经把这些行为移到FlyBehavior和QuackBehavior类里了。
我们将使用两个相似的performFly()和performQuack()方法来替换fly()和qucak()方法;你将看到它们接下来是怎么工作的。
② 现在我们实现performQuack()方法:
很简单,哈!为了执行呷呷叫行为,一个Duck只要允许一个通过quackBehavior变量引用的对象给它提供呷呷叫行为就可以了。
在这部分代码里,我们不关心哪个对象是什么类型的,我们只关心它知道怎样呷呷叫就行了!
③ 好的,该是思考怎么设置flyBehavior和quackbehavior实例变量的时候了。让我们看看MallardDuck类:
这样看来,MallardDuck(野鸭)的呷呷叫行为是货真价实的活鸭子的呷呷叫,不是吱吱叫声也不是没声的。那么这里发生了什么呢?当MallardDuck实例化的时候,它的构造函数把它继承的quackBehavior实例变量初始化为一个Qucak类型的实例(一个QuackBehavior接口的具体实现类)。
鸭子的飞行行为也是一样的 – MallardDuck的构造函数把flyBehavior实例变量初始化为一个FlyWithWings类型的实例(一个FlyBehavior接口的具体实现类)。
女侠:等一下,你不是说我们将不对具体实现编程吗?但是我们在那个构造函数里做的什么呢?我们正在制造一个具体的Quack实现类的实例。
这个问题问得好,我们确实暂时是那样做了…
在本书后面的章节里,我们的工具箱里将会有更多的模式帮我们解决这个问题。
尽管如此,可以发现在我们设置行为到具体类(通过实例化一个象Quack或FlyWithWings一样的行为类,并且把它分配给我们的行为引用变量)的时候,我们可以很容易的在运行时改变它。
所以,这样做我们仍然有很大的灵活性,但是我们正在使用一个灵活的方法做一个乏味的初始化实例变量的工作。那么,考虑一下,因为quackBehavior是一个接口类型的实例变量,所以我们能够(通过多态的魔力)在运行时动态地分配一个QuackBehavior的具体实现给它。
花一些时间想一想,你将怎样实现一个可以在运行时改变的鸭子。(从现在开始,在接下来几页里,你将看到这样做的代码)
测试鸭子的代码
① 敲入并且编译下面的Duck类(Duck.java),和两页后面的MallardDuck类(MallardDuck.java)。
@2敲入并且编译FlyBehavior接口(FlyBehavior.java)和两个具体行为实现类(FlyWithWings.java和FlyNoWay.java)
③ 敲入并且编译QuackBehavior接口(QuackBehavior.java)和它的三个具体实现类(Quack.java,MuteQuack.java,和Sqeak.java)。
④ 敲入并且编译测试类(MiniDuckSimulator.java)。
a. 这里调用了MallardDuck继承的performQuack()方法,然后再委托到对象里的QuackBehavior接口上(它调用从Duck继承的quackBehavior变量引用的对象的quack()方法)。
b. 然后我们使用MallardDuck继承的performFly()方法做同样的事。
⑤ 运行代码!
动态设置行为
有这么多可以动态构造我们的鸭子的好方法而不用是一件多么羞耻的事情。设想一下你想通过一个在鸭子子类里的设置方法来设置鸭子的行为类型,这么做胜于在鸭子的构造函数里初始化它。
①在Duck类里增加两个新方法
我们可以在任何想改变鸭子的行为的时候调用这些方法。
②制造一个新鸭子类型(ModelDuck.java)。
③制造一个新的飞行行为类型(FlyRocketPowered.java)。
哈哈,我们创建了一个有火箭一样速度的飞行行为。
④ 改变测试类,增加模型鸭子(ModelDuck),然后使模型鸭子有火箭的能力。
Before:第一次我们调用的performFly()方法是委托于在ModelDuck的构造函数设置的flybehavior对象,它是一个不会飞的实例。
After:这次调用的是model继承的行为色绘制方法,并且…瞧!模型鸭子突然有火箭一样飞行能力了!
如果这行运行,模型鸭子会动态改变它的飞行行为!如果具体实现运行在Duck类里,你就不能这么做了。
⑤ 运行它!
如果想在运行时改变鸭子的行为,只需要对要改变的行为调用鸭子的设置方法就可以了!
封状行为的大局观
好的,我们已经深入地研究过鸭子模拟器的设计了,现在是时候回来从更高的角度来看一看整个大局。
下面是整个的类结构。我们有所有你期待的东西:从Duck扩展的鸭子,实现了FlyBehavior接口的飞行行为和实现了QuackBehavior接口的呷呷叫行为。
同样注意我们开始用不同的方式描述事物。相对于把鸭子的行为想象成一组行为,我们将开始把它们想象成一个算法家族(family
of)。考虑这个问题:在SimUDuck系统的设计里,用来表示一个鸭子行为(通过不同发方式实现呷呷叫或飞行行为)的算法,我们可以同样仅仅对一组类使用同样的技术来实现计算不同州的销售税。
注意类之间的关系。事实上,我们抢了你的钢笔并且在类图里给关系(“是一个”,“有一个”和“实现”关系)画上了适当的箭头。
|