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