分享

多态(Polymorphism)

 richsky 2013-03-04

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 的时候都要将它向下转型成为该对象的实际类型才能放心使用。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多