5.1 多态的概念 面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。 多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用) 实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。 多态的作用:消除类型之间的耦合关系。 现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。 下面是多态存在的三个必要条件,要求大家做梦时都能背出来!
5.2 多态存在的三个必要条件 一、要有继承; 二、要有重写; 三、父类引用指向子类对象。
5.3 TestPolymoph.as —— 多态的应用,体会多态带来的好处 package { public class TestPolymoph { public function TestPolymoph() { var cat:Cat = new Cat("MiMi"); var lily:Lady = new Lady(cat);
// var dog:Dog = new Dog("DouDou"); // var lucy:Lady = new Lady(dog);
lady.myPetEnjoy(); } } }
class Animal { private var name:String; function Animal(name:String) { this.name = name; } public function enjoy():void { trace("call..."); } }
class Cat extends Animal { function Cat(name:String) { super(name); } override public function enjoy():void { trace("Miao Miao..."); } }
class Dog extends Animal { function Dog(name:String) { super(name); } override public function enjoy():void { trace("Wang Wang..."); } }
// 假设又添加了一个新的类 Bird class Bird extends Animal { function Bird(name:String) { super(name); } override public function enjoy():void { trace("JiJi ZhaZha"); } }
class Lady { private var pet:Animal; function Lady(pet:Animal) { this.pet = pet; } public function myPetEnjoy():void { // 试想如果没有多态 //if (pet is Cat) { Cat.enjoy() } //if (pet is Dog) { Dog.enjoy() } //if (pet is Bird) { Bird.enjoy() } pet.enjoy(); } } 首先,定义 Animal 类包括:一个 name 属性(动物的名字),一个 enjoy() 方法(小动物玩儿高兴了就会叫)。接下来,定义 Cat, Dog 类它们都继承了 Animal 这个类,通过在构造函数中调用父类的构造函数可以设置 name 这个属性。猫应该是“喵喵”叫的,因此对于父类的 enjoy() 方法进行重写(override),打印出的叫声为 “Miao Maio…”。Dog 也是如此,重写 enjoy 方法,叫声为 “Wang Wang…”。 再定义一个 Lady 类,设置一个情节:假设这个 Lady 是一个小女孩儿,她可以去养一只宠物,这个小动物可能是 Cat, Dog,或是 Animal 的子类。在 Lady 类中设计一个成员变量 pet,存放着宠物的引用。具体是哪类动物不清楚,但肯定是 Animal 的子类,因此 pet 的类型为 Animal,即 pet:Animal。注意这是父类引用,用它来指向子类对象。 最后在 Lady 类里面有一个成员函数 myPetEnjoy(),这个方法中只有一句 pet.enjoy(),调用 pet 的 enjoy() 方法。 现在来看测试类。new 出来一只 Cat,new 出来一个 Lady,将 Cat 的对象传给 Lady。现在 Lady 中的成员变量应该是 pet:Animal = new Cat(“MiMi”)。下面,调用 lady.myPetEnjoy() 方法,实际就是在调用 pet.enjoy(),打印出 Miao Miao。pet 的类型明明是 Animal,但被调的方法却是 Cat 的 enjoy(),而非 Animal 的 enjoy(),这就叫动态绑定——“在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法”。 想象一下,如果没有多态的话,myPetEnjoy() 中方法可能要做这样的一些判断: if (pet is Cat) { new Cat(“c”).enjoy() } if (pet is Dog) { new Dog(“d”).enjoy() } 判断如果 pet 是Cat 类型的话调用 new Cat().enjoy(),如果是 Dog 的话调用 new Dog().enjoy()。假设有一天我要传入一个 Bird,那还得手动加上: if (pet is Bird) { new Bird (“b”).enjoy() } 新加入什么类型的都要重新修改这个方法,这样的程序可扩展性差。但是现在我们运用了多态,可以随意地加入任何类型的对象,只要是 Animal 的子类就可以。例如,var lily:Lady = new Lady(new Bird(“dudu”)),直接添加进去就可以了,不需要修改其它任何地方。这样就大大提升的代码的可扩展性,通过这个例子好好体会一下多态带来的好处。 最后再补充一点,在使用父类引用指向子类对象时,父类型的对象只能调用是在父类中定义的,如果子类有新的方法,对于父类来说是看不到的。拿我们这个例子来说,如果 Animal 类不变,在 Cat 和 Dog 中都新定义出一个 run() 方法,这个方法是父类中没有的。那么这时要使用父类型的对象去调用子类新添加的方法就不行了。 下面看一个这个例子的内存图。
5.4 TestPolymoph 内存分析 在内存中,一个个方法就是一段段代码,因此它们被存放在代码段中。上例中的 pet 是 Animal 类型的成员变量,但是它指向的是一个 Cat 类型的具体对象,同时 Cat 又是它的子类,并且重写了 enjoy() 方法,满足了多态存在的三个必要条件。那么当调用 pet.enjoy() 的时候,调用的就是实际对象 Cat 的 enjoy() 方法,而非引用类型 Animal 的 enjoy() 方法。
5.5 多态的好处 多态提升了代码的可扩展性,我们可以在少量修改甚至不修改原有代码的基础上,轻松加入新的功能,使代码更加健壮,易于维护。 在设计模式中对于多态的应用比比皆是,面向对象设计(OOD)中有一个最根本的原则叫做“开放 – 关闭”原则(Open-Closed Principle OCP),意思是指对添加开放,对修改关闭。看看上面的例子,运用了多态以后我们要添加一个 Bird 只需要再写一个 Bird 类,让它继承自 Animal,然后 new 出来一个对象把它传给 lily 即可。 我们所做的就是添加新的类,而对原来的结构没有做任何的修改,这样代码的可扩展性就非常好了!因为我们遵循了“开放-关闭”原则 —— 添加而不是修改。 前面这个例子中还有一个地方需要说明,Animal 这个类,实际上应该定义为一个抽象类,里面的 enjoy() 方法,事实上不需要实现,也没法实现。想一想,Animal 的叫声?!你能想象出 Animal 是怎么叫的吗?显然,这个方法应该定义为一个抽象方法,留给它的子类去实现,它自己不需要实现,那么一旦这个类中有一个方法抽象的,那么这个类就应该定义为抽象类。但是很遗憾 AS 3 不支持抽象类,因为它没有 abstract 关键字。但是抽象类也是一个比较重要的概念,因此下面还要给大家补充一下。
5.6 抽象类的概念 一个类如果只声明方法而没有方法的实现,则称为抽象类。 含有抽象方法的类必须被声明为抽象类,抽象类必须被继承,抽象方法必须被重写。如果重写不了,应该声明自己为抽象。 抽象类不能被实例化。 抽象方法只需声明,而不需实现。 ActionScript 3.0 不支持抽象类(abstract),以后肯定会支持的,相信我,那只是时间问题。因此这里只介绍一下抽象类的概念。
5.7 对象转型(Casting) 一个基类类型变量可以“指向”其子类的对象。 一个基类的引用不可以访问其子类对象新增加的成员(属性和方法)。 可以使用“变量 is 类名”来判断该引用型变量所“指向”的对象是否属于该类或该类的子类。 子类的对象可以当作基类的对象来使用称作向上转型(upcasting),反之称为向下转型(downcasting)。 每说到转型,就不得不提到“里氏代换原则(LSP)”。里氏代换原则说,任何基类可以出现的地方,子类一定可以出现。里氏代换原则是对“开放—关闭”原则的补充。 里氏代换原则准确的描述:在一个程序中,将所有类型为 A 的对象都转型为 B 的对象,而程序的行为没有变化,那么类型 B 是类型 A 的子类型。 比如,假设有两个类:Base 和 Extender,其中 Extender 是 Base 的子类。如果一个方法可以接受基类对象 b 的话: method(b:Base) 那么它必然可以接受一个子类对象 e,即有 method(e)。注意,里氏代换原则反过来不能成立。使用子类对象的地方,不一定能替换成父类对象。 向上转型是安全的,可以放心去做。但是在做向下转型,并且对象的具体类型不明确时通常需要用 instanceof 判断类型。下面看一个例子 TestPolymoph.as: package { public class TestCast { public function TestCast() { // -------------- UpCasting -------------- var cat:Cat = new Cat(); var dog:Dog = new Dog(); var animal:Animal = Animal(cat); animal.call(); animal.sleep(); //animal.eat(); // 不能调用父类中没有定义的方法 // ------------- DownCasting ------------- if (animal is Cat) { cat = Cat(animal); cat.eat(); } else if (animal is Dog) { dog = Dog(animal); dog.eat(); } } } }
class Animal { public function call():void{}; public function sleep():void{}; } class Cat extends Animal { override public function call():void { trace("Cat Call"); } override public function sleep():void { trace("Cat Sleep"); } public function eat():void { trace("Cat Eat"); } }
class Dog extends Animal { override public function call():void { trace("Dog Call"); } override public function sleep():void { trace("Dog Sleep"); } public function eat():void { trace("Dog Eat"); } } 首先创建 Animal 类,定义两个方法 call() 和 sleep(),它的子类 Cat 和 Dog 分别重写了这两个方法,并且都扩展了出了一个新的方法 eat()。 来看测试类,new 出来一个 cat,再将它向上转型 animal:Animal = Animal(cat)。由于向上转型是安全的,所以这样做没有问题,但是当它转型成了父类对象后,就不能再调用 eat() 方法了,因为在父类中只有call() 和 sleep() 方法,父类对象不能调用子类扩展出的新方法。 接下来一段代码是在进行向下转型,animal 这个对象可以是一个放一个 dog 也可以放一个 cat,当这两种情况都有可能时,进行向下转型就要判断一下当然对象到底是哪个类型的,使用“is”进行判断,看看该对象是不是一个 Cat 或 Dog,如果是 Cat 就将它向下转型为一个 Cat,这样就可以安全地调用 Cat 的 eat() 方法了。 最后再举一个现实中的例子 TestEventCast.as : package { import flash.display.Sprite; import flash.events.Event; public class TestEventCast extends Sprite { public function TestEventCast() { var ball:Sprite = new Sprite(); ball.graphics.beginFill(0xff0000); ball.graphics.drawCircle(0,0,50); ball.graphics.endFill(); ball.y = 150; ball.x = 150; addChild(ball); ball.addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(evt:Event):void { // evt.target 是 Object 类型的,需要转型成为实际类型才能使用 x 属性 var ball:Sprite = Sprite(evt.target); ball.x += 5; } } } 构造函数中创建一个 Sprite 类的对象,并在里面绘制一个圆,加入 ENTER_FRAME 侦听,在 onEnterFrame 函数中,var ball:Sprite = Sprite(evt.target) 这里我们必须做向上转型,如果不做的话系统会报错,为什么呢? 查看一下帮助文档,Event 类 target 属性的实现:public function get target():Object。这是一个只读属性,它返回的是一个 Object 类型的对象。由于 AS 3 是单根继承的,因此任何一个对象都可以向上转型成 Object 类型的。因此每次要拿到这个 evt.target 的时候都要将它向下转型成为该对象的实际类型才能放心使用。
|