type
status
date
slug
summary
tags
category
icon
password
复习第二章 OO基本概念 2.1 基本概念2.1.1 对象Object2.1.2 类Class 2.1.3 方法Method 2.1.4 消息(message) 2.1.5 封装(encapsulation) 2.1.6 继承(inheritance) 2.1.7 多态和重载 2.1.8 动态联编(dynamic binding) 2.2 对象 2.2.1 对象的特性(property) 2.2.2 对象标识(object identifier) 2.2.3 复合对象 2.2.4 对象持久化 2.3 类和实例2.3.1 什么是类 2.3.2 类的实例 第三章 OO程序设计3.1 OOPL中的类和对象3.1.1 类的定义3.1.2 类的使用3.1.3对象的创建3.1.4 对象的结构3.1.5 类对象和反射3.1.6 消息传递补充3.2 类和继承3.2.1 超类 superclass 和子类 subclass3.2.2 继承的传递性3.2.3 单继承和多继承3.2.4 子类继承方案3.2.5 泛化和特化3.2.6 替换原则3.2.7 重置/改写 overriding3.2.8 静态类和动态类3.2.9 Java中的实现继承3.2.10 软件复用机制3.2.11 接口和抽象类3.2.12 继承的形式3.3 多态 Polymorphism3.3.1 多态变量3.3.2 多态的形式3.3.3 重载与类型转换 UML类图第四章 面向设对象设计模式4.3 创建型模式4.3.1 简单工厂→工厂方法 4.3.2 工厂方法 Factory Method4.3.4 单例模式 Singleton4.4 结构性模式4.4.1 Adapter(适配器)模式4.4.2 Decorator(装饰器)模式4.4.4 Bridge (桥梁)模式 4.5 行为型模式4.5.1 观察者模式 Observer 4.5.2 STRATEGY(策略)模式 优缺点简单工厂工厂方法单例模式桥模式适配器策略模式装饰者模式观察者模式第五章 面向设对象设计原则5.1 开—闭原则OCP: Open-Closed Principle ⭐5.2 里氏代换原则LSP: Liskov Substitution Principle ⭐5.3 依赖倒置原则DIP: Dependency Inversion Principle ⭐5.4 组合复用原则CRP: Composition Reuse Principle5.5 迪米特法则LoD: Law of Demeter5.6 接口隔离原则ISP: Interface Segregation Principle5.7 单一职责原则SRP其他资料2023回忆版
复习
题型:简答题+补全代码题+设计题
重点概念题整理第二章 OO基本概念
2.1 基本概念
2.1.1 对象Object
对象是独立存在的客观事物,它由一组属性和一组操作构成。
属性和操作是对象的两大要素。属性是对象静态特征的描述,操作是对象动态特征的描述。
属性一般只能通过执行对象的操作来改变。
操作又称为方法或服务,它描述了对象执行的功能。
2.1.2 类Class
对象按照不同的性质划分为不同的类
同类对象在数据和操作性质方面具有共性
把一组对象的共同特性加以抽象并存贮在一个类中
类是对象之上的抽象,有了类之后,对象则是类的具体化,是类的实例
类是静态概念,对象是动态概念
2.1.3 方法Method
定义于某一特定类上的操作与规则
具有同类的对象才可为该类的方法所操作
这组方法表达了该类对象的动态性质
而对于其它类的对象可能无意义,乃至非法
规则,说明了对象的其他特征之间是怎样联系的,或者对象在什么条件下是可行的
方法也称作行为(behavior)
2.1.4 消息(message)
对另一个对象的操作在于选择一对象并通知它要作什么
该对象“决定”如何完成这一任务
在其所属类的方法集合中选择合适的方法作用于其身
所谓“操作一个对象”并不意味着直接将某个程序作用于该对象 ,而是利用传递消息,通知对象自己去执行这一操作 ,接收到消息的对象经过解释,然后予以响应
发送消息的对象不需要知道接收消息的对象如何对请求予以响应
2.1.5 封装(encapsulation)
所有信息都存贮在对象中
- 即其数据及行为都封装在对象中
影响对象的唯一方式是执行它所属的类的方法即执行作用于其上的操作
信息隐藏(information hiding)
- 将其内部结构从其环境中隐藏起来
- 要是对对象的数据进行读写,必须将消息传递给相应对象,对象调用自身相应的方法对自身数据进行读写
当使用对象时,不必知道对象的属性及行为在内部是如何表示和实现的,只须知道它提供了那些方法(操作)即可
2.1.6 继承(inheritance)
继承是一种使用户得以在一个类的基础上建立新的类的技术
新的类自动继承旧类的属性和行为特征,并可具备某些附加的特征或某些限制
新类称作旧类的子类,旧类称作新类的超类
继承机制的强有力之处还在于它允许程序设计人员可重用一个未必完全符合要求的类,允许对该类进行修改而不至于在该类的其它部分引起有害的副作用
是其它语言所没有的
2.1.7 多态和重载
在收到消息时对象要予以响应,不同的对象收到同一消息可以产生完全不同的结果
多态是指能够在不同上下文中对某一事物(变量、函数或对象)赋予不同含义或用法
多态一般分为继承多态、重载多态、模板多态
重载多态和模板多态是静态多态,即多态行为是在编译期决定的
而继承多态是一种动态多态的形式,即多态行为可以在运行时动态改变
2.1.8 动态联编(dynamic binding)
联编(binding)是把一个过程调用和响应这个调用而需要将执行的代码加以结合的过程
联编在编译时刻进行的叫静态联编(static binding)
动态联编则是在运行时(run time)进行的,因此,一个给定的过程调用和代码的结合直到调用发生时才得以进行,因而也叫迟后联编(late binding)
这个考了
2.2 对象
对象指是一个实体
- 它能够保存一个状态(或称信息或数据)
- 它能提供一系列操作(或称行为),这些操作或能检查或能影响对象的状态。
能够表示现实或抽象的事物
- 具有良好定义的责任和良好定义的行为
- 具有良好定义的接口
2.2.1 对象的特性(property)
对象的属性和方法称作对象的特性(property)
属性值即对象的状态
方法即对象的行为
对象的性质
对象具有封装性(encapsulation)
信息隐藏(information hiding)
对象具有自治性(autonomy)
对象具有通信性
对象具有暂存性
对象具有持久性
2.2.2 对象标识(object identifier)
缩写为OID
是将一个对象和其它对象加以区别的标识符
一个对象标识和对象永久结合在一起,不管这个对象状态如何变化,一直到该对象消亡为止
面向对象程序设计语言中的OID
强调对象标识的表达能力
用变量名充当标识
可寻址性和标识这两个概念做了混合
强类型变量,像C++,JAVA
Employ emp=new Employ();
非强类型的变量
var emp=new Employ()
直接标识与间接标识
直接标识就是变量的值即为要标识的对象
间接标识指变量的值不是要标识的对象而是该对象的指针
2.2.3 复合对象
指一个对象的一个属性或多个属性引用了其他对象
- 复合对象也称作复杂对象
- 说一个对象O1引用了另外一个对象O2,意味着O1的一个属性的值是O2
- O1中引用O2的属性的值是O2的对象标示
委托delegation
- 是复合对象的一个特例,在委托方式下可有两个对象参与处理一个请求,接受请求的对象将责任委托给它的代理者
组合
一个对象可以由其它对象构造
聚合
描述对象间具有相互关系的另一种方式
例如在家庭关系中,将男人、女人和孩子组合在一起建立起一个聚合关系称作“家庭”
组合与聚合的区别
组合:
语义规则:a-part-of
其中整体负责部分
而每个部分对象也仅与一个整体对象联系
是更强形式的聚合
聚合是同质的:
语义规则:has-a
部分可以脱离整体而存在
例如 摄影协会和会员的关系
2.2.4 对象持久化
要长久保存的对象,也就是持久对象(persistent object)
- 持久对象不随着创建它的进程结束而消亡
- 在外存中存贮
不需要长期保存的暂存对象(transient object)
2.3 类和实例
2.3.1 什么是类
类就是这些具有相同或相似行为或数据结构的对象的共同描述
类是若干对象的模板,并且能够描述这些对象内部的构造
属于同一个类的对象具有相同数据结构及行为
类一一是一组具有相同属性特征的对象的抽象描述(抽象的概念)。
类的性质
类的名标识一个类
在同一个系统环境中,类的名能够唯一标识一个类
类必须具有一个成员集合:属性、方法、方法的操作接口
类的属性的域:基本类、用户定义的类
支持信息隐藏
2.3.2 类的实例
一个实例是从一个类创建而来的对象
属于某个类的对象称为该类的一个实例(instance),每个实例具有一个对象标识
类和对象间具有instance-of关系
类描述了这个实例的行为(方法)及结构(属性)
每个实例可由该类上定义的操作(方法)来操纵
类及实例的特征
同一个类的不同实例
- 具有相同的数据结构
- 承受的是同一方法集合所定义的操作,因而具有相同的行为
同一个类的不同实例可以持有不同的值,因而可以有不同的状态
实例的初始状态(初值)可以在实例化中确定
HOMEWORK
3.31第三章 OO程序设计
复习不用看第一、二章,只看第三章即可
3.1 OOPL中的类和对象
3.1.1 类的定义
实例
惯例和建议
- 惯例 类名首字母大写 数据字段private Accessor(Getter)/Setter/Is
- 声明次序建议-可读性 先列出主要特征,次要的列在后面 私有数据字段列在后面
- 类主题的变化:接口 不提供实现 接口定义新类型,可以声明变量 类的实例可以赋值给接口类型变量
可视性修饰符 – 封装性
可视性修饰符public, private, protected
Java和C++中,封装性由程序员确定
数据类型两个方面:外部用户/实现者
封装的实现:
1、修改属性的可见性来限制对属性的访问
2、为每个属性创建一对赋值(setter)方法和取值(getter) 方法,用于对这些属性的访问
3、在setter和getter方法中,加入对属性的存取限制
类的数据字段/类属性
- Java同时也包含无对象(objectless)变量及无对象方法,称为类变量和类方法
- 类变量被一个类的所有实例共享,典型使用方式是定义常量
- 这样的常量不仅被该类的所有对象共享,而且通常被声明为公共变量,以供给该程序中所有的对象和类使用
- 除了用来定义常量以外,类变量也可以用来使得某类所有的实例(对象)共享一份数据
- 定义: 静态成员,声明为 static 的类成员或者成员函数便能在类的范围内共同享,我们把这样的成员称做静态成员和静态成员函数。
- 性质: 静态成员是属于类的而不是对象的;静态数据成员的值在函数退出时不消失,作为下一次调用时的初值,所以可在各对象之间传递数值。
- 举例理解:
汽车制造商为统计汽车的产量,可以在在汽车类 car 类中增加用于计数的静态数据成员变量,比如在 car 类中声明一
static int number
;初始化为 9。这个number 就能被所有 car 的实例共用。在 car 类的构造函数里加上number++在 car 类的析构函数里加上 number--。那么每生成一个 car 的实例,number 就加,每销毁一个 car 的实例 (汽车报废) ,number 就减一,这样,number 就可以记录在市场上 car 的实例个数。
- 静态成员函数: 不能访问非静态成员,无 this/不能使用 this 引用
- 构造和析构函数不能为静态成员。
- Java中的类方法和类变量
- Java同时也包含无对象(objectless)变量及无对象方法,称为类变量和类方法
- 类变量的典型使用方式是定义常量
- 类方法可以被视为可独立于某类的所有对象而进行调用的方法,而非传递给该类的对象的消息
内部类
将一个类的定义放在另一个类的定义内部(Java内部类。C++嵌套类)
- 语义差别
- Java内部类被连接到外部类的具体实例上,并且允许存取其实例和方法
- C++仅是命名手段,限制和内部类相关的特征可视性
- 内部类是非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性
- 内部类的作用
1.可以无条件地访问外围类的所有元素
2.实现隐藏
3.可以实现多重继承
4.通过匿名内部类来优化简单的接口实现
3.1.2 类的使用
- 类的使用有两种形式
- 允引 (class A is a client of class B)
- 继承 (class A is a descendant of class B)
- 实现允引的两个方法
- 用户和供应商 (Client and supplier) :在B 类中声明A类的实例
- 功能调用 (Feature call): 在 B 类中调用 A 类的方法。
注:类的继承作为重要概念,在以后会有详细的介绍
3.1.3对象的创建
创建对象语法
C++:
PlayingCard * aCard = new PlayingCard(Diamond, 3);
Java, C#:
PlayingCard aCard = new PlayingCard(Diamond, 3);
Smalltalk:
aCard <- PlayingCard new.
对象数组的创建
- 数组的分配和创建
- 数组所包含对象的分配和创建
Java:new仅创建数组。数组包含的对象必须独立创建。
构造函数
- 构造函数作用: 初始化新创建对象 优点:确保初始化之前不会被使用,防多次调用 Java/C++:名称,返回值
New语法
初始化
内存划分:对象创建之初
声明在栈内存中,new在堆内存中
内存操作:为属性赋值
初始化常量
可直接初始化
在构造函数中初始化
- Const与final区别
- const常量,不允许改变。
- Final
- 仅断言相关变量不会赋予新值
- 并不能阻止在对象内部对变量值进行改变
析构函数
内存回收
- C++: 使用 new 创建的对象在堆上为它分配空间,并且需要显式的释放空间,delete对象的时候才会调用对象的析构函数。
- Java,C#,Smalltalk: 垃圾回收机制,时刻监控对象的操作,对象不再使用时,自动回收其所占内存。通常在内存将要耗尽时工作。
3.1.4 对象的结构
简单类型与引用类型
- 简单类型 : int a = 1; 变量保存的是数据
- 引用类型 : int *a = 1; 变量保存的是数据地址,a 指向 10 所在的地址。
对象同一和对象相等
- 对象同一:具有相同的标识
- 对象相等:两个对象的标识不同,但具有相同的值
浅克隆和深克隆
- 浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象,如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
- 深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
输出的结果说明int类型的变量aInt和UnCloneA的实例对象unCA的clone结果不一致,int类型是真正的被clone了,因为改变了b2中的aInt变量,对b1的aInt没有产生影响,也就是说,b2.aInt与b1.aInt已经占据了不同的内存空间,b2.aInt是b1.aInt的一个真正拷贝。
相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCA和b1.unCA是仅仅指向同一个对象的不同引用!从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。
大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为“影子clone”。要想让b2.unCA指向与b2.unCA不同的对象,而且b2.unCA中还要包含b1.unCA中的信息作为初始信息,就要实现深度clone。
默认的克隆方法为浅克隆,只克隆对象的非引用类型成员。
3.1.5 类对象和反射
类本身也是一个对象。
这个特殊的对象也有其属性和方法,我们称之为类属性和类方法。
普通对象的属性和方法称作实例属性和实例方法。
元类
- 描述类的类 如果将类看作一个对象,该类必定是另一个特殊类的实例 这个特殊类我们称作元类
- 类也是对象 每个类一定是某个元类的实例 对于类A,它是元类 A class 的实例 A class 也是对象
- 元类的优点
- 概念上一致 只用一个概念——对象就可表述系统中所有成分
- 使类成为运行时刻一部分有助于改善程序设计环境
- 继承的规范化 类与元类的继承采用双轨制
作为对象的类
Java,Smalltalk中类本身是对象。那么什么类代表了对象所属的类别,即这个类是什么类。有一个特殊的类,一般称为Class,这就是类的类。
这个类所具有的行为:创建实例,返回类名称,返回类实例大小,返回类实例可识别消息列表。
类的操作
获取类对象
C++
typeinfo aClass = typeid(AVariable);
Java
Class aClass = aVariable.getClass();
获取父类
Class parentClass = aClass.getSuperclass(); // Java
字符串类名称
char * name = typeinfo(aVariable).name(); // C++
String internalName=aClass.getName();//Java
String descriptiveName=aClass.toString();
反射
HOMEWORK
4.283.1.6 消息传递
消息:对象间互相请求互相协作的途径
在两个实体间通信,其必要条件是在它们之间至少存在一条通道,并且遵守同一种通信协议
发送一条消息时,应指明信道或给出信道的决定方法,最常用的是用接收方的标识(如名字)来命名信道
发送消息的流程
考虑对象A向对象B发送消息,也可以看成对象A向对象B请求服务
- 对象A要明确知道对象B提供什么样的服务
- 根据请求服务的不同,对象A可能需要给对象B一些额外的信息,以使对象B明确知道如何处理该服务
- 对象B也应该知道对象A是否希望它将最终的执行结果以报告形式反馈回去
消息传递语法
消息-〉接收器。
响应行为随接收器不同而不同。
伪变量
伪变量:接收器并不出现在方法的参数列表中,而是隐藏于方法的定义之中。只有当必须从方法体内部去存取接收器的数值时,才会使用伪变量(pseudo-variable)
Java,C++:this (this隐含指向调用成员函数的对象)
Eiffel:Current
Smalltalk,object-c:self
好像在使用同类的实例。
补充
静态、动态语言
动静态语言的差异在于:变量或数值是否具有类型这种特性
静态类型语言:类型在编译时绑定于变量。
动态类型语言(有时也称为非类型语言,untyped language):类型决定于数值,而与变量无关。
Java是静态语言
动态类型面向对象语言:一般在变量使用之前不需要声明变量类型,而变量的类型通常是由被赋的值的类型决定,类型的检查是在运行时做的
静态类型面向对象语言:类型的检查是在编译时做的,编程时需要明确声明变量类型
方法绑定
方法绑定:一个对象接到消息将消息名与要执行的代码进行绑定。
静态方法绑定:在编译时刻进行方法绑定
动态方法绑定:在运行时刻进行方法绑定
对比:动态绑定灵活性相对静态绑定来说要高,因为它在运行之前可以进行选择性的绑定。但动态绑定的执行效率要低些,实现起来更加复杂。
静态方法绑定和动态方法绑定优缺点
- 静态方法绑定:
- 缺点:耦合性高;灵活性低
- 优点:执行速度快,效率高;编译时触发,可以提前知道程序错误
- 动态方法绑定:
- 优点:灵活性高,运行时候才触发,可修改性强;耦合性低
- 缺点:执行速度和效率比静态方法绑定低;不可以提前预知错误,有编译器不可预见的错误而产生很多程序漏洞
3.2 类和继承
- 向上传递,向下传递
向下传递:子类所具有的数据和行为总是作为与其相关的父类的属性的扩展( extension)(即更大的集合)。子类具有父类的所有属性以及其他属性
概念
在已有的类的基础上建立新的类的方法
重复使用和扩展那些经过测试的已有的类,实现重用
可以增强处理的一致性(是一种规范和约束)
作用
代码复用
概念复用。共享方法的定义。
“is-a”检验,检验两个概念是否为继承关系
3.2.1 超类 superclass 和子类 subclass
已存在的类通常称作超类
新的类通常称作子类
子类不仅可以继承超类的方法,也可以继承超类的属性
如果超类中的某些方法不适合与子类,则可以重置这些方法
定义
如果类C能使用类B中的方法及属性,称B是C的超类,C是B的子类,也称类C继承类B
3.2.2 继承的传递性
如果Dog是Mammal的派生类,而Mammal又是Animal的派生类,则Dog不仅继承了Mammal的属性,同时也继承了Animal的属性
直接超类(子类)
间接超类(子类)
3.2.3 单继承和多继承
单继承
如果一个类只有一个直接超类
单继承构成类之间的关系是一棵树
- JAVA只支持单继承,为什么?
因为当一个类同时继承两个父类时,两个父类中有相同的功能,则子类对象调用该功能时,无法确定运行的是哪一个
多继承
如果一个类有多于一个的直接超类
多继承构成的类之间的关系是一个网格
C++规定,多继承时,直接超类的构造函数的调用次序是:
(1)抽象超类。若有多个抽象超类,按继承说明次序从左到右。
(2)非抽象超类:若有多个非抽象超类,按继承顺序从左到右。
3.2.4 子类继承方案
访问权限修饰符:Public, Private and Protected
区别 :
public:可以被所有其他类所访问
private:只能被自己访问和修改
protected:自身,子类可以访问
3.2.5 泛化和特化
泛化generalization:
通过将若干类的所共享的公共特征抽取出来,形成一个新类,并且将这个类放到类继承层次的上端以供更多的类所重用
抽取出的超类称作抽象类(abstract class)抽象类不能创建实例
抽象类没有实例
特化(specialization)
新类作为旧类的子类
若B是A的子类,则从A到B是特化,B到A是泛化
接口和抽象类
与类一样,接口可以继承于其他接口,甚至可以继承于多个父接口。
虽然继承类和实现接口并不完全相同,但他们非常相似,因此使用继承这一术语来描述这两种行为
抽象方法:介于类和接口之间的概念。
定义方法但不实现。
创建实例前,子类必须实现父类的抽象方法。
Java,C#:abstract
C++:virtual
3.2.6 替换原则
考试必考
用子类替换父类(任何父类出现的地方都能用子类来替代)
在静态类型语言中
父类和子类数据类型的关系?
- 子类实例必须拥有父类的所有数据成员。
- 子类的实例必须至少通过继承实现父类所定义的所有功能。
- 这样,在某种条件下,如果用子类实例来替换父类实例,那么将会发现子类实例可以完全模拟父类的行为,二者毫无差异。
指对于类A和类B,如果B是A的子类,那么在任何情况下都可以用类B来替换类A
可替换性是面向对象编程中一种强大的软件开发技术
- 可替换性的意思是:变量声明时指定的类型不必与它所容纳的值类型相一致
- 这在传统的编程语言中是不允许的,但在面向对象的编程语言中却常常出现
继承和替换原则的引入对编程语言的影响:存储分配
例:
分配方案
考了这个优缺点,没讲过不知道
考过简答题(三点+对三点的解释)
- 最小静态空间分配:只分配基类所需的存储空间。
C++保证变量x只能调用定义于Window类中的方法,不能调用定义于TextWindow类中的方法。
定义并实现于Window类中的方法无法存取或修改定义于子类中的数据,因此不可能出现父类存取子类的情况。
C++规则: 对于指针(引用)变量:当消息调用可能被改写的成员函数时,选择哪个成员函数取决于接收器的动态数值。 对于其他变量:关于调用虚拟成员函数的绑定方式取决于静态类(变量声明时的类),而不取决于动态类(变量所包含的实际数值的类)。
- 最大静态空间分配:无论基类还是派生类,都分配可用于所有合法的数值的最大的存储空间。
分配变量值可能使用的最大存储空间。
- 动态内存分配:只分配用于保存一个指针所需的存储空间。在运行时通过堆来分配数值所需的存储空间,同时将指针设为相应的合适值。
堆栈中不保存对象值。
堆栈通过指针大小空间来保存标识变量,数据值保存在堆中。
指针变量都具有恒定不变的大小,变量赋值时,不会有任何问题。
Smalltalk、Java都采用该方法。
3.2.7 重置/改写 overriding
overridding 可以重新修正从超类继承下来的属性及方法
子类有时为了避免继承父类的行为,需要对其进行改写
- 语法上:子类定义一个与父类有着相同名称且类型签名相同的方法。
- 运行时:变量声明为一个类,它所包含的值来自于子类,与给定消息相对应的方法同时出现于父类和子类。
改写与替换结合时,想要执行的一般都是子类的方法。
在重置中,只有操作的实现体被改变,而操作的说明及表示仍与以前一样
重置时子类不改变超类中的已有接口定义
重置机制是基于动态联编的
根据当前该对象对应的类型?
重定义redefination:操作的表示和操作的实现体将都改变
改写、重载、重定义
相同点:
都是函数名相同,有不同的实现
区别:
发生在同一个类中,类型签名不同:重载 overloading
发生在父类和子类中,类型签名相同:改写/重置 overridding
发生在父类和子类中,类型签名不同:重定义 redefination
重载通常是在编译时解析的,而改写则是一种运行时机制。对于任何给定的消息,都无法预言将会执行何种行为,而只有到程序实际运行的时候才能对其进行确定。
改写(重置,重写)是动态绑定,重定义是静态绑定
代替与改进(重置/改写的方式)
- 代替(replacement):在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。
- 改进(refinement):实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。
几乎所有的语言在构造函数中都使用改进语义。即,子类构造函数总是调用父类的构造函数,来保证父类的数据字段和子类的数据字段都能够正确地初始化。
延迟方法(抽象方法/c++中:纯虚方法)
如果方法在父类中定义,但并没有对其进行实现,那么我们称这个方法为延迟方法。
延迟方法的一个优点是可以使程序员在比实际对象的抽象层次更高的级别上考虑与之相关的活动。
延迟方法更具实际意义的原因:在静态类型面向对象语言中,对于给定对象,只有当编译器可以确认与给定消息选择器相匹配的响应方法时,才允许发送消息给这个对象。
Java,C#:
abstract
C++:
virtual = 0
遮蔽
改写与遮蔽存在着外在的语法相似性
类似于重载,改写区别于遮蔽的最重要的特征就是,遮蔽是在编译时基于静态类型解析的,并且不需要运行时机制。
几种语言需要对改写显式声明,如果不使用关键字,将产生遮蔽。
在c++中:
如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)
如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
改写overriding、遮蔽和重定义
差异:
1.改写 父类与子类的类型签名相同,并且在父类中将方法声明为虚拟的。
2.遮蔽 父类与子类的类型签名相同,但是在父类中并不将方法声明为虚拟的。是在编译时基于静态类型解析的,并且不需要运行时机制 。
3.重定义 父类与子类的类型签名不同。(当子类定义了一个与父类具有相同的名称但类型签名不同的方法时,发生重定义。)类型签名的变化是重定义区别于改写的主要依据。
希望禁止子类对父类方法的改写?
java:
final
c#:
sealed
重载和改写的区别
- 方法的改写/覆盖是子类和父类之间的关系,而重载一般是同一类内部多个方法间的关系
- 方法的改写/覆盖一般是两个方法间的,而重载时可能有多个重载方法
- 改写/覆盖的方法有相同的方法名和形参表,而重载的方法只能有相同的方法名,不能有相同的形参表
- 改写/覆盖时区分方法的是根据被调用方法的对象,而重载是根据参数来决定调用的是哪个方法
- 用final修饰的方法是不能被子类覆盖的,只能被重载
继承和构造函数
Java等语言:只要父类构造函数不需要参数,父类的构造函数和子类的构造函数都会自动地执行。当父类需要参数时,子类必须显示地提供参数。在java中通过
super
这个关键字来实现。注意:默认调用父类无参构造函数,即使子类调用的是有参的构造函数
3.2.8 静态类和动态类
- 变量的静态类
指用于声明变量的类。静态类在编译时就确定下来,并且再也不会改变
- 变量的动态类
指与变量所表示的当前数值相关的类。动态类在程序的执行过程中,当对变量赋新值时可以改变
对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性不是基于接收器的当前动态数值,而是基于接收器的静态类来决定的
合法性:编译时能否通过,根据静态类决定
例:
某年的考题……
响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。
3.2.9 Java中的实现继承
继承的缺点:
- 读者追溯执行流程变得的十分困难(尤其是具有很多世代的深度继承树之中,树的底层的方法的代码分散在高层的祖先当中)
- 所有子类都是和父类紧密联系的
示例一:
考试考过如何将该段代码修改为正确的代码,上例只需要从子类中删除重写的setSize方法即可(父类的setSize会调用子类的setWidth方法的) 重置/改写overriding
//省略示例二、三
示例四:框架
对于一类相似问题的骨架解决方案
通过类的集合形成,类之间紧密结合,共同实现对问题的可复用解决方案
继承和改写的强大能力体现
3.2.10 软件复用机制
继承 A is-a B
无法知道一个方法是否可以合法地应用于集合。
继承无法防止用户使用父类的方法来操纵新的数据结构:FirstElement
使用继承构建数据抽象的代码的简洁性是继承的一个优点
组合 A has-a B
提供了一种利用已存在的软件组件来创建新的应用程序的方法。
较为简单的一种技术。只需考虑在特定的数据结构中需要执行哪些操作,而无需考虑列表类所定义的所有操作
组合、继承的优缺点
- 组合的优缺点:
- 缺点:容易产生过多的对象;为了可以组合多个对象,必须仔细对接口进行定义
- 优点:耦合性低,可修改性强;灵活性高;通过包含的对象去调用方法,封装性好
- 继承的优缺点:
- 优点:子类可以重写父类的方法来实现对父类的扩展;代码简洁
- 缺点:耦合性高,当父类代码改变时,继承的子类代码也需要改变;灵活性差;父类的细节对子类可见封装性差
- 应用场景:
如果语义满足“IS-A”且行为上满足LSP(里氏代换原则),则使用继承
如果语义上满足“HAS-A”,那么使用组合,或者按组合复用原则,优先考虑组合
3.2.11 接口和抽象类
抽象类
在定义方法时可以只给出方法头,而不必给出方法体、即方法实现的细节,这样的方法被称为抽象方法。
特性:
抽象方法必须使用关键字abstract修饰(Java语言),包含抽象方法的类必须声明为抽象类。
抽象类不能被实例化。
Java语言规定,子类必须实现其父类中的所有抽象方法,否则该子类也只能声明为抽象类。(可以不全部实现抽象父类中的抽象方法)
抽象类中可以不包含抽象方法。
父类不是抽象类,但在子类中可以添加抽象方法,但子类需声明为抽象类。
抽象类中可以声明static属性和方法。
接口
只包含静态最终变量和抽象方法,或者是只有抽象方法的时候,抽象类可以等同于一个接口,接口是特殊的抽象类
接口有比抽象类更好的特性:
1.可以被多继承
2.设计和实现完全分离
3.更自然的使用多态
4.更容易搭建程序框架
5.更容易更换实现
接口里的常量和方法的定义可以省略,因为所有定义在接口中的常量都默认为public、static和final。所有定义在接口中的方法默认为public和abstract,所以可以不用修饰符限定它们。
特性:
实现类可以实现多个接口。(多个接口间用”,”分隔)
接口不可以被实例化。
实现类必须实现接口的所有方法(抽象类除外)。
接口中的变量都是静态常量。
(JAVA语法规定,接口中的变量默认自动隐含是public static final)
Java接口中不能有方法体实现。
3.2.12 继承的形式
特化子类化(子类型化):子类是父类的一个实例,ppt里有详细定义
规范子类化:继承方法定义,实现各不相同
5种坏的(不提倡的)继承方式:
构造子类化:
泛化子类化
扩展子类化
限制子类化
变体子类化
结合子类化(C++支持多继承,java不支持多继承,通过接口实现)
特殊化继承 is-a 理想的方式
新类是基类的一种特定类型,它能满足基类的所有规范。
用这种方式创建的总是子类型,并明显符合可替换性原则。is-a
规范化继承 理想的方式
规范化继承用于:保证派生类和基类具有某个共同的接口,即所有的派生类实现了具有相同方法界面的方法
基类中既有已实现的方法,也有只定义了方法接口、留待派生类去实现的方法
派生类只是实现了那些定义在基类却又没有实现的方法。也就是说,基类定义了某些操作,但并没有去实现它,只有派生类才能实现这些操作。
在这种情况下,基类有时也被称为抽象规范类。
辨认方法:基类中只是提供了方法界面,并没有实现具体的行为,具体的行为必须在派生类中实现。
构造继承 ×
当继承的目的只是用于代码复用时,新创建的子类通常都不是子类型。称为构造子类化。
一个类可以从其基类中继承几乎所有需要的功能,只是改变一些用作类接口的方法名,或是修改方法中的参数列表。
构造子类化经常违反替换原则(形成的子类并不是子类型)。
泛化子类化 ×
派生类扩展基类的行为,形成一种更泛化的抽象。
扩展继承 √
派生类只是往基类中添加新行为,并不修改从基类继承来的任何属性。
(泛化子类化对基类已存在的功能进行修改或扩展,扩展子类化则是增加新功能)
由于基类的功能仍然可以使用,而且并没有被修改,因此扩展继承并不违反可替换性原则,用这种方式构建的派生类还是派生类型 。
限制继承 ×
如果派生类的行为比基类的少或是更严格时,就是限制继承。
常常出现于基类不应该、也不能被修改时。
限制继承可描述成这么一种技术:它先接收那些继承来的方法,然后使它们无效。
违反可替换性原则
变体子类化 ×
两个或多个类需要实现类似的功能,但他们的抽象概念之间似乎并不存在层次关系。
合并继承
多继承
合并两个或者更多的抽象特性来形成新的抽象。
一个类可以继承自多个基类的能力被称为多重继承。
3.3 多态 Polymorphism
3.3.1 多态变量
多态变量
指可以引用多种对象类型的变量
这种变量在程序执行过程可以包含不同类型的数值
对于动态类型语言,所有的变量都可能是多态的
对于静态类型语言,则是替换原则的具体表现
继承是子类使用父类的方法,而多态则是父类使用子类的方法。 (把父类当做子类来用)
3.3.2 多态的形式
- 重载(overloading):同一个类中,类型签名(参数类型、参数顺序和返回值类型)区分
- 改写(overriding):父类和子类中,相同类型签名
- 多态变量(polymmorphic variable)
Parent variable = new Child();
- 简单多态变量
- 接收器变量 多态变量最常用的场合是作为一个数值,用来表示正在执行的方法内部的接收器。
this
- 反多态(向下造型)
- 纯多态(多态方法)
向下造型是处理多态变量的过程,并且在某种意义上这个过程的取消操作就是替换。
会考阅读程序的简答题
先看静态类确定方法,更多阅读程序做题方法参考java笔记
- 泛型 Generic(考试不怎么考,但日常中编程中需要学会使用)
是指具有在多种数据类型上皆可操作的含意 。既编写的代码可以在不同的数据类型上重用。
泛型原理:
- Java泛型是在编译器的层面上实现的
- 在编译后,通过擦除,将泛型的痕迹全部抹去。
- 擦除:将任何具体的类型信息都消除,唯一知道的就是正在使用一个对象
- JVM不知道泛型的存在
泛型的好处:
1.避免由于数据类型的不同导致方法或类的重载。
2.类型安全。 泛型的一个主要目标就是提高 程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
3.消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
4.多态的另一种表现形式。(参量多态)
3.3.3 重载与类型转换
重载(overloading):同一个类中,方法名相同,函数类型签名不同 函数类型签名:是关于函数参数类型、参数顺序和返回值类型的描述
关于重载的解析,是在编译时基于参数值的静态类型完成的。不涉及运行时机制。
继承层次的例子(考试会出类似的题目)
编译器匹配步骤
第一步,找精确匹配(形参实参精确匹配的同一类型)找到,则执行,找不到转第二步。
第二步,找可行匹配(符合替换原则的匹配,即实参所属类是形参所属类的子类),没找到可行匹配,报错;只找到一个可行匹配,执行可行匹配对应的方法;如果有多于一个的可行匹配,转第三步。
第三步,多个可行匹配两两比较,如果一个方法的各个形参,或者:与另一个方法对应位置形参所属类相同,或者:形参所属类是另一个方法对应位置形参所属类的子类,该方法淘汰另一个方法。
最后,如果只剩一个幸存者,执行;如果多于一个幸存者,报错。
又一个例子(某次期末考试题目,不难捏)
UML类图
说不考,但会结合在后面设计模式里让画图(嗯…这怎么不算考呢
第四章 面向设对象设计模式
↑这个讲的真的不错,可惜我在自己整理完代码才发现的/(ㄒoㄒ)/~~
重点 时间不够就讲七个,不讲的不用复习啦
4.3 创建型模式
4.3.1 简单工厂→工厂方法
开始封装创建对象的代码
优点:实现了责任的分割
利用判断逻辑,决定实例化哪一个产品类
客户端可以免除直接创建产品类对象的责任,仅仅使用该产品
缺点:没有完全做到“开—闭”原则OCP
一旦新增产品,必须修改工厂的代码
但是客户代码不需要修改
4.3.2 工厂方法 Factory Method
使用多态来应对
提供一个抽象工厂的接口
具体工厂分别负责创建具体产品对象
增加新的产品只需要相应增加新的具体工厂类
在简单工厂模式中,一个果园只有一个园丁,管理全部果树 现在我们果园扩大规模,雇佣新园丁,规定每个新园丁专门负责一种水果,也就是说:葡萄园的园丁只管理葡萄、苹果园的园丁只管理苹果。这样园丁们各负其责,互不相关。
一个抽象产品类,可以派生出多个具体产品类。
一个抽象工厂类,可以派生出多个具体工厂类。
每个具体工厂类只能创建一个具体产品类的实例。
优点:
它能够使工厂可以自主确定创建何种产品对象。而且如何创建一个具体产品的细节完全封装在具体工厂内部,符合高内聚,低耦合。
在系统中加入新产品时,无需修改抽象工厂和抽象产品提供的接口,无需修改客户端,也无需修改其他的具体工厂和具体产品。
缺点:
在添加新产品时,需要编写新的具体产品类(其实这不算一个缺点,因为这是不可避免的),要增加与之对应的具体工厂类。
4.3.4 单例模式 Singleton
单例模式的特点:
单例类只可有一个实例。
他必须给自己创立自己这个唯一的一个实例。
它必须给所有其他的类提供自己这一实例。
有些对象我们只需要一个:注册表、日志、线性池、缓存
饿汉式 eager mode
懒汉式 lazy mode
没有考虑并发,两个线程同时执行时可能会出错(在if那一步),需要加一个同步锁
单例模式讲的很少,私以为不需要知道具体使用的代码框架类图等,遂只看了上面(下面这个例子是2022年的题目,让判断出打印池是应该使用单例模式即可) 失策了,考了单例模式懒汉式和饥饿式的优缺点,让写了线程安全的懒汉式代码,寄! 这些老师上课都没讲捏
在操作系统中,打印池是一个用于管理打印任务的应用程序,通过打印池用户可以删除、中止或者改变任务的优先级,在一个系统中只允许运行一个打印池对象,如果重复创建打印池则抛出异常。现使用单例模式来模拟实现打印池的设计,请根据类图编程实现该系统,并写出相应Java代码
4.4 结构性模式
4.4.1 Adapter(适配器)模式
将一个类的接口转换成客户希望的另一个接口。
Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
也叫做包装器Wrapper
对象适配
依赖于对象组合(组合模式)
例:希望交通工具继承自Car类,然而Truck类与Car类不匹配,添加适配器Lorry
类适配
依赖于继承
画出类图并写出代码框架(不用写具体实现)
缺省适配器
当你不想/不能实现接口的所有方法时:利用缺省适配器类,提供这些方法的缺省实现
从这个类再派生出的子类就可以不去实现那些不想实现的方法了
4.4.2 Decorator(装饰器)模式
示例一
咖啡馆 咖啡小料定价问题
示例二
考试考过,不会再考同一个题目
假设需要打印发票 sales ticket , 发票有抬头、正文和脚注,发票抬头可以是企事业单位,发票号等,脚注也一样,可能有很多不同种类的脚注需要打印。
如果发票格式固定那也就没必要继续讨论了,现在的问题是,不同的客户需要的发票或者收据的抬头或脚注,需要的条目是不一样的,有的需要著明单位,有的只需要发票号,但是脚注需要开票人, 等等
客户的要求是动态;不过发票的正文是不会变化的,是固定的
优点:
- 比继承更灵活 从为对象添加功能的角度来看,装饰模式比继承来得更灵活。继承是静态的,而且一旦继承是所有子类都有一样的功能。而装饰模式采用把功能分离到每个装饰器当中,然后通过对象组合的方式,在运行时动态的组合功能,每个被装饰的对象,最终有哪些功能,是由运行期动态组合的功能来决定的。
- 更容易复用功能 装饰模式把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样实现装饰器变得简单,更重要的是这样有利于装饰器功能的复用,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,从而复用装饰器的功能。
- 简化高层定义 装饰模式可以通过组合装饰器的方式,给对象增添任意多的功能,因此在进行高层定义的时候,不用把所有的功能都定义出来,而是定义最基本的就可以了,可以在使用需要的时候,组合相应的装饰器来完成需要的功能。
- 会产生很多细粒度对象 装饰模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样会产生很多细粒度的对象,而且功能越复杂,需要的细粒度对象越多
4.4.4 Bridge (桥梁)模式
容易与装饰模式弄混
设计原则:组合优先
继承复用的优点:
可以很容易的修改或扩展父类的实现
继承复用的缺点:
继承破坏封装,因为父类的实现细节完全暴露给子类(白盒复用)
父类的实现发生改变,则子类必受牵连
继承是静态的,不能在运行时发生改变,不灵活
组合复用的优点:
不破坏封装,这种复用是黑盒复用,因为成员对象的内部细节对新对象保密
所需依赖少(只依赖接口)
是动态的,可以把成员对象动态替换为另一个类型相同的对象
组合复用的缺点:
对象数量会增加
使用委托(delegation)会使得系统复杂
有些人既是经理,又是学生,比如某位在读MBA的老总
换一个角度:雇员、经理、学生其实都是角色的一种,人拥有角色
“汽车”拥有某种或某些“用途”
“汽车”和“用途”独立变化,互不影响
设计原则:封装可变性
发现代码容易变化的部分,封装之,使它和不容易变化的部分独立开来
示例一
使用桥梁模式的效果:
比如增加一种鸟类“鹅”,相应的要增加一种游泳的行为“红掌拨清波”
只需要增加一个鸟类的子类“鹅”
增加一个游泳的行为“红掌拨清波”
设置“鹅”的飞翔行为为“飞不起来”
设置“鹅”的游泳行为为“红掌拨清波”
原有代码不需要改动!
示例二
到咖啡厅喝咖啡,咖啡有中杯和大杯之分,同时还有加奶和不加奶之分
用继承来解决:
这四个具体实现(中杯 大杯 加奶 不加奶)之间有概念重叠 因为有中杯加奶,也有中杯不加奶,如果再在中杯这一层再实现两个继承,,很显然混乱,扩展性极差
用bridge模式来实现:将抽象和行为分开(加奶不加奶属于行为)
示例三
“小朋友画画”
4.5 行为型模式
4.5.1 观察者模式 Observer
观察者模式在对象之间定义了一个一对多的依赖,当一个对象改变了状态,就通知依赖于它的其他对象,并自动地更新这些对象。
观察者模式也称为发布-订阅模式
① 报社的业务就是出版报纸
② 向某家报社订阅报纸,只要他们有新报纸出版,就会给
你送来。只要你是他们的用户,你就会一直收到新报纸
③ 当你不想再看报纸的时候,取消订阅,他们就不会再送
新报纸
④ 只要报社还在运营,就会一直有人(或单位)向他们订
阅报纸或取消订阅报纸
松耦合的威力
当两个对象是松耦合的,它们彼此之间能够交互,但是相互了解很少。
观察者模式提供了主题和观察者之间的松耦合设计。因为主题只知道观察者实现了某个接口(即Observer接口),主题不需要知道具体观察者是谁、作了些什么或其它任何细节。要增加新的观察者或删除观察者,主题不会受到任何影响,不必修改主题代码。
可以独立地复用主题和观察者,它们之间互不影响,即是松耦合的。
例:设计气象站
考试考的话,一定要知道会写subject中的三个方法
考试:给一个例子(某一年:股票),画类图,稍微具体一点(类似设计气象站的那种类图),别太抽象
4.5.2 STRATEGY(策略)模式
最简单的一种
Strategy模式又称Policy模式,它定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。该模式使算法可以独立于使用它的客户而变化。
- 环境(Context)
持有一个Strategy类的引用。可定义一个接口让Strategy访问它的数据。
- 抽象策略(Strategy)
给出所有的具体策略类所需的接口,通常由一个接口或抽象类实现。
- 具体策略(ContreteStrategy)
包装了相关的算法或行为,实现Strategy接口的某个具体类。
示例一
某网上商城举行促销活动:图书每本折扣1元,服装类8折,家居类9折,护肤品没折扣。
顾客结算时,计算购物车中所有商品的总金额。
示例二
A店除了正常日不打折,在节假日会推出满300减100,全场8折等活动
练习
要设计一个系统,对输入的数据实现排序,系统提供几种排序方法。系统运行时根据用户选择的排序方法对数据进行排序。(另外,可能会常常增加新的排序算法、或删除某个算法)
优缺点
简单工厂
优点:帮助封装;解耦,实现了客户端和具体实现类的解耦;
缺点:
没做到完全开-闭,客户端符合但增加新产品的时候需要修改工厂的代码。
可能增加客户端的复杂度,由于是从客户端在调用工厂的时候,传入选择的参数,这就说明客户端必须知道每个参数的含义,也需要理解每个参数对应的功能处理。这就要求必须在一定程度上,向客户暴露一定的内部实现细节。
不利于产生系列产品;
工厂方法
针对不同的产品提供不同的工厂,定义一个用于创建对象的接口,让实现类(具体工厂类)决定实例化哪一个类,使一个类的实例化延迟到实现类
优点:帮助封装,封装了创建具体对象的工作
使得客户代码“针对接口编程”,保持对变化的“关闭”
缺点:
对于使用工厂方法的类(客户端),如果要更换另外一种产品,仍然需要修改实例化的具体工厂类,客户端不满足开-闭原则。
单例模式
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
优点:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
桥模式
组合优先;将抽象部分与它的实现部分分离,使它们都可以独立地变化;防止类爆炸;
符合的原则:组合复用原则(组合优先),封装可变性,开-闭原则。
适配器
新老代码不一致,将一个类的接口转换成客户希望的另外一个接口。
优点:使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
策略模式
算法可以在不影响到客户端的情况下发生变化,让算法的变化独立于使用算法的客户,平滑的处理算法切换。
符合的原则:组合复用原则,封装可变性,开-闭原则,单一职责原则。
装饰者模式
动态添加新职责,防止类爆炸,
优点:比继承灵活,不改变原有对象情况下扩展原有对象功能。
通过不同装饰类以及这些类的排列组合可以实现不同的效果。
松耦合。
符合的原则:开-闭原则,接口隔离原则,单一职责原则。
观察者模式
触发联动,通知,监听同一主题,有变化时通知观察者更新自己。
优点:当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
第五章 面向设对象设计原则
重点捏 七个原则 考设计原则的有意思+给例子能对应上
重构 Refactoring
5.1 开—闭原则OCP: Open-Closed Principle ⭐
软件组成实体应该是可扩展的,但是不可修改的。
Software entities should be open for extension, but closed for modification.
开放-封闭法则认为应该试图去设计出永远也不需要改变的模块。
开闭原则的提出是为了满足当系统需求发生了变化时,希望已经设计好的系统可以不修改原有类的代码,而是通过增加新的类来应对需求的变化。即在设计系统时,就要提前预知到可能发生变化的点,对这样的变化点要分离出一层抽象层,然后系统只针对这个抽象层进行编程,而不对变化点(实现层)编程,当有新的变化点时,只需要新写一个变化点的类,让它继承原有的抽象层的类,这样的变化不会对系统其它部分有影响。
使用策略模式
5.2 里氏代换原则LSP: Liskov Substitution Principle ⭐
凡是父类适用的地方子类应当也适用
使用指向基类(超类)的引用的函数,必须能够在不知道具体派生类(子类)对象类型的情况下使用它们。
(Function That Use References To Base(Super) Classes Must Be Able To Use Objects Of Derived(Sub) Classes Without Knowing It)
例子:鸵鸟非鸟,企鹅非鸟
原因一:对类的继承关系的定义没有搞清楚。
面向对象的设计关注的是对象的行为,它是使用“行为”来对对象进行分类的,只有行为一致的对象才能抽象出一个类来。
类的继承关系就是一种“Is-A”关系,实际上指的是行为上的“Is-A”关系,可以把它描述为“Act-As”。
再来看“正方形不是长方形”例子,正方形在设置长度和宽度这两个行为上,与长方形显然是不同的。长方形的行为:设置长方形的长度的时候,宽度保持不变,设置宽度的时候,长度保持不变。正方形的行为:设置正方形的长度的时候,宽度随之改变;设置宽度的时候,长度随之改变。所以,如果我们把这种行为加到基类长方形的时候,就导致了正方形无法继承这种行为。我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。
“鸵鸟非鸟”基本上也是同样的道理。我们一讲到鸟,就认为它能飞,有的鸟确实能飞,但不是所有的鸟都能飞。问题就是出在这里。如果以“飞”的行为作为衡量“鸟”的标准的话,鸵鸟显然不是鸟;如果按照生物学的划分标准:有翅膀、有羽毛等特性作为衡量“鸟”的标准的话,鸵鸟理所当然就是鸟了。鸵鸟没有“飞”的行为,我们强行给它加上了这个行为,所以在面对“飞越黄河”的需求时,代码就会出现运行期故障。
原因二:设计要依赖于用户要求和具体环境。
继承关系要求子类要具有基类全部的行为:这里的行为是指落在需求范围内的行为。
所有派生类的行为功能必须和使用者对其基类的期望保持一致,如果派生类达不到这一点,那么必然违反里氏替换原则。
子类适用的地方不要求父类一定能适用。
5.3 依赖倒置原则DIP: Dependency Inversion Principle ⭐
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
(High level modules should not depend upon low level modules, both should depend upon abstractions. Abstractions should not depend upon details, details should depend upon abstractions.)
要针对接口编程,不要针对实现编程。
代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。
实现方式:在代码中使用抽象类,而将具体类放在配置文件中
实例一
某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,但违背了开闭原则。现使用依赖倒转原则对其进行重构。
实例二
实例三
(某次期末考题)
灯和开关,开关能控制灯,……
缺点:耦合太紧密,Light发生变化将影响ToggleSwitch。
将Light作成Abstract,然后具体类继承自Light。
优点:ToggleSwitch依赖于抽象类Light,具有更高的稳定性,而BulbLight与TubeLight继承自Light,可以根据“开放-封闭”原则进行扩展。
只要Light不发生变化,BulbLight与TubeLight的变化就不会波及ToggleSwitch。
缺点:如果用ToggleSwitch控制一台电视就很困难了。总不能让TV继承自Light吧。
DIP常见应用
- 1、AGP插槽。主板和显卡之间关系的抽象。主板和显卡通常是使用AGP插槽来连接的,这样,只要接口适配,不管是主板还是显卡更换,都不是问题。
- 2、驾照。司机和汽车之间关系的抽象。有驾照的司机可以驾驶各种品牌汽车。
- 3、电源插座。
设计模式中最能体现DIP原则的是抽象工厂模式。在抽象工厂模式中,工厂和产品都可以是抽象的,如果客户要使用的话,只要关注于工厂和产品的接口即可,不必关注于工厂和产品的具体实现。
5.4 组合复用原则CRP: Composition Reuse Principle
优先使用(对象)组合,而非(类)继承。
(Favor Composition Over Inheritance.)
详见4.4.4
5.5 迪米特法则LoD: Law of Demeter
一个软件实体应当尽可能少的与其他实体发生相互作用。
每个软件单元对其它单元尽可能少了解,而且仅限于那些与自己密切相关的单元
又叫做最少知识原则,“只与你直接的朋友们通信”,不要跟“陌生人”说话,也就是信息隐藏、封装
在迪米特法则中,对于一个对象,其朋友包括以下几类:
(1) 当前对象本身(this);
(2) 以参数形式传入到当前对象方法中的对象;
(3) 当前对象的成员对象;
(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5) 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
如果某对象是调用其他的方法的返回结果,不要调用该对象的方法【相当于向另一个对象的子部分发请求(增加了我们直接认识的对象数目);原则要我们改为要求该对象为我们做出请求,这么一来,我们就不需要认识该对象的组件了。】
迪米特法则在详细设计中的体现
- 尽量降低成员的访问权限 public -> protected -> private
- 限制局部变量的有效范围
实例:修改不满足的例子
问题关键出在了 Someone 里的 operation1 通过 Friend 类返回 Stranger 调用了Stranger 的 operation3,导致了多余的耦合,即 Someone 和 Stranger 产生了依赖关系,因为 Someone 在知道 Friend 类的条件下却还需要额外知道Stranger 类才能实现功能,这显然是不合理的,因为稍作修改,Someone就可以不需要了解Stranger 类了,而是在 Friend 中加入一个委托 Stranger进行operation3 操作的forward()方法,而 Someone 的 operation1 只需要将 Friend 类当作参数传进来即可实现操作。
5.6 接口隔离原则ISP: Interface Segregation Principle
一个类对另一个类的依赖应当建立在最小的接口上
- 角色的合理划分
- 一个接口应当简单的只代表一个角色
- 接口污染
- 过于臃肿的接口是对接口的污染
- 不要把没什么关系的接口合并在一起
接口隔离原则和迪米特法则的关系
- 迪米特法则要求尽量限制通信的广度和深度
- 而对接口进行分割,使其最小化,避免对客户提供不需要的服务,当然是符合迪米特法则的
也体现了高内聚、低耦合
如果类的接口不是内聚的,就表示该类具有“胖”的接口。
ISP建议客户程序不应该看到它们作为单一的类存在。客户程序看到的应该是多个具有内聚接口的抽象基类。
5.7 单一职责原则SRP
优良的系统设计强调模块间保持低耦合、高内聚的关系
一个类,最好只做一件事,只有一个引起它变化的原因
- 如果一个类承担的职责过多,等于把这些职责耦合在了一起
- 这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏
单一职责原则分析
一个模块(或者大到类,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
设计原则名称 | 设计原则简介 | 重要性 |
单一职责原则
(Single Responsibility Principle, SRP) | 类的职责要单一,不能将太多的职责放在一个类中 | ★★★★☆ |
开闭原则
(Open-Closed Principle, OCP) | 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能 | ★★★★★ |
里氏代换原则
(Liskov Substitution Principle, LSP) | 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象 | ★★★★☆ |
依赖倒转原则
(Dependency Inversion Principle, DIP) | 要针对抽象层编程,而不要针对具体类编程 | ★★★★★ |
接口隔离原则
(Interface Segregation Principle, ISP) | 使用多个专门的接口来取代一个统一的接口 | ★★☆☆☆ |
合成复用原则
(Composite Reuse Principle, CRP) | 在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系 | ★★★★☆ |
迪米特法则
(Law of Demeter, LoD) | 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互 | ★★★☆☆ |
其他资料
Sduonline2023回忆版
- 改写 重载在应用、定义、编译及联编方面的区别等 用代码
- java和c++ 内存分配方式 四种优缺点
- 改进和代替的区别 代码表述
- 单例模式 饿汉式 懒汉式优缺点 懒汉式线程安全的代码
- 编译器匹配步骤 判断使用哪个方法
(5 * 6 = 30)
设计模式大题(20*3)
装饰器模式(给车加安全系统 音响系统等)
观察者模式(股票)
设计原则修改设计 DIP(从键盘读入数据在显示器上显示)
补充代码(10)
桥梁模式(卷子有点小问题,笑死)
- 作者:Rainnn
- 链接:https://tangly1024.com/article/88483958-5e1f-457b-a63e-fb00ecdad249
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。