设计原则与思想:面向对象(11 讲)
1. 什么是面向对象编程(OOP)
- 面向对象编程是一种
编程范式
或编程风格
。它以类或对象
作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性
,作为代码设计和实现的基石
2. 什么是面向对象编程语言
- 面向对象编程语言是
支持类或对象的语法机制
,并有现成的语法机制,能方便地实现面向对象编程四大特性
(封装、抽象、继承、多态)的编程语言
3. 面向对象编程和面向对象编程语言之间的关系
- 面向对象编程一般使用面向对象编程语言来进行,但是,不用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的
4. UML 统一建模语言
- 学习成本较高,可以适当简单学习
5. 封装、继承、多态、抽象分别解决哪些编程问题
* 封装
- 概念:封装也叫作
信息隐藏
或者数据访问保护
,类
通过暴露有限的访问接口
,授权外部仅能
通过类提供的方法(函数)
来访问内部的信息和数据- 作用:
- 保护数据不被随意修改,提高代码的
可维护性
- 仅暴露有限的必要接口,提高
类的易用性
- 封装特性,必须使用
访问控制权限
- Demo: 金融系统简化版虚拟钱包
public class Wallet {
private String id;// 钱包唯一编号
private long createTime;// 钱包创建时间
private BigDecimal balance;// 钱包余额
//BigDecimal 对数字进行精度计算,返回的是对象,不能通过传统 + - * / 计算,而是必须调用相应的方法
private long balanceLastModifiedTime;// 上次钱包余额变更时间
// 构造函数,完成对象初始化。Java 构造函数的名称必须与类名相同, 包括大小写; 构造函数没有返回值, 也不能用 void 修饰;
public Wallet() {this.id = IdGenerator.getInstance().generate();// 全局唯一 ID 生成器
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;//0 用于和 0 比较大小
this.balanceLastModifiedTime = System.currentTimeMillis();// 获取时间,毫秒级}
public String getId() { return this.id;}
public long getCreateTime() { return this.createTime;}
public BigDecimal getBalance() { return this.balance;}
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime;}
public void increaseBalance(BigDecimal increasedAmount) {if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);// 添加数值
this.balanceLastModifiedTime = System.currentTimeMillis();}
public void decreaseBalance(BigDecimal decreasedAmount) {if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);// 减去数值
this.balanceLastModifiedTime = System.currentTimeMillis();}
}
- 解析
- 从业务的角度来说,id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动,所以,我们并没有在 Wallet 类中,暴露 id、createTime 这两个属性的任何修改方法,比如 set 方法。所以,
在 Wallet 类的构造函数内部将其初始化设置好
,而不是通过构造函数的参数来外部赋值。
- 对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所以,我们在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。对于 balanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候,这个属性才会被修改。所以,我们把 balanceLastModifiedTime 这个属性的修改操作完全
封装
在了 increaseBalance() 和 decreaseBalance() 两个方法中,不对外暴露任何修改这个属性的方法
和业务细节。这样也可以保证 balance 和 balanceLastModifiedTime 两个数据的一致性。
抽象
- 概念:抽象是如何隐藏方法的具体实现,让使用者只需要关心调用哪些方法,不需要知道具体如何实现
- 抽象可以通过
接口类
(Java 中的 interface 关键字)或抽象类
(Java 中的 abstract 关键字)来实现- 作用:
- 提高代码的可扩展性、维护性,修改时不需要改变定义,减少代码的改动范围
- 抽象是处理复杂系统的有效手段,能有效过滤掉不必要关注的信息
- Demo: 图片存储功能
public interface IPictureStorage {
// void 是空,在方法声明中表示该方法没有返回值
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}
public class PictureStorage implements IPictureStorage {
// ... 省略其他属性...
@Override
public void savePicture(Picture picture) {...}
@Override
public Image getPicture(String pictureId) {...}
@Override
public void deletePicture(String pictureId) {...}
@Override
public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) {...}
}
- 解析
- 在上面的这段代码中,我们利用 Java 中的 interface 接口语法来实现抽象特性
- 调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑
- 举个简单例子,比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名,如果更换存储设备,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl(),那即便内部存储方式修改了,我们也不需要修改命名。
继承
- 概念:继承用来表示
类与类之间 is- a 关系
,可分为单继承和多继承
- 实现继承,需要语言的语法支持,Java 使用
extends
关键字来实现继承,C++ 使用冒号
(class B : public A),Python 使用paraentheses()
,Ruby 使用 <- 单继承:Java、PHP、C#、Ruby 等
- 多继承:C++、Python、Perl 等
- 作用:提高
代码复用性
,比如通用的方法抽象到父类- 缺点:过度使用继承,继承层次过深、过复杂,就会导致代码的可读性、可维护性变差
- 解决思路:
多用组合少用继承
多态
- 概念:
父类,被多个子类继承,如果父类的某个方法,在多个子类中被重写,表现出不同功能,就是多态(同一个类的不同子类表现不同形态)
- 实现方法:
继承 + 方法重写
-Demo1
- 利用
接口类
语法(C++ 不支持)-Demo2
- 利用
duck-typing
语法(仅有 Python、JavaScript 支持)-Demo3
- 作用:多态可以提高代码的
扩展性
和复用性
- Demo1
// 继承 + 方法重写
public class DynamicArray {
private static final int DEFAULT_CAPACITY = 10;
protected int size = 0;
protected int capacity = DEFAULT_CAPACITY;
protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
public int size() { return this.size;}
public Integer get(int index) {return elements[index];}
//... 省略 n 多方法...
public void add(Integer e) {ensureCapacity();
elements[size++] = e;
}
protected void ensureCapacity() {//... 如果数组满了就扩容... 代码省略...}
}
public class SortedDynamicArray extends DynamicArray {
@Override
public void add(Integer e) {ensureCapacity();
int i;
for (i = size-1; i>=0; --i) { // 保证数组中的数据有序
if (elements[i] > e) {elements[i+1] = elements[i];
} else {break;}
}
elements[i+1] = e;
++size;
}
}
public class Example {
// 静态方法,直接类名. 方法调用
public static void test(DynamicArray dynamicArray) {dynamicArray.add(5);
dynamicArray.add(1);
dynamicArray.add(3);
for (int i = 0; i < dynamicArray.size(); ++i) {System.out.println(dynamicArray.get(i));
}
}
//main()方法是 Java 应用程序入口方法,程序运行,第一个执行的方法就是 main()方法
public static void main(String args[]) {DynamicArray dynamicArray = new SortedDynamicArray();
test(dynamicArray); // 打印结果:1、3、5
}
}
- 解析
- 多态这种特性也需要编程语言提供特殊的语法机制来实现。在上面的例子中,我们用到了三个语法机制来实现多态。
- 第一,支持父类对象引用子类对象,也就是可以将 SortedDynamicArray 传递给 DynamicArray。
- 第二,支持继承,也就是 SortedDynamicArray 继承了 DynamicArray,才能将 SortedDyamicArray 传递给 DynamicArray
- 第三,支持子类可以重写(override)父类中的方法,也就是 SortedDyamicArray 重写了 DynamicArray 中的 add() 方法。
- 通过这三种语法机制配合在一起,我们就实现了在 test() 方法中,子类 SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArray 的 add() 方法,也就是实现了多态特性。
- Demo2
// 利用接口类实现
public interface Iterator {String hasNext();// 检测序列是否还有元素
String next();// 获取序列下一个元素
String remove();// 将迭代器新返回的元素删除}
public class Array implements Iterator {private String[] data;
public String hasNext() { ...}
public String next() { ...}
public String remove() { ...}
//... 省略其他方法...
}
public class LinkedList implements Iterator {
private LinkedListNode head;
public String hasNext() { ...}
public String next() { ...}
public String remove() { ...}
//... 省略其他方法...
}
public class Demo {private static void print(Iterator iterator) {while (iterator.hasNext()) {System.out.println(iterator.next());
}
}
public static void main(String[] args) {Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}
- 解析
- 在这段代码中,Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator
- 我们通过传递不同类型的实现类(Array、LinkedList)到 print(Iterator iterator) 函数中,支持动态的调用不同的 next()、hasNext() 实现。
- Demo3
class Logger:
def record(self):
print(“I write a log into file.”)
class DB:
def record(self):
print(“I insert data into db.”)
def test(recorder):
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)
- 解析
- 从这段代码中看出,duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但是只要它们都有定义了 record() 方法,就可以被传递到 test() 方法中
- 在实际运行的时候,执行对应的 record() 方法。也就是说,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口。
- 扩展:
- Java,PHP 不支持多继承原因:
多重继承有副作用:
菱形继承 (钻石问题)
假设类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,那么此时类 D 会继承 B、C 的方法,那对于 B、C 重写的 A 中的方法,类 D 会继承哪一个呢?这里就会产生歧义。
- Python 多继承的实现方法:
针对多继承时,多个父类的同名方法,
Python 采用 MRO
,在多继承时,判断方法、属性的调用路径
在当前类中找到方法,就直接执行,不再搜索; 如果没有找到,就查找下一个类是否有对应方法,如果找到就直接执行,不再搜索; 直到最后一个类,没找到就报错