1、面向对象的四大特性

1、封装

封装:信息隐藏或者数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据

1.1、示例

下面这段代码是金融系统中一个简化版的虚拟钱包的代码实现
在金融系统中,我们会给每个用户创建一个虚拟钱包,用来记录用户在我们的系统中的虚拟货币量
对于虚拟钱包的业务背景,这里你只需要简单了解一下即可,在面向对象的实战篇中,我们会有单独两节课,利用 OOP 的设计思想来详细介绍虚拟钱包的设计实现

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

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

1.2、解释

从代码中,我们可以发现,Wallet 类主要有四个属性(也可以叫作成员变量),也就是我们前面定义中提到的信息或者数据

  • id 表示钱包的唯一编号
  • createTime 表示钱包创建的时间
  • balance 表示钱包中的余额
  • balanceLastModifiedTime 表示上次钱包余额变更的时间

我们参照封装特性,对钱包的这四个属性的访问方式进行了限制,调用者只允许通过下面这六个方法来访问或者修改钱包里的数据

  • String getId()
  • long getCreateTime()
  • BigDecimal getBalance()
  • long getBalanceLastModifiedTime()
  • void increaseBalance(BigDecimal increasedAmount)
  • void decreaseBalance(BigDecimal decreasedAmount)

之所以这样设计,是因为从业务的角度来说

  • id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动
    所以我们并没有在 Wallet 类中,暴露 id、createTime 这两个属性的任何修改方法,比如 set 方法
    而且这两个属性的初始化设置,对于 Wallet 类的调用者来说,也应该是透明的
    所以我们在 Wallet 类的构造函数内部将其初始化设置好,而不是通过构造函数的参数来外部赋值
  • 对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置
    所以我们在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法
  • 对于 balanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的,只有在 balance 修改的时候,这个属性才会被修改
    所以我们把 balanceLastModifiedTime 这个属性的修改操作完全封装在了 increaseBalance() 和 decreaseBalance() 两个方法中
    不对外暴露任何修改这个属性的方法和业务细节
    这样也可以保证 balance 和 balanceLastModifiedTime 两个数据的一致性

1.3、意义

如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活
但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性
比如某个同事在不了解业务逻辑的情况下
在某段代码中 "偷偷地" 重设了 wallet 中的 balanceLastModifiedTime 属性,这就会导致 balance 和 balanceLastModifiedTime 两个数据不一致

除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性
如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解,而这对于调用者来说也是一种负担
相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多
这就好比,如果一个冰箱有很多按钮,你就要研究很长时间,还不一定能操作正确
相反,如果只有几个必要的按钮,比如开、停、调节温度,你一眼就能知道该如何来操作,而且操作出错的概率也会降低很多

2、抽象

封装主要讲的是如何隐藏信息、保护数据
而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的

2.1、示例

public interface IPictureStorage {

    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) { ... }
}

2.2、解释

在上面的这段代码中,我们利用 Java 中的 interface 接口语法来实现抽象特性
调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑

抽象这个特性是非常容易实现的,并不需要非得依靠接口类或者抽象类这些特殊语法机制来支持
换句话说,并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫作抽象
即便不编写 IPictureStorage 接口类,单纯的 PictureStorage 类本身就满足抽象特性

之所以这么说,那是因为:类的方法是通过编程语言中的 "函数" 这一语法机制来实现的,通过函数包裹具体的实现逻辑,这本身就是一种抽象
调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了
比如我们在使用 C 语言的 malloc() 函数的时候,并不需要了解它的底层代码是怎么实现的

抽象有时候会被排除在面向对象的四大特性之外
抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等,而且这个特性也并不需要编程语言提供特殊的语法机制来支持
只需要提供 "函数" 这一非常基础的语法机制,就可以实现抽象特性、所以它没有很强的 "特异性",有时候并不被看作面向对象编程的特性之一

2.3、意义

如果上升一个思考层面的话,抽象及其前面讲到的封装都是人类处理复杂性的有效手段
在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节
而抽象作为一种只关注功能点不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息

除此之外,抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用,很多设计原则都体现了抽象这种设计思想
比如:基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等

换一个角度来考虑,我们在定义类的方法的时候,也要有抽象思维
不要在 "方法定义" 中暴露太多的实现细节,以保证在某个时间点需要 "改变方法的实现逻辑" 的时候,不用去修改其 "定义"
比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改
相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl(),那即便内部存储方式修改了,我们也不需要修改命名

3、继承

3.1、介绍

继承是用来表示类之间的 is - a 关系,比如猫是一种哺乳动物
从继承关系上来讲,继承可以分为两种模式,单继承和多继承
单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物

为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持

  • 比如 Java 使用 extends 关键字来实现继承
  • C++ 使用冒号(class B : public A)
  • Python 使用 paraentheses()
  • Ruby 使用 <

不过有些编程语言只支持单继承,不支持多重继承,比如 Java、PHP、C#、Ruby 等
而有些编程语言既支持单重继承,也支持多重继承,比如 C++、Python、Perl 等

3.2、意义

继承最大的一个好处就是代码复用
假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分抽取到父类中,让两个子类继承父类,这样两个子类就可以重用父类中的代码,避免代码重复写多遍
不过这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用 "组合关系" 而不是 "继承关系"

如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解
我们代码中有一个猫类,有一个哺乳动物类,猫属于哺乳动物,从人类认知的角度上来说,是一种 is - a 关系
我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且从设计的角度来说,也有一种结构美感

继承的概念很好理解,也很容易使用,但是过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差
为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看 "父类、父类的父类 ..." 的代码
还有子类和父类高度耦合,修改父类的代码,会直接影响到子类

所以继承这个特性也是一个非常有争议的特性,很多人觉得继承是一种反模式,我们应该尽量少用,甚至不用
关于这个问题,在后面讲到 "多用组合少用继承" 这种设计思想的时候,我会非常详细地再讲解,这里暂时就不展开讲解了

4、多态

多态:子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现

4.1、示例 1

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 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 && elements[i] > e; i--) {
            elements[i + 1] = elements[i];
        }
        elements[i + 1] = e;
        size++;
    }
}
public class Demo {

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

    public static void main(String[] args) {
        DynamicArray dynamicArray = new SortedDynamicArray();
        test(dynamicArray); // 打印结果: 1、3、5
    }
}

4.2、解释 1

多态这种特性也需要编程语言提供特殊的语法机制来实现,在上面的例子中,我们用到了三个语法机制来实现多态

  • 支持父类对象可以引用子类对象,也就是可以将 SortedDynamicArray 传递给 DynamicArray
  • 支持继承,也就是 SortedDynamicArray 继承了 DynamicArray,才能将 SortedDyamicArray 传递给 DynamicArray
  • 支持子类可以重写(override)父类中的方法,也就是 SortedDyamicArray 重写了 DynamicArray 中的 add() 方法

通过这三种语法机制配合在一起
我们就实现了在 test() 方法中,子类 SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArray 的 add() 方法,也就是实现了多态特性

4.3、示例 2

public interface Iterator {

    boolean hasNext();

    String next();

    String remove();
}
public class Array implements Iterator {

    private String[] data;

    @Override
    public boolean hasNext() { ... }

    @Override
    public String next() { ... }

    @Override
    public String remove() { ... }

    // ... 省略其他方法 ...
}

public class LinkedList implements Iterator {

    private LinkedListNode head;

    @Override
    public boolean hasNext() { ... }

    @Override
    public String next() { ... }

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

4.4、解释 2

Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器,Array 和 LinkedList 都实现了接口类 Iterator
我们通过传递不同类型的实现类(Array、LinkedList)到 print(Iterator iterator) 函数中,支持动态的调用不同的 next()、hasNext() 实现

  • 当我们往 print(Iterator iterator) 函数传递 Array 类型的对象的时候,print(Iterator iterator) 函数就会调用 Array 的 next()、hasNext() 的实现逻辑
  • 当我们往 print(Iterator iterator) 函数传递 LinkedList 类型的对象的时候,print(Iterator iterator) 函数就会调用 LinkedList 的 next()、hasNext() 的实现逻辑

4.5、意义

多态特性能提高代码的可扩展性和复用性

我们利用多态的特性,仅用一个 print() 函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据
当再增加一种要遍历打印的类型的时候,比如 HashMap
我们只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext()、next() 等方法就可以了,完全不需要改动 print() 函数的代码

如果我们不使用多态特性,我们就无法将不同的集合类型(Array、LinkedList)传递给相同的函数(print(Iterator iterator) 函数)
我们需要针对每种要遍历打印的集合,分别实现不同的 print() 函数
比如针对 Array,我们要实现 print(Array array) 函数,针对 LinkedList,我们要实现 print(LinkedList linkedList) 函数
而利用多态特性,我们只需要实现一个 print() 函数的打印逻辑,就能应对各种集合数据的打印操作,这显然提高了代码的复用性

多态也是很多设计模式、设计原则、编程技巧的代码实现基础
比如:策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等

posted @ 2023-06-24 15:21  lidongdongdong~  阅读(14)  评论(0编辑  收藏  举报