设计原则与思想: 面向对象(理论二)

144 阅读7分钟

前言:

封装, 抽象, 继承, 多态分别解决哪些编程问题?

需要知道四大特性, 每个特性存在的意义和目的, 以及它们能解决哪些编程问题

一. 封装

封装特性: 信息隐藏或数据访问保护

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));  
    }  
 }  
 
 
}

以上例子, 我们用到了三个语法机制来实现多态, 分别是:

  1. 支持父类对应引用子类对象, 也就是可以将"SortedDynamicArray"传递给"DynamicArray";
  2. 编程语言要支持继承, 也就是"SortedDynamicArray"继承了"DynamicArray",才能够将前者传递给后者.
  3. 要支持子类可以重写父类中的方法, 也就是"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,其它两个就没有存在的意义。从四大特性可以看出,面向对象的终极目的只有一个:可维护性。易扩展、易复用,降低复杂度等等都属于可维护性的实现方式。