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.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 的时候都要将它向下转型成为该对象的实际类型才能放心使用。
6.3 实现接口的原则 在实现接口的类中,实现的方法必须(选自帮助文档): (1)使用 public 访问控制标识符。 (2)使用与接口方法相同的名称。 (3)拥有相同数量的参数,每一个参数的数据类型都要与接口方法参数的数据类型相匹配。 (4)使用相同的返回类型。
6.4 TestInterFaceAccess.as —— 实现多个接口 package { public class TestInterFaceAccess { public function TestInterFaceAccess() { var duck:Duck = new Duck(); duck.run(56); duck.fly(); } } }
interface Runnable { function run(meter:uint):void; }
interface Flyable { function fly():void; }
class Duck implements Runnable, Flyable { public function run(meter:uint):void { trace("I can run " + meter + " meters"); } public function fly():void { trace("I can fly"); } } 这个例子很简单,首先定义两个接口 Runnable 和 Flyable。Runnable 中定义了一个抽象的 run() 方法,Flyable 中定义了一个抽象的 fly() 方法。我们知道,接口是在定义标准,它自己不需要实现,具体的实现交给实现该接口的类去完成。 Duck 类实现(implements)了 Runnable 和 Flyable,因此它必需去实现这两个接口中定义的所有方法。并且方法名,参数类型,返回值类型要与接口中定义的完全一致,权限修饰符必需是 public。大家可以试一试其它的访问权限,例如,private, internal 看看能不能测试通过。结论是不能,请查看 6.3 节实现接口的原则。 其实这些结论大家通过动手实验就能得出结论。比如说如果实现了该接口的类的方法权限不是 public 或者方法返回值、参数类型、参数个数与接口中定义的不同,是否可以测试通过呢?如果在定义接口时在 function 前面加入了访问权限修饰符,可以不可以以呢?类似这些问题不需要查书或去问别人,自己动手做实验是最快最高效的学习方法,编译器会告诉你,行还是不行,直接问它就可以了!以上做法都行不通。 为了更好地保证接口的实现不出差错,通常最保险的做法就将该方法复制(ctrl + c)过来,并在前面加上 public,再去实现。
6.6 TestInterFacePoly.as —— 接口实现多态 package { public class TestInterFacePoly { public function TestInterFacePoly() { var cat:Cat = new Cat(); var duck:Duck = new Duck(); var racing:Racing = new Racing(cat); racing.go(); } } }
interface Runnable { function run():void; }
interface Swimmable { function swim():void; }
interface Flyable { function fly():void; }
class Cat implements Runnable, Swimmable { public function run():void { trace("Cat run"); } public function swim():void { trace("Cat swim"); } public function climb():void { trace("Cat Climb"); } }
class Duck implements Runnable, Flyable { public function run():void { trace("Duck run"); } public function fly():void { trace("Duck fly"); } }
6.7 策略模式(Strategy Pattern) 同样,不直接给出最终的答案,先看下面这个例子: package { public class TestStrategy { public function TestStrategy() { var rabbit:Rabbit = new Rabbit(); rabbit.run(); rabbit.jump(); } } }
interface Runnable { function run():void; }
interface Jumpable { function jump():void; }
class Rabbit implements Runnable, Jumpable { public function run():void { trace("I can run fast"); } public function jump():void { trace("I can jump 5m"); } } 这个例子很简单,让 Rabbit 实现 Runnable, Jumpable 接口,让它能跑能跳。 现在如果要让 Rabbit 跳不起来,那么就要修改它的 jump() 方法,打印出 “I can’t jump”。如果要让它能跑 1000 m,并且还能跨栏,那么还要修改 run() 方法的实现。还记得 OO 设计的最根本原则吗?“开放—关闭”原则 —— 对添加开放,对修改关闭。下面来看看策略模式是怎样做到“开放—关闭”原则的。以下是 TestStrategy.as: interface Runnable { function run():void; }
interface Jumpable { function jump():void; }
class FastRun implements Runnable { public function run():void { trace("I can run fast"); } }
class JumpHigh implements Jumpable { public function jump():void { trace("I can jump 5m"); } }
class JumpNoWay implements Jumpable { public function jump():void { trace("I can't jump"); } }
class Rabbit { var runBehavior:Runnable = new FastRun(); var jumpBehavior:Jumpable = new JumpHigh(); // new JumpNoWay(); public function run() { runBehavior.run(); } public function jump() { jumpBehavior.jump(); } } 现在 Rabbit 中加入了两个成员变量 runBehavior,jumpBehavior 分别是 Runnable 和 Jumpable 类型的引用,又是父类引用(接口)指向子类对象。Rabbit 的 run 和 jump 直接调用了 runBehavior.run() 和 jumpBehavior.jump()。而 runBehavior,jumpBehavior 指向的是两个实现了 Runnable 和 Jumpable 接口的类 FastRun 类和 JumpHigh 类。而在这两个类中分别实现了 Runnable 和 Jumpable 接口,run() 和 jump() 的具体实现被放到 FastRun 和 JumpHigh 这两个类中去了。 这样做有什么好处呢?首先,如果将来的策略发生了变化让兔子跳不起来,那么只需要添加一个新的类(策略):JumpNoWay 同样让它实现 Jumpable 接口,jump 方法中打印出 "I can't jump",然后将 Rabbit 类中的new JumpHigh() 改为 new JumpNoWay() 即可,这样就实现了“添加而不是修改”的原则,我们只添加了一个新的策略(类),对原来的策略没有任何修改,最后只是替换了一个策略而以(当然这种修改是必要的)。另一个好处是,将来如果要修改 JumpHigh 的算法,让它可以跳 150 米,那么直接去修改 JumpHigh 里的 jump() 就可以,而不会影响到 Rabbit,从而降低了耦合度,这是封装算法所带来的好处。 这一切的灵活性都是多态所带来了,因此在很多的设计模式中都会用到多态,为的就是降低耦合度,增加程序的灵活性以及提高扩展性。以下是该模式的 UML 类图: