设计模式05-面向对象四大特性能解决什么问题

Posted murongmochen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式05-面向对象四大特性能解决什么问题相关的知识,希望对你有一定的参考价值。

  前文提到,理解面向对象编程和面向对象编程语言,关键是要理解四大特性(封装、抽象、继承、多态)。仅仅知道定义是不够的,我们要深刻理解它们的意义和目的,以及能解决什么问题。

  本文结合代码来解析四大特性。有一点要注意,不同编程语言对于四大特性的语法机制不尽相同,但我们的分析不与特定编程语言挂钩,不要局限在语法的细节中。

 

一、封装(Encapsulation)

1. 什么是封装

  封装也叫信息隐藏数据访问保护。类隐藏具体的数据和实现细节,只暴露有限的访问接口。由此,可以限制外部,只能通过类提供的这些访问接口来访问类内部的数据和信息。

  接下来,我们用一段 demo 为例来分析。在金融系统中,会给每个用户创建一个虚拟钱包,记录用户在系统中的虚拟货币量。对于虚拟钱包的业务背景,只需要简单了解即可。后续我们会利用 OOP 的设计思想来详细介绍虚拟钱包的设计实现。

 1 public class Wallet {
 2 
 3     private String id;                      // 钱包编号
 4     private long createTime;                // 钱包创建时间
 5     private BigDecimal balance;             // 钱包余额
 6     private long balanceLastModifiedTime;   // 上次余额变更时间
 7 
 8     public Wallet() {
 9         this.id = IdGenerator.getInstance().generate();
10         this.createTime = System.currentTimeMillis();
11         this.balance = BigDecimal.ZERO;
12         this.balanceLastModifiedTime = System.currentTimeMillis();
13     }
14 
15     public String getId() {
16         return this.id
17     }
18 
19     public long getCreateTime() {
20         return this.createTime;
21     }
22 
23     public BigDecimal getBalance() {
24         return this.balance;
25     }
26 
27     public long getBalanceLastModifiedTime()
28     {
29         return this.balanceLastModifiedTime;
30     }
31 
32     public void increaseBalance(BigDecimal increasedAmount) {
33         if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
34             throw new InvalidAmountException("...");
35         }
36         
37         this.balance.add(increaseBalance);
38         this.balanceLastModifiedTime = System.currentTimeMillis();
39     }
40     
41     public void decreaseBalance(BigDecimal decreasedAmount) {
42         if (decreasedAmount.compareTo(ZERO) < 0) {
43             throw new InvalidAmountException("...");
44         }
45         
46         if (decreasedAmount.compareTo(this.balance) > 0) {
47             throw new InsufficientAmountException("...");
48         }
49         
50         this.balance.subtract(decreasedAmount);
51         this.balanceLastModifiedTime = System.currentTimeMillis();
52     }
53 }

  从以上代码可以看到,Wallet 类有四个成员变量,我们使用封装特性,对这四个成员变量的访问进行限制。调用者只能通过以下六个方法来访问或修改钱包中的数据:

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

  之所以这样设计,是因为 id、createTime 在对象创建时确定,不应再被改动,所以 Wallet 类不对外暴露它们的修改方法。此外,这两个属性的初始化设置,对于 Wallet 类的调用者来说应该是透明的。因此,在 Wallet 类的构造函数中将其初始化,而非通过构造函数的参数来外部赋值。

  对于钱包余额 balance 属性,只能增减,不能重新设置。所以在 Wallet 类中只暴露 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。

  对于 balanceLastModifiedTime 这个属性,应该和 balance 的修改操作绑定在一起。所以将这个属性的修改操作完全封装在 increaseBalance() 和 decreaseBalance() 方法中,不对外暴露。这样也可以保证 balance 和 balanceLastModifiedTime 两个数据的一致性。

  对于封装特性,需要编程语言本身提供访问权限控制来支持。如果没有访问权限控制的功能,那么外部代码可以随意读写属性,无法达到隐藏信息、保护数据的目的,封装自然也就无从谈起了。

2. 封装的意义是什么?能解决什么问题?

  如果对类中属性的访问不做限制,那么任意代码都可以访问、修改类中的属性。这样看起来虽然比较灵活,但过度灵活意味着不可控——属性可能被随意修改,而且修改逻辑散落在各处,势必影响代码可读性和可维护性。

  此外,如果类仅仅通过有限的方法暴露必要操作,可以提高易用性。如果把所有细节暴露出来,调用者需要对类有详尽了解才能确保正确调用。这实际是一种负担。如果我们把细节封装起来,暴露几个方法给调用者使用,那就会比较容易使用,而且大大降低了出错的概率。

 

二、抽象(Abstraction)

1. 什么是抽象

  抽象讲的是如何隐藏方法的具体实现,让调用者只需关注方法提供了哪些功能即可,不用关注实现细节。

  在面向对象编程中,通常借助编程语言提供的接口类抽象类这两种语法机制来实现抽象特性。PS:接口的概念比较宽泛,譬如还有 API 的含义。我们后续都用“接口类”来特指编程语言提供的接口语法

  我们还是用一个 demo 来理解抽象特性:

 1 public interface IPictureStorage {
 2     void savePicture(Picture picture);
 3     Image getPicture(String pictureId);
 4     void deletePicture(String pictureId);
 5     void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
 6 }
 7 
 8 public class PictureStorage implements IPictureStorage {
 9 
10     // ...省略其他属性...
11 
12     @Override
13     public void savePicture(Picture picture) { ... }
14 
15     @Override
16     public Image getPicture(String pictureId) { ... }
17 
18     @Override
19     public void deletePicture(String pictureId) { ... }
20 
21     @Override
22     public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
23 }

  以上代码利用 Java 的 interface 接口语法来实现抽象特性。调用者在使用图片存储功能时,只要知道 IPictureStorage 接口类暴露了哪些方法即可,不用去 PictureStorage 类里查看具体实现逻辑。

  抽象特性其实是很容易实现的。即使不依赖接口类和抽象类这种特殊语法机制,单纯的 PictureStorage 类本身就满足抽象特性。因为类的方法包裹了具体的实现逻辑,这本身就是一种抽象。调用者在使用函数时,并不需要去研究函数内部的实现逻辑,只要通过函数的命名、注释和文档,了解其功能即可使用。

  前文提到面向对象三大特性和四大特性的问题,为什么有时会把抽象排除在外?这是因为抽象是一个通用的设计思想,并非仅用在面向对象编程中,架构设计等方面也经常使用抽象。此外,抽象特性本身不需要编程语言提供特殊语法机制,支持函数即可实现。综上所述,抽象没有很强的特异性,所以有时不被看作面向对象编程的特性之一。

2. 抽象的意义是什么?它能解决什么问题?

  抽象作为一种只关注功能,不关注实现的设计思路,帮我们过滤掉了很多不必要信息。

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

  我们在给类的方法命名时,也要有抽象思维,不要在方法定义中暴露太多细节,从而保证后续需要修改方法实现逻辑时,不用修改其定义。譬如将方法命名为 getAliyunPictureUrl(),那么后续不再使用阿里云时,这个命名也要随之修改。如果定义成 getPictureUrl() 这种比较抽象的函数,就避免了这个问题。

 

三、继承(Inheritance)

1. 什么是继承

  继承用于表示类之间的 is-a 关系。譬如我们说人是一种哺乳动物。

  从继承关系来区分,继承可以分为两种模式——单继承多继承。单继承就是说,一个子类只继承一个父类。多继承则是说,一个子类可以继承多个父类。

  为了实现继承特性,编程语言需要提供特殊的语法机制来支持。譬如,Java 使用 extends,C++ 使用 : 。区别在于,有些编程语言只支持单继承,譬如 Java、php。有些语言支持多继承,譬如 C++、Python。

2. 继承的意义是什么?它能解决哪些问题?

  继承最大的好处是代码复用。如果两个类有相同的属性和方法,我们就可以将这些相同部分抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。但这并非继承独有的,我们也可以用其他方式来解决代码复用问题,譬如组合。

  过度使用继承,继承的层次过深过复杂,就会使得代码可读性、可维护性变差。为了了解一个类的功能,可能需要按照继承关系向上逐层查看直接父类和间接父类。此外,子类和父类高度耦合,修改父类的代码,也会影响到子类。

  综上,继承特性比较有争议。有些观点认为,继承是一种反模式,我们应该少用甚至不用。后续会写篇关于“多用组合少用继承”设计思想的博客,再详细分析。

 

四、多态(Polymorphism)

1. 什么是多态

  多态是指,定义时使用父类,实际运行时可以传入子类,从而可以调用子类的方法实现。譬如,定义时写的参数是汽车(父类),实际调用的时候传入的是 F1 赛车(子类),那就可以使用 F1 赛车的特性,譬如速度超快。

  多态的特性用文字描述不易理解,还是老规矩,写个 demo:

 1 public class DynamicArray {
 2     private static final int DEFAULT_CAPACITY = 10;
 3     protected int size = 0;
 4     protected int capacity = DEFAULT_CAPACITY;
 5     protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
 6 
 7     public int size() {
 8         return this.size;
 9     }
10 
11     public Integer get(int index) {
12         return elements[index];
13     }
14 
15     //...省略n多方法...
16   
17     public void add(Integer e) {
18         ensureCapacity();
19         elements[size++] = e;
20     }
21 
22     protected void ensureCapacity() {
23         //...如果数组满了就扩容...代码省略...
24     }
25 }
26 
27 public class SortedDynamicArray extends DynamicArray {
28     @Override
29     public void add(Integer e) {
30         ensureCapacity();
31         
32         int i;
33         
34         for (i = size-1; i>=0; --i) { //保证数组中的数据有序
35             if (elements[i] > e) {
36                 elements[i+1] = elements[i];
37             } else {
38                 break;
39             }
40         }
41         
42         elements[i+1] = e;
43         ++size;
44     }
45 }
46 
47 public class Example {
48     public static void test(DynamicArray dynamicArray) {
49         dynamicArray.add(5);
50         dynamicArray.add(1);
51         dynamicArray.add(3);
52         
53         for (int i = 0; i < dynamicArray.size(); ++i) {
54             System.out.println(dynamicArray.get(i));
55         }
56     }
57 
58     public static void main(String args[]) {
59         DynamicArray dynamicArray = new SortedDynamicArray();
60         test(dynamicArray); // 打印结果:1、3、5
61     }
62 }

  多态这种特性也需要编程语言提供一些语法支持。在上面的 demo 中,用到了三个语法机制来实现多态:

    • 编程语言要支持引用父类对象的变量可以引用子类对象。
    • 编程语言要支持继承。
    • 编程语言要支持子类可以重写父类方法。

  对于多态特性的实现方式,除了“继承+重写”之外,还有两种实现方式比较常见。一是利用接口类语法,另一个是利用 duck-typing 语法。但是要注意,不是所有编程语言都支持接口类或 duck-typing。譬如 C++ 不支持接口类,而 duck-typing 只有一些动态语言才支持,譬如 Python。

  接下来,我们再通过一个 demo 看看,如何利用接口类来实现多态特性。

 1 public interface Iterator {
 2     boolean hasNext();
 3     String next();
 4     String remove();
 5 }
 6 
 7 public class Array implements Iterator {
 8     private String[] data;
 9 
10     public boolean hasNext() {
11         ...
12     }
13 
14     public String next() {
15         ...
16     }
17 
18     public String remove() {
19         ...
20     }
21 
22     //...省略其他方法...
23 }
24 
25 public class LinkedList implements Iterator {
26     private LinkedListNode head;
27 
28     public boolean hasNext() {
29         ...
30     }
31 
32     public String next() {
33         ...
34     }
35 
36     public String remove() {
37         ...
38     }
39 
40     //...省略其他方法... 
41 }
42 
43 public class Demo {
44     private static void print(Iterator iterator) {
45         while (iterator.hasNext()) {
46             System.out.println(iterator.next());
47         }
48     }
49   
50     public static void main(String[] args) {
51         Iterator arrayIterator = new Array();
52         print(arrayIterator);
53 
54         Iterator linkedListIterator = new LinkedList();
55         print(linkedListIterator);
56     }
57 }

  在以上 demo 中,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() 的实现逻辑。

  接下来用 Python 写了一个 demo,用于说明如何用 duck-typing 来实现多态特性。

 1 class Logger:
 2     def record(self):
 3         print(“I write a log into file.”)
 4         
 5 class DB:
 6     def record(self):
 7         print(“I insert data into db. ”)
 8         
 9 def test(recorder):
10     recorder.record()
11 
12 def demo():
13     logger = Logger()
14     db = DB()
15     test(logger)
16     test(db)

  duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系。但只要它们都定义了 record() 方法,就可以被传递到 test() 方法中,在实际运行的时候,执行对应的 record() 方法。

  所谓 duck-typing,即只要两个类具有相同方法,就可以实现多态,并不要求两个类之间有任何关系。这是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口。

2. 多态的意义是什么?它能解决什么问题?

  多态特性能提高代码的可扩展性和复用性。咱们以上文的第二个 demo(Iterator)为例来分析。

  在此例中,利用多态特性,仅用一个 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 语句等。

以上是关于设计模式05-面向对象四大特性能解决什么问题的主要内容,如果未能解决你的问题,请参考以下文章

设计模式学习笔记封装继承多态抽象能解决什么问题?

当讨论面向对象的时候,我们到底谈论什么?

面向对象编程的四大基本特征

面向对象

面向对象的四大特点

设计模式之美——封装,继承,多态的意义