文章目录
  1. 1. 面向对象和面向过程的区别
  2. 2. 设计类
  3. 3. 类设计指导原则
  4. 4. 利用对象设计好的系统
  5. 5. 继承和组合

最近在团队内部组织了一场面向对象的分享会,做了部分内容整理。

面向对象和面向过程的区别

1.在OO设计中,属性和行为都包含在一个对象中;而在过程式程序设计中,属性和行为是分开的,它把程序的内容分为数据和操作数据的操作两部分,这种编程方式的核心问题是数据结构和算法的开发和优化。

也就是说,在结构化设计中,数据和过程通常是分离的,有时数据时全局的,多个函数都可以访问全局数据,说明对数据的访问是非受控而且不可预测的,这给测试和调试带来了很多困难。而对象技术可以解决这些问题,它将数据和行为合并到一个完备的包中。

2.面向过程是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向对象是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物(对象)在解决整个问题步骤中的行为。

例如五子棋系统的开发:

五子棋

面向过程的设计思路就是首先分析问题的步骤:

  1. 开始游戏
  2. 黑子先走
  3. 绘制画面
  4. 判定输赢
  5. 轮到白子
  6. 绘制画面
  7. 判定输赢
  8. 返回步骤2
  9. 输出最后结果

把上面每个步骤分别用函数实现,问题就解决了。

而面向对象的设计原则是从另外的思路来解决问题。整个五子棋可以分为:

  1. 黑白双方(玩家对象),这两方的行为是一模一样的
  2. 棋盘系统(对象),负责绘制画面
  3. 规则系统(对象),负责判定诸如犯规、输赢等。

玩家对象负责接受用户输入,并告知棋盘对象棋子布局的变化,棋盘系统对象接收到棋子的变化负责在屏幕上显示出变化,同时利用规则系统对象来对棋局进行判定。

可以看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图功能的统一。

功能上的统一保证了面向对象的可扩展性。

比如要加入悔棋的功能,在面向过程的设计中,从输入到判定到显示这一连串的步骤都要改动,甚至整体上要进行大规模的改动。假如是面向对象设计的话,只用改动棋盘对象就行了,棋盘对象保存了黑白双方的棋谱,简单的回溯就可以了,而其他地方则不用改动,改动的只是局部。

到这里可以很明显的看出面向对象和面向过程的区别了。

面向过程向面向对象的演进:

在结构化编程中,人们发现把某种数据结构和用于操纵它的各种操纵绑定到一起会非常方便,如果对抽象数据类型进一步的抽象,会发现这种数据类型的实例会成为一个具体的东西、事物、对象,这就引发了人们对编程过程中怎么看待处理问题的一次大的改变。经过不断的发展,人们处理问题的思考的方式不再是怎样的数据结构描述问题,而是直接考虑各个对象之间的关系。这就出现了现在的面向对象开发。

====================================================================

设计类

面向对象三大核心要素:封装、继承、多态

封装

通过把属性和方法合并到一个实体中,这在OO术语称为封装。可以再不影响使用的情况下改变类的内部实现。同时限制对某些属性或方法的访问,保护类内部数据。

比如Math对象中有两个整数intAintB,同时有一个sum()方法将这两个整数相加求和,通过封装处理,限制其它对象对Math对象中数据的访问,这样的好处是你无须知道两个数的和是如何计算的。采用这种设计方法,我们可以改变Math对象计算两个数的和的方法,而不需要修改调用者的逻辑。你想要的只是两个数的和,并不关心它是如何计算的。

接口

接口是对象间通信的基本途径。在面向对象设计中,要尽可能多地隐藏数据。

public方法和属性才属于接口。

设计类是要遵循最小接口原则:

  1. 只有在用户需要的时候才增加接口。
  2. 只为用户提供他们确实需要的东西,这意味着类的接口尽可能少。
  3. 从用户角度设计类,而不要从信息系统的角度进行设计。应该从易于用户使用的角度考虑。
  4. 确保设计类时与将真正使用这个类的人(不只是开发人员)反复考虑过需求和设计。

设计接口的时候要使用抽象思维:OO程序设计的主要优点之一是类可以重用。一般地,可重用类的接口往往更抽象而不是更具体。具体接口通常比较特定,而抽象的接口更为一般。(通常如此)。

所以我们在设计接口的时候,要想充分利用OO带来的好处,我们就需要设计高度抽象的用户接口。

例如:创建了一个出租车对象,有一个载我去机场 的接口比诸如左转、右转、启动、停车 等单独的接口更好用。

对属性的访问加以控制,一旦出现问题,就不必操心去跟踪可能改变该属性的每一段代码,它只会在一个地方改变(即设置方法中)。从安全性的角度看,也不希望无控制的代码修改或获取敏感数据。

构造函数(如果一个方法与所在类同名,而且没有提供任何返回类型,则这个方法就是一个特殊的方法。oc是以init开头)

通常把构造函数作为类的入口点,构造函数非常适合完成初始化和准备工作。其中初始化属性是构造函数完成的一个常见功能,能确保应用处于一种稳定的状态。

一般经验是,总是应该提供一个构造函数,即使你不打算在其中做任何事情。可以先提供一个构造函数,其中不包含任何内容,等以后补充。使用编译器提供的默认构造函数尽管从技术上没什么问题,但是最好清楚地知道你的代码到底是怎样的。比如一个属性默认初始化为0,当这个属性用作除法运算时,就会出现不稳定的情况。

类名

类名相当重要,类名可以描述类本身,它提供了这个类做什么以及它与更大系统如何交互的有关信息。见名知意才是比较好的类名设计。

类设计指导原则

  1. 真实世界系统建模。

    面向对象程序设计的主要目标之一就是类似于人们真正的思维方式对真实世界的系统建模。这种思想的妙处在于,类能够对真实对象以及这些对象与其他真实对象如何交互进行建模。比如CatDog类都是对真实世界的实体建模。

  2. 明确最小公共接口,并隐藏实现。

    为用户提供简洁和有用的功能。并对用户隐藏实现细节。

  3. 设计健壮的构造函数。

  4. 在类中设计错误处理。

    每个系统都会遇到不可遇见的问题。设计类时,开发人员应该要预计可能出现的错误,并包含一些代码,从而在真正遇到这种错误时处理这些情况。

    一般经验是,应用绝对不能崩溃。遇到错误时,系统应当自行修正并继续,或者妥善地退出,不要丢失任何对用户重要的数据。

  5. 设计时充分考虑重用。

    对象可以在不同系统中重用,因此编写代码时应当充分考虑重用。设计类时应当做周全的考虑,尽可能想到所有可能性。

  6. 设计时充分考虑可扩展性。

    比如在设计Person 类的时候,应当只包含一个人特定的数据和行为。这样其他类派生该类时,继承其适当的数据和行为,而不会夹杂其它不属于该类的数据或行为。

    1. 抽出不可移植的代码

      • 指的是将这些不可移植的代码单独放在一个类中,或者至少单独放在一个方法中(一个可以覆盖的方法)。
    2. 让作用域尽可能的小

      • 这与抽象和将实现隐藏的概念是紧密相关的。这种思想是尽可能将属性和行为置于局部。采用这种方式,维护、测试和扩展类就会容易得的。
    3. 类应当对自己负责

      • 所有的对象都应当尽可能的自行负责自己的行为。举个例子:

        假如有个Shape类,有一个print 打印形状的接口,如果你想打印圆形该如何处理呢。这里可以利用多态把Circle 类归到 Shape 类中,Shape 知道应该如何自行打印。如果再扩展其它形状也就很好扩展。(多态是一种非常好的解耦合方式)

  1. 设计时充分考虑可维护性

    设计类的过程要求你将代码组织到多个可管理的部分中。单独的代码段往往比更大的代码更可维护。为了提高可维护性,一定要减少相互依赖的代码,降低类的耦合度。

多态

不同类的对象对同一消息作出不同的响应就叫多态。也可以简单理解为:父类指针指向子类对象。多态是面向对象的重要特征,可以帮助我们消除类之间的耦合关系。

Shape的例子

利用对象设计好的系统

一个可靠的OO设计往往包括以下步骤:

  1. 完成适当的分析
  2. 建立一份描述系统的工作陈述
  3. 从这个工作陈述中收集需求
  4. 开发一个用户界面原型
  5. 明确类
  6. 确定各个类的职责
  7. 确定各个类如何相互交互
  8. 创建一个高层模型来描述要构建的系统

继承和组合

继承和组合都是实现重用的机制。要想重用原先构建的类,只能通过继承和组合这两种途径。

继承

继承 是从其他类继承属性和行为。这种情况下,存在一种真正的父/子关系。(is-a)

OO程序设计中很强大的一个特性是代码重用,继承不仅有利于代码重用,还可以实现更好的整体设计。OO程序设计中主要的设计问题之一就是抽取不同类的共性,而继承就会发挥很好的作用。



DogCat的代码都包含表示眼睛颜色的属性,可以利用继承找出他们的共性,相同的特性上移到Mammal类中,这种情况下DogCat 都继承自 Mammal 类。

这样带来的显著的好处:我们编写 DogCat 类时不需要重新编写父类已有的方法,假如要修改 Mammal 中的方法,就不需要再在 DogCat 类中修改了。 而且 DogCat 都是 Mammal 也符合我们的认知,并且以后扩展不同种的狗或猫都可以分别继承 DogCat 。这样逐步向下建立继承树时对象会越来越具体。继承的思想就是通过抽出共性实现从一般化到特殊化(具体化)

EffectiveC C++ 中给出了一个例子,来说明使用继承完成设计时遭遇的一个难题。考虑为鸟建立一个类。鸟的突出特征就是鸟能飞,所以创建了一个包含 fly 方法的 Bird 类。这个时候应该发现问题,企鹅和鸵鸟怎么办,他们都是鸟,但是不能飞。可以局部覆盖这种行为,但是这个方法名仍然是 fly ,对于一个不会飞的鸟,有一个 fly 方法显然是不合理的。

其实我认为这里并不是继承的缺陷,而是抽出共性的过程出现了问题,可以创建两个类,分别对应能飞的鸟和不能飞的鸟即可。

继承同时也是面向对象的另一个重要特征——多态 的实现的基础。

设计决策

理论上讲,应该尽可能多地抽出共性。尽管尽可能多地抽取共性可以尽量接近实际生活,但也许并不能尽可能贴切地表示你的模型。抽取得越多,系统就会越复杂(在大规模系统中,反复这种决策,复杂性会飞速增加)。这里就带来了一个难题:你想要一个更精确的模型还是一个不太复杂的系统,这里就要根据自己的具体情况作出选择。

有些情况下,你的设计中,模型的更准确所带来的好处比不上它所增加的复杂性。

设计的根本目标是构建一个灵活的系统,但不要增加太大的复杂性使系统自己不堪重负。

比如:你是一位动物管理员,只是养一些寻常的鸟类,并且以后也没有计划进购那些笨重的鸟类,那么在设计的时候,Bird 中就可以有它们共同的行为 fly

继承会削弱封装

封装在OO中是非常重要的,这是OO中的基本概念。通过封装,类隐藏了不需要其它类知道的所有细节。(这句可以不要)

继承关系的类与其它类仍然是强封装,但是超类和子类之间的封装被削弱了。如果子类从超类继承了一个实现,然后超类修改这个实现,那么超类的修改可能会对整个类层产生涟漪影响。

为了减少这种情况所产生的风险,使用继承时一定要坚持严格的 is-a 条件,这很重要。如果子类确实是超类的一种特殊化,那么父类的修改会是一种自然的、可预见的方式影响子类。其实好与坏只是看运用方式是否得当。

组合

组合 是指使用其他对象来构建对象。这是一种组装,不存在父/子关系。(has-a)

例如,汽车有一个(has-a)发动机。发动机和汽车是不同的对象。这就构成了一种组合关系。

使用组合的原因是,可以通过结合不太复杂的部分来构建复杂系统。这是人们解决问题的常用方法。

比如,我们看到一辆车,我们会说“这里有一辆车”,而不会说“这里有一个包括一个方向盘,4个轮子和一个发动机等的大家伙”。

使用组合的另一个主要优点是系统和子系统可以独立构建,更重要的是,可以独立地测试和维护。当今的软件系统越来越复杂,要让大型软件系统正常地工作而且易于维护,它们必须分解为较小的、更可管理的部分。而组合则可以做到这些。

而且使用组件的另一个好处是,你可以使用其他开发人员构建的组件,方便自己的开发。



比如音响系统,单独的部件坏了可以重新换一个部件即可继续使用,而如果这个系统是个集成系统(也就是说该系统完全是一个大黑箱系统),不是基于组件构成,这个情况下,你需要把整个系统拿去修理,这样做不仅复杂,费用高,同时你也将无法使用其他并没有损坏的部分。

诺贝尔奖得主Herbert Simon对稳定系统提出以下观点:

  1. 稳定的复杂系统通常采用一种层次结构的形式,其中各个系统都是由更简单的子系统构成,而各个子系统则是由更简单的下一级子系统构成。
  2. 稳定的复杂系统几乎都是可以分解的。
  3. 稳定的复杂系统几乎总是只由几个不同类型的子系统组成,并以不同的结合方式组织。
  4. 能正常工作的稳定系统几乎总是由能正常工作的简单系统发展而来。

这也说明的组合的重要性。

使用组合时,要避免建立非常依赖其他对象的对象,也即是避免依赖性

使用太多组合也会导致更大的模型复杂性。这和继承中模型复杂性的问题类似。

假设汽车由一个发动机、一个音响、和一个车门组成。发动机又包含活塞和火花塞。音响包含收音机和CD播放器。车门包含一个把手。再往下一个层次,收音机还需要一个调谐开关,车门把手包含一把锁等。多层次详细的组合树也会导致模型复杂性的问题。

小结

很多知名的OO设计人员指出,应该尽可能的地使用组合,而只在必要的情况下才使用继承。 尽管更多的情况下使用组合比使用继承更合适,但是继承和组合都是很好的设计方式,需要应用到适当的上下文环境中。

 



扫描下面二维码关注公众号,干货大集合:

文章目录
  1. 1. 面向对象和面向过程的区别
  2. 2. 设计类
  3. 3. 类设计指导原则
  4. 4. 利用对象设计好的系统
  5. 5. 继承和组合