前言:
封装, 抽象, 继承, 多态分别解决哪些编程问题?
需要知道四大特性, 每个特性存在的意义和目的, 以及它们能解决哪些编程问题
一. 封装
封装特性: 信息隐藏或数据访问保护
1.1 封装的定义
package org.example.case_01;
import java.math.BigDecimal;
/**
* @Author: Nisy
* @Date: 2023/05/23/12:43
*/
public class Wallet {
/**
* 钱包的唯一编号
*/
private String id;
/**
* 钱包的创建时间
*/
private long createTime;
/**
* 钱包的余额
*/
private BigDecimal balance;
/**
* 上次钱包余额的变更时间
*/
private long balanceLastModifiedTime;
// ...省略其他属性...
public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}
// 注意:下面对get方法做了代码折叠,是为了减少代码所占文章的篇幅
public String getId() {
return this.id;
}
public long getCreateTime() {
return this.createTime;
}
public BigDecimal getBalance() {
return this.balance;
}
public long getBalanceLastModifiedTime() {
return this.balanceLastModifiedTime;
}
/**
* 增加余额
* @param increasedAmount
*/
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
/**
* 减少余额
* @param decreasedAmount
*/
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new RuntimeException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
id, createTime: 在创建钱包的时候已经确定好了, 之后需要改变, 所以不提供set方法
balance: 钱包余额, 从业务角度讲, 余额要么增加, 要么减少, 无需被重新set, 所以这里只对外暴露了2个方法, 一个增加余额: increaseBalance(), 另外一个减少余额: decreaseBalance();
balanceLastModifiedTime: 上一次余额变更时间, 跟balance绑定在一起的, 也无需单独被设置, 所以也没有提供set方法
总结: 对于封装这种特性, 我们需要编程语言本身提供一定的语法机制来支持, 这个语法机制就是: 访问控制权限, 比如Java中: private, protected, public
1.2 封装的意义, 它能解决什么编程问题?
(1). 提高了代码的可维护性
当我们类中的属性不做任何限制, 任何代码都可以访问, 修改类的属性, 会造成修改逻辑散落在各个角落, 势必影响代码的可读性, 可维护性
(2). 提高了类的易用性
给调用者提供有限的方法, 调用者无需了解方法的细节, 减少了调用者的负担, 那么用错的概率就会变小.
二. 抽象
封装主要讲的是如何隐藏信息, 保护数据, 那么抽象就是对如何隐藏方法的具体实现. 让调用者只需关心该方法提供了哪些功能, 不需要知道具体的功能是如何实现的
java中的抽象实现有2种, 分别是: interface接口 和 abstract关键字
2.1 抽象的定义
/**
* 图片存储接口
* @Author: Nisy
* @Date: 2023/05/23/13:01
*/
public interface IPictureStorage {
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}
package org.example.case_01;
/**
* 图片存储实现
* @Author: Nisy
* @Date: 2023/05/23/13:02
*/
public class PictureStorage implements IPictureStorage{
@Override
public void savePicture(Picture picture) {
}
@Override
public Image getPicture(String pictureId) {
return null;
}
@Override
public void deletePicture(String pictureId) {
}
@Override
public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) {
}
}
如上代码, 由IPictureStorage接口定义图片存储相关功能的抽象, 调用者在调用图片存储功能的时候, 只需要知道IPictureStorage接口暴露了哪些方法即可, 无需关心具体的实现.
其实不仅仅只有IPictureStorage才是抽象, 其实PictureStorage本身就是抽象, 因为类中的方法(函数)本身就包裹着具体的实现逻辑
总结: 类的方法是一层抽象, 接口又是一层抽象, 相当于抽象的抽象.
2.2 为什么抽象有时候会被排除在面向对象四大特性之外
抽象是一种通用的设计思想, 不单单表现在面向对象编程中. 该特性不需要编程语言提供特殊的机制, 只需要满足"函数(方法)"即可, 就可以实现抽象. 所以这是它有时候不被看作是面向对象特性之一的原因.
2.3 抽象的意义, 它能解决什么编程问题?
(1). 抽象简化, 降低了复杂度 抽象会让调用者只需要关心功能, 无需关注具体的实现
(2). 抽象在代码设计中, 起到指导作用 很多设计原则都体现了抽象的设计思想, 比如: 基于接口而非实现编程, 开闭原则(对修改关闭, 对扩展开放), 代码解耦.
开闭原则: 抽象面向的是接口, 而不是具体的实现. 当需求变化时, 只需要添加一个接口, 实现该接口即可.(无需修改, 只需扩展)
(3). 方法的命名也需要抽象 比如: getAliyunPictureUrl()就不是一个抽象的命名, 如果我们哪天换成了其他云, 这个命名就要被改动. 所以最好叫做: getPictureUrl()
三. 继承
继承用来表示类之间的is-a关系. 比如: 猫是一种哺乳动物.
继承分为: 单继承和多继承. 单继承表示一个子类只继承一个父类; 多继承表示一个子类可以继承多个父类.如: 猫是哺乳动物, 又是爬行动物.
java中使用extends关键字实现继承, C++使用冒号:, py使用parentheses(), ruby使用<.
java只支持单继承, 为什么有些语言支持多重继承, 有些语言不支持呢?
答: 我们定义一个动物, 既是狗(父类1), 也是猫(父类2), 2个父类都有"叫"这个方法, 当我们调用"叫"方法时, 它就不知道是狗叫还是猫叫了. 这就是多重继承的冲突.
3.1 继承的意义, 它能解决什么编程问题?
继承最大的好处: 代码复用. 如果2个类有共同的属性和方法, 我们就可以将相同的部分抽取到父类中, 让2个子类继承共同的父类.避免代码重写. 不过这一点并不是继承独有的, 我们也可以利用组合关系而非继承关系.比如提取到一个第三方的工具类中.
勿要过度继承,继承层次过深, 容易导致代码的可读性, 可维护性变差.
后面会讲到: 多用组合少用继承
四. 多态
多态指: 子类可以替换父类, 在父类的地方可以用子类替换
4.1 多态特性实现方式一: 继承加方法重写
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/20:53
*/
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() {
//...如果数组满了就扩容...代码省略...
}
}
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/20:55
*/
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;
}
}
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/20:57
*/
public class Example {
public static void main(String args[]) {
DynamicArray dynamicArray = new SortedDynamicArray();
test(dynamicArray); // 打印结果:1、3、5
}
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));
}
}
}
以上例子, 我们用到了三个语法机制来实现多态, 分别是:
- 支持父类对应引用子类对象, 也就是可以将"SortedDynamicArray"传递给"DynamicArray";
- 编程语言要支持继承, 也就是"SortedDynamicArray"继承了"DynamicArray",才能够将前者传递给后者.
- 要支持子类可以重写父类中的方法, 也就是"SortedDynamicArray"重写了父类的add方法
多态特性实现方式二: 接口类
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/21:08
*/
public interface Iterator {
boolean hasNext();
String next();
String remove();
}
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/21:08
*/
public class Array implements Iterator{
@Override
public boolean hasNext() {
return false;
}
@Override
public String next() {
return null;
}
@Override
public String remove() {
return null;
}
}
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/21:09
*/
public class LinkedList implements Iterator{
@Override
public boolean hasNext() {
return false;
}
@Override
public String next() {
return null;
}
@Override
public String remove() {
return null;
}
}
package org.example.case_01.polymorphism;
/**
* @Author: Nisy
* @Date: 2023/05/23/21:09
*/
public class Demo {
public static void main(String[] args) {
Iterator iterator = new Array();
print(iterator);
Iterator linkIterator = new LinkedList();
print(linkIterator);
}
private static void print(Iterator iterator){
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
4.3 多态的意义, 它能解决什么编程问题?
多态: 提高的代码的可扩展性和复用性;
如: 4.2例子, 仅一个print()函数就可以实现遍历打印不同类型(Array, LinkedList)集合的数据, 如果新增加一种类型: HashMap, 只需要实现Iterator接口, 重写hasNext()和next()方法即可, 不用改print()方法.
五. 评论总结
争哥对面向对象的总结完美符合 What/How/Why 模型,我按照模型作下梳理。
封装:
What:隐藏信息,保护数据访问。
How:暴露有限接口和属性,需要编程语言提供访问控制的语法。
Why:提高代码可维护性;降低接口复杂度,提高类的易用性。
抽象:
What: 隐藏具体实现,使用者只需关心功能,无需关心实现。
How: 通过接口类或者抽象类实现,特殊语法机制非必须。
Why: 提高代码的扩展性、维护性;降低复杂度,减少细节负担。
继承:
What: 表示 is-a 关系,分为单继承和多继承。
How: 需要编程语言提供特殊语法机制。例如 Java 的 “extends”,C++ 的 “:” 。
Why: 解决代码复用问题。
多态:
What: 子类替换父类,在运行时调用子类的实现。
How: 需要编程语言提供特殊的语法机制。比如继承、接口类、duck-typing。
Why: 提高代码扩展性和复用性。
3W 模型的关键在于 Why,没有 Why,其它两个就没有存在的意义。从四大特性可以看出,面向对象的终极目的只有一个:可维护性。易扩展、易复用,降低复杂度等等都属于可维护性的实现方式。