特征 | 传统看法 | 新思维 |
对象 | 一堆数据和方法 具有方法的数据 | 拥有责任的实体,这些责任定义了对象的行为。(我们应该关注对象的意图行为,而不是对象如何实现。) 对象的设计多关注应该做什么,而不是如何实现它。 对象设计的基本观点:关注动机而不是实现。(关注对象要做什么,能帮助我们免于过早地操心实现细节,从而将这些实现细节隐藏起来。) |
封装 | 隐藏数据 | 任何形式(数据隐藏、类隐藏、实现隐藏)的隐藏。 封装可以用于在行为中包含变化。 考虑设计中哪些点是可变的,然后封装变化点。(即:发现变化并将其封装) |
继承 | 特化和复用 | 对象分类的一种方式。 封装变化的方法。 |
对象
对象真正的威力并不在于继承,而是来自封装行为。
面向对象范型以对象概念为中心,一切都集中在对象上。使用对象的优点在于可以定义自己负责自己的事物,确定自己的责任。理解对象的最佳方式,是将其看成具有责任的东西。有一条好的设计规则:对象应该自己负责自己,而且应该清楚地定义责任。从概念、规约、实现三个不同视角来观察对象:
1) 在概念层次上,对象是一组责任。或者说对象是具有责任的一个实体,这些责任定义了对象的行为。
2) 在规约层次上,对象是一组可以被其他对象或对象自己调用的方法(也称为行为)。
3) 在实现层次上,对象是代码和数据,以及他们之间的计算交互。
概念视角对对象的定义有助于使我们关注对象的意图行为,而不是对象如何实现。关注对象要做什么,还能帮助我们免于过早的操作实现细节,从而将这些实现细节隐藏起来。
关于动机而非实现,是设计模式中反复出现的主题。将实现隐藏在接口之后,实际上是将对象的实现与使用它们的对象解耦。
抽象
抽象是指强调实体的本质、内在的属性。在系统开发中,抽象指的是在决定如何实现对象之前的对象的意义和行为。使用抽象可以尽可能避免过早考虑一些细节。
类实现了对象的数据(即状态)和行为的抽象。
隐藏问题/实体的详细/具体部分,只提取最重要的部分。对Problem Space进行抽象,强调问题/实体的简单化。把现实世界的问题/实体抽象化,定义对象并面向这些对象模拟业务领域。依赖倒置原则(DIP):面向抽象编程,而不是面向具体编程。
JAVA的两种类:
抽象类:不可实例化,不能生成对象,但可以实现具体的方法。一定是用来继承的。
具体类:可以实例化,具体类,不是用来继承的。
抽象类与子类的关系,实际是模板方法的应用。在抽象类与具体类的树形结构中,树枝节点是抽象类,树叶节点是具体类。
代码重构建议(如果B是A的子类,建立C,抽象类或接口)
抽象类,应当拥有尽可能多的共同代码
在一个从抽象类到多个具体类的继承关系中,共同的代码应当尽量向上移动到位于金字塔顶端抽象类中。
这样,可以提供代码的复用率,由于代码在公共的超类,而不是子类中出现,在代码发生改变时,程序员只需修改一个地方,程序可维护性更好。
抽象类,应当包含尽可能少的数据
数据,会占用内存,而且复用率不高,所以尽量将数据向下移动,即尽量不要置于超类中,而是要置于具体类中。
针对抽象编程的核心思想
针对抽象的编码,主要针对超类,而不是具体类的编程。
重点,解决代码复用问题。
封装
封装性就是把对象的属性和服务结合成一个独立的相同单位,并尽可能隐蔽对象的内部细节,包含两个含义:
1) 把对象的全部属性和全部服务结合在一起,形成一个不可分割的独立单位(即对象)。
2) 信息隐蔽,即尽可能隐蔽对象的内部细节,对外形成一个边界〔或者说形成一道屏障〕,只保留有限的对外接口使之与外部发生联系。
封装的原则在软件上的反映是:要求使对象以外的部分不能随意存取对象的内部数据(属性),从而有效的避免了外部错误对它的"交叉感染",使软件错误能够局部化,大大减少查错和排错的难度。
在程序设计中,封装是指将一个数据和与这个数据有关的操作集合在一起,形成一个能动的实体——对象,用户不必知道对象行为的实现细节,只需根据对象提供的外部接口访问对象即可。因此,从用户的观点来看,这些对象的行为就像包含在一个“黑匣子”里,是隐蔽的、看不见的。
封装有两个基本前提:一是对象必须是完备的,即必须能够表示整个概念,描述整个问题的各个方面;二是私有性。大多数对象都需要对其内部的数据和过程限制处理权限。私有性不但可以保证对对象的正确操作,而且有利于查错,使一些对象的成员函数私有化,减少它们被处理的机会,于是在追踪时许多地方都可以不必去查。
封装不是面向对象语言所独有的特性,但这种在单一实体中把数据结构和行为捆绑在一起的能力,使封装比传统的把数据结构和行为分离的语言更加清晰、更强有力。
对象是数据和操作的集合,良好的封装保证只有通过对象的操作才能修改对象的状态/数据。封装保护了对象的数据,防止对对象状态的误操作。封装不仅仅是简单的数据封装,而是将复杂的、可变的业务功能的具体实现通过模块/接口的形式来隐藏。把系统表现为有自身固有属性的对象和对象之间的相互关系。
另外还可以封装变化、封装对象的请求(Command模式)、封装对象的状态(State模式)等等。
封装应该被视为“任何形式的隐藏”。换句话说,可以是隐藏数据,但还可以是隐藏实现细节、派生类、设计细节、实例化规则等。封装做的是隐藏,而抽象做的是简化。
封装的目标
把对象的属性和服务结合成一个独立的系统单位。
尽可能隐蔽对象的内部细节,只向外部提供接口,降低对象间的耦合度。
对可变性的封装原则(Encapsulation of Variation Principle. EVP)。
信息隐蔽
1) 可见性控制;
数据不能被对象的使用者直接访问,只允许通过由对象提供的方法或代码访问数据。
2) 对象将愿意提供给所有对象的公共服务公开化;
3) 对象也提供仅限于特定对象的其它服务(受保护的和私有的);
封装的重要意义
使对象能够集中而完整地描述并对应一个具体事物;
体现了事物的相对独立性,使对象外部不能随意存取对象的内部数据;
对象内部的修改对外部的影响很小,减少了修改引起的波动效应;
公开静态的、不变的服务,而把动态的、易变的服务隐藏起来;
封装变化
应对变化,使用功能分解,用模块化封装变化。我们虽然无法预测会发生什么变化,但是通常可以预期哪里会发生变化。面向对象的巨大优点之一,就是可以封装这些变化区域,从而更容易地将代码与变化产生的影响隔离开来。
继承
继承是代码复用的一种机制,使得很容易地定义具有相近功能的对象族。子类通过继承父类,以复用父类定义的数据和操作。当“新的类”属于“老的类”/“原有的类”的其中的一种时(a kind of),适合使用继承。
继承表明类之间的关系。类不仅仅说明一组对象上的约束,还说明与其他类之间的关系。两个类型可以有共同的特性和行为,但是,一个类型可能包括比另一个类型更多的特性,也可以处理更多的消息。继承表示了基类和派生类之间的相似性。一个基类具有所有由它派生出来的类所共有的特性和行为,而派生类则通过继承重用了基类的特性和行为。
抽象基类提供了公共接口,当希望通过公共接口操作一组(派生)类时就创建抽象基类。
继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
继承性是面向对象程序设计语言不同于其它语言的最重要的特点,是其他语言所没有的。
在类层次中,子类只继承一个父类的数据结构和方法,则称为单重继承。
在类层次中,子类继承了多个父类的数据结构和方法,则称为多重继承。
在软件开发中,类的继承性使所建立的软件具有开放性、可扩充性,这是信息组织与分类的行之有效的方法,它简化了对象、类的创建工作量,增加了代码的可重性。
采用继承性,提供了类的规范的等级结构。通过类的继承关系,使公共的特性能够共享,提高了软件的重用性。
正确使用继承
里氏代换原则(LSP):一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。只有衍生类替换基类的同时软件实体的功能没有发生变化,基类才能真正被复用。
继承分为两种:
类对接口的实现,接口继承;
类对类的继承,实现继承。实现继承,容易被滥用。
对于抽象类,尽量使用合成,而不是继承,来达到复用目的。
参考“组合/聚合复用原则”,举例
继承复用的使用场合
继承代表“一般化、特殊化”关系,抽象的基类代表一般,而衍生的具体类代表特殊。
Peter Coad条件,符合该条件所有内容,才可以使用抽象类:
1、子类是超类的一个特殊种类,而不是超类的一个角色。
Has-A关系应当使用聚合关系;Is-A关系符合继承关系。
Has-A :一个类是另一个类的角色;人---黑社会小弟,老大角色,可以互换:人,可以做黑社会小弟,某时,也可以做老大。
Is-A:一个类是另一个类的一种。鸟---麻雀、天鹅种类,不可以互换:鸟,不可以某时是麻雀,某时是天鹅。
2、永远不会出现需要将子类换成另一个类的子类的情况。
3、子类具有扩展超类的责任,而不具有置换掉、注销掉超类的责任。如果子类需要大量的置换掉超类的行为,则不适合。
4、只有在分类学角度上,才可以使用继承,不要从工具类继承
多态
动态绑定允许你在运行时刻彼此替换具有相同接口的对象,这种可替换性就称为多态。多态使得对象间彼此独立,并可以在运行时刻动态改变它们相互的关系。
依赖倒置原则(DIP):面向抽象编程,而不是面向具体编程。
多态的含义是相同的行为(在基类中定义)在不同的类中有着不同的实现(在子类中实现),或者更彻底的说,多态性就是以相同的指令唤起不同的函数。
当处理类型层次结构时,程序员常常希望不把对象看作是某一特殊类型的成员,而把它看作基本类型的成员,这样就可以编写不依耐于特殊类型的代码,在添加新的子类后也不影响原来的代码,这是扩展面向对象程序以处理新情况的最普通的方法——通过派生新的子类来扩展程序功能,这个能力极大地减少了软件维护的花费(。
多态性使指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
在java语言中,多态性体现在两个方面:由方法重载实现的静态多态性(编译时多态)和方法重写实现的动态多态性(运行时多态)。
1) 编译时多态
在编译阶段,具体调用哪个被重载的方法,编译器会根据参数的不同来静态确定调用相应的方法。
2) 运行时多态
由于子类继承了父类所有的属性(私有的除外),所以子类对象可以作为父类对象使用。程序中凡是使用父类对象的地方,都可以用子类对象来代替。一个对象可以通过引用子类的实例来调用子类的方法。
重写方法的调用原则:java运行时系统根据调用该方法的实例,来决定调用哪个方法。对子类的一个实例,如果子类重写了父类的方法,则运行时系统调用子类的方法;如果子类继承了父类的方法(未重写),则运行时系统调用父类的方法。