键盘敲烂,月薪过万作业不做,等于没学
当前系列: 编程语言 修改讲义

多态(polymorphism),是面向对象三大特征中最难以理解的一个。


PS:飞哥当年就是靠着背“多态”骗到的第一份正儿八经的开发工作

首先它的名字就非常难以理解,飞哥试着用自己的语言对它进行一个解释:

同一个变量,调用“同一个”的方法,可以呈现出多种形态。

PS:JavaScript弱类型/函数式编程语言,不存在多态……

怎么回事呢?我们首先来了解几个语法现象。


相同的方法

子类和父类可不可以有签名(名称、参数和返回值)完全相同的方法?这是可以的。

但C#和Java有不同的机制,C#需要在父类方法中添加virtual关键字,将其变成虚方法(Java默认所有方法都是虚方法,不需要/不能添加virtual关键字):

    class Person
    {
        public virtual void Eat()     //注意关键字 virtual
        {
            Console.WriteLine("人吃饭");
        }
    }

然后,子类方法用override标记,重写父方法(Java也不需要/不能添加override关键字):

    class Student : Person
    {
        public override void Eat()   //注意关键字 override
        {
            Console.WriteLine("学生吃饭");
        }  
    }

@想一想@:静态方法可以virtual和override么?

演示:不同的对象,调用不同的方法

依对象调用

结合之前学习的语法:父类装子类

Person ywq = new Student();
ywq.Eat();        //结果为:学生吃饭
就会造成一个问题:ywq究竟该调用哪一个Eat()方法呢?究竟是依变量调用Person的,还是依对象调用Student的?

运行上述代码,你会发现,使用的是Student类的Eat()方法。

多态面世

语法到此为止,那多态如何体现呢?让我们再将ywq赋值给一个Person对象:

Person ywq = new Student();
ywq.Eat();     //代码第2行,输出:学生吃饭
ywq = new Person();
ywq.Eat();     //代码第4行,输出:人吃饭

对比代码第2行和第4行,同一个对象ywq,调用“同样的”方法Eat(),得到的却是不同的结果——这就是多态。请仔细体会,^_^

总结一下,多态出现的三要素:

  • 继承
  • 父类装子类
  • override坑爹

其他

此外,子类中可以调用父类的方法。为了避免重名,C#中使用base,Java中使用super表示基类对象(复习:this代表当前

在override方法中调用其基类方法,是一种非常常见的操作:

    class AgileStudent : Student
    {
        override void Eat()  //继续override
        {
            base.Eat();     //base指Student
        }
    }

@想一想@:假设你忘了base.,会是怎么一个情景?

其次,可以在多层virtual和override:

    class AgileStudent : Student
    {
        public override void eat()  //继续override
        {
            base.eat();     //base仅指Student,不包含父类的父类
        }
    }


为什么需要?

我们举例来说明,我们需要实现一个源栈食堂供应午餐(ServeLunch)的方法,里面包含:

  1. 通知:开饭啦
  2. 老师学生开吃
  3. 收拾碗筷啥的
  4. ……

这么一些流程。1/3/4都是一样的,但:

  • 如果是老师,就吃小灶;
  • 如果是学生,就吃大锅饭。

怎么办?是不是这样:

        static void ServeLunch(string role)
        {
            Console.WriteLine("开饭啦……");
            if (role == "老师")
            {
                new Teacher().Eat();
            }
            else if (role == "学生")
            {
                new Student().Eat();
            }
            //else ignore
            //其他代码……
        }

减少if...else

这样的代码问题很多:

  1. 没有必要引入一个string role,
  2. 更不应该在方法中new Teacher或者Student,调用一次ServeLunch()就要生成一个新的老师或学生?难道不应该是为某个老师或学生提供午餐么?
  3. 要小心if...else...,在实际的开发中,复杂的if...else...嵌套是代码维护的噩梦,能够少一点就少一点。

怎么优化呢?这就要利用到多态了:

        static void ServeLunch(Person person)
        {
            Console.WriteLine("开饭啦……");
            person.Eat();   //不同的Person对象自然会调用不同的方法
            //其他代码……
        }

咦?!首先if...else...没了,@想一想@:需求中的“如果...就……”能实现么?

但减少if...else只是多态应用的结果,还不是应用多态的关键。

抽象的思考

因为多态的作用就是减少if...else,那是你站得不够高……

  • 将才 vs 帅才:区别在哪里?
  • 蒋委员长:军事才能,也就是个步兵团长

我们很多程序员只看到了底层原理,忽视了顶层——驾驭大型项目的能力。造轮子,了不起;但是,能用轮子,能用好成千上万的轮子,也是本事呀!否则,你永远不会明白:航母母舰不就是一艘大船么,有什么了不起?……

PS:过度“吹捧”底层,

  • 原因:格局小了,眼界不够 (学生党+理论派
  • 带来的问题:产生一种幻觉,上层/顶层/软件工程很简单的嘛,没有技术含量,不就是增删改查吗?

多态为我们编程开发,带来的最大的好处,是:让我们抽象的设计成为可能。

什么是抽象?大处着眼、屏蔽细节、归纳总结,就是抽象。

面向对象 vs 面向过程

对象我们知道是啥玩意了,但“面向”呢?

什么是面向对象(Object Oriented)中的“面向(Oriented)”?和“面向未来”“面向现代化”里的面向类似,指的是在项目的设计和开发(甚至于测试)中,都应该“以对象为核心以对象为导向”进行。

和面向对象相对应(但不是对立)的是“面向过程”。他们的区别是什么?拿到一个需求,如果你:

  • 面向过程,那你考虑的就是如何一步一步的完成,分支循环之类的东西;
  • 面向对象,那你考虑的就是这事由哪个或者哪几个对象来做,如果是几个对象,他们之间如何协作……

这就是面向对象,不考虑具体的实现细节,而是考虑对象的组织调用。

@想一想@:为什么要这样思考?—— 代码规模的增加,人脑的局限:迫不得已

比如:

  • 炒一盘番茄炒蛋,你可以面向过程;
  • 管一个上万人的筵席,你还能面向过程不?

你是不是就只能管人、管物、管流程?谁谁谁负责采购,谁谁谁负责厨房,各方面如何衔接……还去安排鱼香肉丝怎么切丝怎么上色,行不行?

面向对象的编程也一样,代码规模一上来,你还去关心每一个方法的实现细节是不现实的。

进一步抽象

不要觉得你把任务分派给对象就OK了。

当对象也越来越多了之后,对象还需要被抽象(继承和多态),否则大量的类一样会把我们搞得懵逼。

一个人其实最多只能管理七个人。

有了继承和多态,我们就可以通过抽象出一些(数量更少的)基类进行更简化的思考:屏蔽一些具体的细节,进行一种“更高层级”“更大范围”的思考和实践。

就像上面的例子,我们把学生和老师都抽象成“人”,是人就吃饭,至于他们各自具体吃什么,我们在进行“全局思考”时不关心。

架构师 vs 小码农

理解多态,一种比较好的方法:把写代码的过程想象成是两个人:

  • 一个是架构师,他在宏观的层面进行思考和coding
  • 一个是小码农,他复制实现具体的功能。

比如我们在设计一个军事管理系统,其中有一个环节/需求是:发现敌机,防空系统开始射击

架构师怎么弄?他就想到,要在之前的代码里,插入这么两行代码:

            //其他code

            FindEnemy();    //发现敌机
            airDefense.Shoot();    //有个airDefense,它能够开火就行
            
            //其他code....

airDefense是啥?AirDefense的实例啊,定义这样一个类,就OK啦:

class AirDefense
{
    public virtual void Shoot()
    {

    }
}

至于防控系统怎么样的开火,我不care……

最后实现的时候,就苦逼小码农上场——架构师的代码,你不要乱动(复习:封装)——那就只有继承+多态了:

    class AirDefense
    {
        public virtual void Shoot(){};
    }
    class MissleAirDefense : AirDefense
    {
        public override void Shoot()
        {
            Console.WriteLine("东方一号导弹发射");
        }
    }

    class FlakAirDefense : AirDefense
    {
        public override void Shoot()
        {
            Console.WriteLine("高射炮开火");
        }
    }

再次理解继承

如果:
  • 你从上往下看,先有父类再有子类,这就是继承:子类继承了父类;
  • 你从下往上看,先有子类再有父类,这就是抽象:从子类中抽象出父类。

继承不是为了重用,而是为了多态。理解:这里的多态,就是抽象。

@想一想@:之前的LegThing和MoveThing啥的为什么让我们别扭?

首先,面向对象要映射现实。面向对象的根本目的是为了让代码更加容易被人理解,而现实是我们最容易理解的部分。合理的映射现实,有助于代码的理解。所以我们Cat和Dog继承自Animal更容易让人理解,而不是继承自什么LegThing,^_^

其次,继承本质上体现的是一种“是”的关系。Cat和Dog继承自Animal,体现的是:猫和狗都“是”动物。那为什么他们都是动物呢?更多的是因为他们共同/类似的行为(会跑会叫有生命),而不是属性(有一个脑袋四条腿),在我们面向对象的设计中,这一点至关重要。

臣妾做不到

不思考不行啊!

——这可能就是初学者最不放心的地方:怎么能不关心呢?!

不关心实现不关心细节,代码功能实现不了一堆bug怎么办?

道理都懂,但是臣妾做不到啊……

做不到就对了。

面向对象对应的是面向过程,他们不说对立的,不是说面向对象了就不能再面向过程了,恰恰相反:面向过程,始终是面向对象的基础。你基础打得不牢,怎么可能建起来气势恢宏的摩天大楼?

还是那句话:有些东西,是要靠时间来堆的!所以,别瞎操心什么“程序猿35岁过后怎么办”,都是扯淡!“键盘敲烂,月薪过万”,同学们还是代码可劲的造吧,^_^


作业

利用继承和多态,实现下列功能:

  1. 所有Content都可以发布(Publish()),但是:
    1. 如果发布Article,需要Article作者消耗一个帮帮币
    2. 如果发布Problem,需要消耗Problem作者其设置悬赏数量的帮帮币
    3. 如果发布Suggest,不需要消耗帮帮币
  2. 添加一个新类ContentService,其中有一个发布(Release())方法,可以发布各种文章。方法体内按顺序:
    1. 控制台输出:准备发布……
    2. 如题1根据文章种类不同,消耗不同的帮帮币
    3. 控制台输出:保存文章到数据库。
学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 编程语言 中继续学习:

下一课: UML:类图 / ……

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码