1、面向 对象三大特征 (封装、继承、多态) 详解 封装 封装是面向对象的特征之一,是对象和类概念的主要特性。 封装,也就是把 客观事物 封装成抽象的 类 ,并且类可以把自己的 数据 和 方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 比如人这个客观事物 可以采用如下方式封装: 人 姓名( 特征 ) 年龄( 特征 ) 性别( 特征 ) 做事(行为) 走路(行为) 说话(行为) 上面是对客观事物人的具体描述,用类 封装后 转化为: class Person string name; /数据:姓名 int age; /数据:年龄 bool sex; /数据:性别 void Do() /方法:做
2、事情 void Walk() /方法:走路 void Say() /方法:说话 采用类来描述客观事物的过程就是一种封装,类可以看成一种封装手段,其实是对客观事物的一种抽象。 类成员采用访问级别来控制让可信的其他类进行操作,对不可信的进行信息隐藏。 加上访问级别修饰符 (标为蓝色) , 关于人 的描述又进一步可以抽象为: /公开的类,允许任何其他类进行访问 public class Person /以下为私有成员变量,仅允许在该类内部访问 private string name; /数据:姓名 private int age; /数据:年龄 private bool sex; /数据:性别 /公
3、有方法,允许任何类访问 public void Do() /方法:做事 /受保护的方法, 仅 允许派生类访问 protected void Walk() /方法:走路 /内部方法, 仅 允许在同一个程序集(项目)内访问 internal void Say() /方法:说话 采用 UML 图形描述为: 通过该图形可以看出我们将一个自然界中真实的事物: 人 进行了 封装 , 将人(包括 特征和行为 ) 抽象成了开发语言当中的 类。因此,类的产生本身就是封装的体现。 于是, 封装 构成了 面向对象的第一大特征。 继承 面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承是指这样一种能力:它
4、可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”。 被继承的类称为“基类”、“父类”或“超类”。 继承的过程,就是从一般到特殊 (具体) 的过程。 关于继承的关系,可以这样描述:子类 is a 父类 或者严格来说 子类 is a kind of 父类 (子类是父类的一种)。 如:狗是动物的一种(狗是动物),学生是人的一种(学生是人) 。 具备继承关系的多个类构成了一种继承体系或者 称为 继承层次 。采用 UML图形 建模方式来展示这种层次关系, 会更加直观 。 (这里的父类为人( Person),共派生了两个子类:学生(
5、 Student)和教师( Teacher) 。 并且 学生类 额外 增加了一个学习( Learn)方法,教师类 额外 增加了一个教学( Teach)方法。) 借此图,我们再进一步阐述 继承关系 。上图说明, Person 是基类,而 Student和 Teacher 都派生自 Person。于是我们说 Student 是 Person 的一种, Teacher 是Person 的一种,并且 Student 和 Teacher 将继承 Person 的所有的功能和属性(这里指访问级别为 公开的 和 受保护的 或者 内部的 )。 关于对象和对象的关系中,还有一种 包含关系 , 如下面图示: (
6、注:上面的图示不属于 UML 建模图形) 通过上图可以看出 Head 这个类是由下面的 Eye、 Nose、 Mouth、 Ear 组合而成的。这时候我们可以说, Eye is a part of Head 或者 Nose is a part of Head 等等。 也就是说,眼、鼻、嘴、耳都是头的一部分。 在 UML 中,又把包含关系细分为两种,分别称为 组合 和聚合 。 组 合: 上面的 UML 图形采用的是实心菱形加实线箭头表示,意思是: Car(汽车)是由 Wheel(轮胎)组成 , Wheel is a part of Car。 除了具备包含关系, 组合的特点还有: Wheel(轮胎
7、)是不能离开 Car(汽车)而单独存在的 。 当然这也需要视问题领域,如果是汽车制造厂,我们说,轮胎是一定要组合到汽车里面去的,因为它离开汽车就没有意义了。 但是,如果是在卖轮胎的店铺里,就算轮胎离开了汽车,它也是有意义的,这里就可以采用 聚合 了。 聚合: 上面的 UML 图形 采用的是空心菱形加实线箭头表示,意思是 Car 聚合 Wheel,同样 Wheel(轮胎)是 Car(汽车)的一部分,但是这里的 Wheel 可以脱离 Car 而独立存在。 因此,关于类的包含关系,到 底是使用聚合还是组合,需要看具体的问题领域,从而设计最合适的关系模型。 思考下 面的问题, 看 是采用聚合还是组合:
8、 1、 眼睛和头的关系 2、 鼻子和头的关系 3、 学生和老师的关系 4、 电脑和硬盘的关系 5、 电器和电池的关系 关于类之间的关系, 我们知道有 三种:继承、包含和 关联。前面我们借着讲继承的概念,顺便用 UML 图形建模描述了继承关系和包含关系,为了概念的完整性,我们再进而来讲讲类之间的最后一个关系:关联关系。 关联: 其实在面向对象的世界里,关联关系最普遍,举例子来说:教师教学生、老公爱老婆、水壶烧水、商店卖商品等等,多不胜数,这些都是关联关系。 上面是对 学生和老师 的关联关系的 UML 描述图。这里的单向箭头说明了教师教学生是一种单向关系, Student 类可以使用 Teache
9、r 类里面的属性和方法。用C#代码描述如下: public class Student /这里省略了 其他数据成员 private Teacher teacher; public void Learn() Console.WriteLine(“我从 0老师那里学到了什么什么 ”,teacher.Name); public class Teacher private string name; public string Name getreturn this.name; 除了单向关联,还有双向关联。因为在实际编程中,双向关联 不好维护和控制,因此一般来说很少使用双向关联来描述类之间的关系。 还有
10、一种关联关系,比我们上面说的要弱很多,它们之间原本就没有任何关系,只是在某些情况下,用到了某个类的方法或者属性,这种关系我们称之为:依赖。 比如,我们在演示 Student 的代码中,用到了控制台 Console 这个类的打印方法,那么 我们说 Student 了依赖了 Console 类,很显然,这是一种弱关联。因为, Student 和 Console 没有明显的关系。 依赖: 关于继承,以及类和类之间的关系,我们就先简单介绍到这,下面 我们将言归正传,再讲讲面向对象的第三大特征:多态性。 多态 多态性 一般指事物能够在不同情况下表现出多种不同的形态 , 在哲学意义上,多态性反映了世界在时
11、间和空间中存在的千姿百态和变化莫测。 在 面向对象的 软件领域,多态性 则主要 体现在类的 继承 体系中。 用父类声明的类型,可以在程序运行期调用不同的子类来完成。由于每个具体的子类都具有自己的独特的行为和属性,当调用同一个方法时, 就 会产生不同的结果 。 实现多态,有两种具体方式:覆盖( override)和重载( overload) 。 覆盖: 是指子类重新定义父类的虚方法 的做法。它是覆盖了一个方法并且对其重写,以求达到不同的作用。 采用覆盖时,需要注意以下几点: 1、覆盖的方法的标志必须要和被覆盖的方法的标志 完全匹配 ,才能达到覆盖的效果; (包括:方法名、参数类型、参数个数) 2
12、、覆盖的方法的返回值必须和被覆盖的方法的返回一致; 3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类; 4、被覆盖的方法不能为 private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。 可以参考下面这个继承层次来演示采用覆盖的方式实现多态的过程: 我们看到 Student 和 Teacher 都继承自 Person,而 Person 中提供了一个方法:Do(), 我们知道人肯定是要有点事干的,不然就没意思,因此 基类 Person 中提供的 做事 方法是很有意义的 。但是具体人要做什么事呢?这样说起来,确实有点笼统,有点含糊其辞。那么我们具体点,教师要
13、做什么事? 学生要做什么 事 ? 这样是不是就清晰了点:教师要教学;学生要学习。 于是,我们将基类 Person 中的 Do 方法设计成虚方法: public virtual void Do() Console.WriteLine(“做事 ”); 在教师类中加入 Do 方法的覆盖实现: public override void Do() Console.WriteLine(“教学 ”); 在学生类中加入 Do 方法的覆盖实现: public override void Do() Console.WriteLine(“学习 ”); 现在,经过我们的设计,我们可以说父类 Person 提供的 Do
14、 方法具备了多态性,或者说支持多态性,那么如何 使用多态呢? 为了说明这一点,我们定义了一个具备 Main 方法的测试类 (省略了类的写法 ,直接进入 Main 方法 ) : public static void Main() /提问者 :我想知道人做事的执行结果? /回答者 :人做事? 好吧,我试着告诉你 Person p = new Person();/先声明并实例化一个人对象 p.Do();/调用人做事的方法 /执行结果: 人做事 /提问者: 不是吧,你的回答太笼统了吧? /回答者 :你又没说什么人,我只能这样回答你! /提问者 :好吧,我想知道教师会做什么事? /回答者 : 没问题 p
15、 = new Teacher();/将上面的人 ( p) 指向了一个具体的教师对象 p.Do();/仍然调用人的做事方法(因为这里的 p 一直 是采用人来声明的) /执行结果:教学 /提问者: 恩,我晓得了。我还想知道学生该做什么事? /回答者 :没问题 p = new Student();/再将上面的人( p)指向一个学生对象 p.Do(); /执行结果 :学习 对于上面的例子,提问者 意犹未尽, 还 在 思考: 难道这就是传说中的多态吗?噢, 声明的时候,采用同一个父类型 Person 来声明 出一个引用变量 ( p) ,而在具体想知道行为结果的时候 ,让 p 指向不同的具体对象( Tea
16、cher、 Student) ,最后 仍然调用 p 的 Do 方法,就会产生两种 不同的行为结果(教学、学习) 。 确实不错。 但是 ,提问者还是有 疑问: 既然想知道教师的做事结果,我是不是可以直接这样写呢: Teacher teacher = new Teacher(); teacher.Do(); 这里的结果就是教学,那同样 ,我用 Stduent stu = new Student(); stu.Do(); 这里的结果就是学习。这不是也可以吗?多态有啥用啊? 回答者这时候意味深长地说: 不错,刚开始接触多态的时候,确实都会产生这种感觉。为了解释清楚,我下面将举一个实际的软件 开发实例,
17、现在请做好充分的准备,放轻松、深吸一口气 , 好戏 马上 开始了 故事开始了 最近小王开了一个卖鸭店,目前主要出售红头鸭,我们为此专门设计了一个类图,以满足他的需求: SellDuckWindow 类是专门用来卖鸭子的窗口 类 ,由于开发人员考虑到了项目的扩展性( 小王现在只卖红头鸭,但是在不久以后很可能还有别的品种的鸭子要卖 ),于是 开发人员 将 Duck 设计成了鸭子的超类,允许将来任何种类的鸭子继承 Duck,而目前由于小王只卖红头鸭,因此就只有一个 RedHeadDuck 子类。Ok,经过小王精心准备后,鸭子店开张了。每天 SellDuckWindow 门前都络绎不绝,人们都慕名前来
18、购买红头鸭: public static void Main(string args) /出售红头鸭 Console.WriteLine(“走过路过,不要错过,快来看呀: “); RedHeadDuck duck = new RedHeadDuck(); Console.WriteLine(“小姐,来一只吧?既饱口福,又养颜美容! “); duck.Sell(); 过了一个多月,人们渐渐对红头鸭失去了刚开始的兴趣,人开始慢慢稀少了。这时候,小王又灵机一动:不如进口一批绿头鸭进来,肯定又能吸引不少人。 小王找到开发人员,要求再添加一个品种:绿头鸭。 开发人员心想,幸亏我想得周到,将 Duck 设
19、计成了超类,那我再加一个绿头鸭子类不就行了吗,于是,设计又 做了如下扩展 : 这时候,售卖鸭子的窗口又要有所变化了,再 看 SellDuckWindow 的 Main 方法: public static void Main(string args) Console.WriteLine(“走过路过,不要错过,快来看呀: “); Console.WriteLine(“先生,您要买红头鸭还是绿头鸭? “); /顾客的选择 string answer = Console.ReadLine(); /判断,根据顾客的选择决定展示哪个品种的特征: if (answer = “红头鸭 “) RedHeadDu
20、ck duck = new RedHeadDuck(); Console.WriteLine(“小姐,来一只吧?既饱口福,又养颜美容! “); duck.Sell(); else GreenHeadDuck duck = new GreenHeadDuck(); Console.WriteLine(“来一只吧?返老还童不是梦! “); duck.Sell(); 同样红火了一阵,小王的生意又开始渐入低谷了,怎么办呢? 小王思考,看来国产的鸭子已经不能引起顾客的注意了,不如引入荷兰鸭吧,说干就干。 于是,小王通知 开发人员说,我 现在开始主 卖 荷兰鸭 了 。 对于客户(小王)的这种 需求变更 ,
21、开发人员 已经习以为常了(干软件的都知道,客户都是情绪化的动物,说变就变, 所以需求变更那是软件开发领域里面的 家常便饭 。) 开发人员想: 这还不简单,我再添加一个荷兰鸭子类继承 Duck,再修改一下 SellDuckWindow 类里面的 Main 方法内容, 再加上荷兰鸭的判断 不就行了吗。 正当软件开发人员打算一鼓作气再次修改卖鸭店的代码时,项目经理走了过来,他询问了整个项目的开发过程,仰头想了一会,然后就对开发人员说: 小王这个人很精明,很会做生意,假设 有一天他的生意越做越大,想在国内开连锁店怎么办? 每个连锁店都相当于一个 SellDuckWindow,那样这种类 就不会是一个而
22、是数十个甚至数百个了 ,如果小王继续改变售卖 鸭子 的 种类,你再次维护的 可不是一个售卖窗口类了 ,你的工作量将会大大的增加,这样无疑会降低工作效率的。你想想有什么办法应付客户的这种持续增长的需求,而又 能有效 减少工作量呢? 开发人员心想:也是啊,如果小王开上 20 家连锁店,那我是不是就要将 Main方法中的代码重复写 20 次啊, 如果修改的话,也得改 20 次, 不妥当。 开发人员经过认真思考,将需求重新捋了一下,终于弄明白了: 其实卖鸭店的窗口只负责卖鸭子,它并不关心卖什么样的鸭子,卖什么样的鸭子其实是老板说了算的。那我是不是可以加一个决策类,这个类主要体现老板的意思,而将买鸭子的
23、窗口类中的代码抽离呢? 试试吧: 开发人员又加了一个类: Decide 整个思路变得清晰了: Duck 是所有鸭子的超类,不管是 RedHeadDuck 还是GreenHeadDuck 还是 HolandDuck(荷兰鸭)都统统继承自 Duck,通过覆盖 Duck中的 Sell 方法, 让不同的鸭子 在 卖出时 展示 不同的特点 。而 SellDuckWindow 现在也不多管闲事了,它只负责卖 鸭子了(具体卖什么鸭子,它不再关心了),新加的 Decide 类则是专门负责卖什么鸭子的决策类(反应老板的具体意思) 。这样,就算是开 10 家、 20 家、 200 家连锁店, 老板再引进新鸭种,修
24、改起来也不会影响到任何一家店铺窗口,只需要修改 Decide 类即可 。(哈哈哈 狂笑数声 ) 想好了,也设计好了,动手干吧: Duck( 鸭子 超类 ) 类代码: using System; namespace SellDuck public class Duck public virtual void Sell() Console.WriteLine(“哇,终于卖出去一只! “); RedHeadDuck( 红头鸭 ) 类代码: using System; namespace SellDuck public class RedHeadDuck:Duck public override vo
25、id Sell() Console.WriteLine(“品种:红头鸭;特点:红头,全身纯白,体表光滑 “); base.Sell(); GreenHeadDuck( 绿头鸭 ) 类代码: using System; namespace SellDuck class GreenHeadDuck:Duck public override void Sell() Console.WriteLine(“品种:绿头鸭;进口鸭种,可舒筋活血! “); base.Sell(); HolandDuck( 荷兰鸭 ) 类代码: using System; namespace SellDuck public c
26、lass HolandDuck:Duck public override void Sell() Console.WriteLine(“品种:荷兰鸭;肉香皮嫩,分外好吃! “); base.Sell(); Decide ( 改变售鸭种类的决策 ) 类代码: using System; namespace SellDuck / / 决定卖什么种类的鸭子的类 / public class Decide public static Duck DecideDuckType() Duck duck; Console.WriteLine(“先生,您要买红头鸭还是绿头鸭 or 荷兰鸭? “); Consol
27、e.WriteLine(“您可以尝 尝 荷兰鸭,这是我们的特色鸭! “); /顾客的选择 string answer = Console.ReadLine(); /判断,根据顾客的选择决定展示哪个品种的特征: if (answer = “红头鸭 “) duck = new RedHeadDuck(); Console.WriteLine(“来一只吧?既饱口福,又养颜美容! “); else if (answer = “绿头鸭 “) duck = new GreenHeadDuck(); Console.WriteLine(“来一只吧?返老还童不是梦! “); else duck = new H
28、olandDuck(); Console.WriteLine(“您真有眼光,这个绝对让你爱不释口! “); return duck; SellDuckWindow ( 售鸭窗口 ) 类代码: using System; namespace SellDuck public class SellDuckWindow public static void Main() Console.WriteLine(“走过路过,不要错过,快来看呀: “); /从 Decide 类接收鸭子,具体什么样的鸭子,不关心 Duck duck = Decide.DecideDuckType(); duck.Sell(); 大功告成,这次 我们发现,不论小王要怎么改变售卖鸭子的策略,我们都只需要修改一个类: Decide,而无需改变 SellDuckWindow 里面的任何代码。看出来了吗?是谁提供了这么神奇的力量? 多态 ( 不是变态 ) ! 正是运用了多态,才使得即便我们开再多的分店,建立再多的售卖鸭子的窗 口,将来也不需要改动任何一个 窗口 的代码, 而 只需修改 反应老板思想的决策类 即可 。 多 态的真正作用在于 提高 了 代码的可维护性,提高了软件开发人员的工作效率,使得 代码更加易扩展、易维护。