大话设计模式 | 0 面向对象基础
《大话设计模式》是作者「程杰」通过趣味的场景设置,以诙谐的表达来解读和剖析「面向对象」编程思维和「设计模式」。书中的示例代码是以 .NET 的 C# 语言编写而成。
本文是我对《大话设计模式》的学习系列笔记的第一篇,面向对象基础。
1. 对象与类
首先,一切事物皆为对象。对象是一个自包含的实体,用一组可识别的特性和行为来标识。面向对象编程是指针对对象来进行编程,英文名为 Object-Oriented Programming。
类是具有相同的属性和功能的对象的抽象的集合。
举例来说,「猫」是一个类,「耶啵」这只猫是一个真实的对象,从类创建对象的过程叫做实例化。
在 C# 中,类的定义是通过 class
关键字来定义的,类名的首字母要大写,且对外公开的方法需要使用 public
修饰符。对象这个实例,是通过 new
关键字来创建的(实例化)。
2. 构造方法
构造方法,又叫构造函数,是为了对类进行初始化。构造函数与类同名,无返回值,也不需要 void
,在 new
实例化时候调用。
所有的类都有构造方法,如果没有定义,则系统会默认生成空的构造方法,如上例中的 Cat()
。若有定义,则默认的构造方法会失效。
在构造方法中,可以使用方法重载,以创建同名的多个方法的能力。在使用方法重载时,方法名必须相同,参数类型或个数必须不同。
3. 属性
属性包含 get
和 set
两个方法,get
访问器返回与声明的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用;set
访问器没有显示设置参数,但它有一个隐式参数,用关键字 value
表示,它的作用是调用属性时可以给内部的字段或引用赋值。
在调用属性的代码来说,属性也可以看作是一个字段。下面通过一些代码段来看下。
ShouNum
属性是 public
的,其 get
方法表示外界调用时可以得到 shoutNum
的值,set
方法有一个隐式的参数,用关键字 value
表示,作用是外界调用属性时可以给内部的字段 shoutNum
赋值。
这里,为什么不直接把 shoutNum
字段的修饰符改为 public
以实现既读又写呢?因为我们期望对那些对外公开的数据做更多的控制,不能让调用者任意的读取或写入。就好比一个房间,其门和窗是 public
的,但房间内部的东西希望是 private
的。
面向对象的三大特征分别是封装、继承和多态,下面来依次讲解。
4. 封装
每个对象都包含它能进行操作所需要的所有信息,而不必依赖其他对象来完成自己的操作,这个特性称为封装。这样,方法和属性都包装在类中,通过类的实例来实现。
封装的好处包括:
良好的封装能减少耦合
类内部的实现可以自由地修改
类具有清晰的对外接口,如
ShoutNum
属性和Shout
方法。
5. 继承
对象的继承代表了一种 is-a
的关系,如果两个对象A和B可以表述为B是A,那么表明B可以继承A,比如猫(B)是哺乳动物(A)就是一种继承和被继承的关系。继承者(子类、派生类)可以看作是被继承者(父类、基类)的特殊化,除了具备被继承者的一些特性外,还具备自己独有的个性。
子类拥有父类的非
private
的属性和功能子类具有自己的属性和功能(可扩展)
子类可以以自己的方式实现父类的功能(方法重写)
比如,对猫和狗,都可以抽象为动物,将其共有的名字、叫的次数这个功能放到动物这个类中,通过继承以避免重复的代码。
继承的缺点就是,当父类发生变化时,子类必须也不得不变。也会破坏了包装,将父类的细节暴露给子类,增大了两个类之间的耦合性。
当类之间是 is-a
的关系时,可以考虑使用继承。但当两个类之间时 has-a
关系时,比如人有两只手,则不能使用继承了。
6. 多态
多态表示不同的对象可以执行相同的动作,但是要通过它们自己的实现代码来执行。需要注意以下三点:
子类以父类的身份出现
子类在工作时以自己的方式来实现
子类以父类的身份出现时,子类特有的属性和方法不可以使用
为了让子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的(virtual
关键字),子类通过使用 override
关键字来重写父类的实现。
比如,猫和狗都会叫,但是叫声不同,此时可以让动物有一个 Shout
的虚方法,然后猫和狗去重写这个Shout
方法以实现各自的叫声,以实现多态。
7. 重构
当我们在原来的基础上,好需要增加动物的种类来实现不同的叫声时,会发现在不同的动物的 Shout
方法中,除了叫声不同外,其他都是相同的,此时就需要对我们之前的代码进行改造了,来消除一些重复。
比如把 Shout
方法变为普通的方法放到 Animal
中,同时增加一个另一个方法 getShoutSound
来获取不同动物的叫声。这样,只需在不同的动物的实现中重写 getShoutSound
即可,将所有的重复都转移到父类的 Animal
中了。
8. 抽象类
C# 允许把类和方法声明为 abstract
`,及抽象类和抽象方法。抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象类时,一定是用来继承的。所以,在一个以继承关系形成的等级结构里面,树叶节点应该是具体类,而树枝节点均应该是抽象类。
抽象类有以下几点需要注意:
抽象类不能实例化
抽象方法是必须要被子类重写的方法
如果类中包含抽象方法,那么类就必须定义为抽象类
前面的 Animal
类其实就是一个抽象的概念,是不可能实例化的,所以可以将其改造为抽象类:
9. 接口
接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。声明接口在语法上与声明抽象类相同,但是不允许提供接口中任何成员的执行方式。一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。实现接口的类,就必须要实现接口中的所有方法和属性。一个类可以支持多个接口,多个类也可以支持相同的接口。
接口通过 Interface
来声明,接口名称前需要加I,接口中的方法或属性前不能有修饰符、方法没有方法体。
比如对前面的猫,如果希望某些猫有特异功能,可以变出一些东西。但是特异功能并不是普通的猫有的能力,所以不能作为猫的一个方法。此时就需要借助接口了。
通过定义一个「变东西」的接口,然后让某些猫,比如机器猫,来实现定义好的「变东西」的接口。
抽象类与接口的不同点:
类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象
如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象化
抽象类是从子类中发现公共的东西,然后泛化处父类,然后子类继承父类;而接口根本就不知道子类的存在,方法如何实现还不确认,预先定义。
10. 集合
介绍集合前,先讲讲数组。数组在内存中是连续的,可以快速地从头到尾遍历元素并对元素进行修改;但数组在创建时必须要指定数组变量的大小,且在两个元素之间添加元素较为困难。
.NET Framework 针对这一问题提供了用于数据存储和检索的专用类,这些类统称为集合。这些类提供对堆栈、队列、列表和哈希表的支持,其中,ArrayList
是最为常用的一种。
ArrayList
是命名空间 System.Collection 下的一部分,它是使用大小可按需动态增加的数组实现 IList
接口。ArrayList
的默认初识容量为0,随着元素的添加,容量会根据需要重新分配自动增加,可通过整数索引访问集合中的元素。
ArrayList
是以 Object
类型来存储所有元素的,不管是字符串类型还是整型都能接受,但是在遍历运行时,会导致类型不匹配而出错,这说明 ArrayList
不是类型安全的。
由于 ArrayList
都是需要将值类型「装箱」为 Obejct
对象,在使用集合的元素时,还需要执行「拆箱」操作,这会带来很大的性能损耗(相对于简单的赋值,装箱和拆箱过程需要大量的计算,对值类型进行装箱时,必须分配并构造一个全新的对象)。
装箱,即把值类型打包到
Obejct
引用类型的一个实例中,比如整型变了i
被装箱并赋值给对象o
:
拆箱时指从对象中提取值类型,比如对象
o
拆箱并将其赋值给整型变量i
:
11. 泛型
泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。泛型集合类可以将类型参数用作它所存储的对象的类型的占位符。
泛型集合需要使用System.Collections.Generic的命名空间,List
类是 ArrayList
类的泛型等效类,该类使用大小可按需动态增加的数组实现 IList
泛型接口。用法是在 IList
和 List
后面加 <T>
,其中 T
是需要指定的集合的数据或对象类型。
泛型集合相对非泛型集合有两个优点:
类型安全
不需要对元素进行装箱操作,如果集合元素都是值类型,性能会更优
12. 委托与事件
委托是对函数的封装,可以当作给方法的特征指定一个名称。委托是一种引用方法的类型,一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托用关键字 delegate
来声明。
事件是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程。事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。事件用关键字 event
来声明。
完整的代码实现可以参考:https://github.com/puran1218/DesignPatternWithCSharp
版权声明: 本文为 InfoQ 作者【Puran】的原创文章。
原文链接:【http://xie.infoq.cn/article/024954aa2b9c56df999bfd5d4】。文章转载请联系作者。
评论