设计模式之美学习(三):封装、抽象、继承、多态分别能够解决哪些编程问题?

理解面向对象编程及面向对象编程语言的关键就是理解其四大特性:封装、抽象、继承、多态。不过,对于这四大特性,光知道它们的定义是不够的,咱们还要知道每一个特性存在的意义和目的,以及它们能解决哪些编程问题。java

封装(Encapsulation)

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

对于封装这个特性,咱们须要编程语言自己提供必定的语法机制来支持。这个语法机制就是访问权限控制privatepublic 等关键字就是 Java 语言中的访问权限控制语法。private 关键字修饰的属性只能类自己访问,能够保护其不被类以外的代码直接访问。若是 Java 语言没有提供访问权限控制语法,全部的属性默认都是 public 的,那任意外部代码均可以经过相似 wallet.id=123; 这样的方式直接访问、修改属性,也就没办法达到隐藏信息和保护数据的目的了,也就没法支持封装特性了。编程

封装的意义是什么?它能解决什么编程问题?设计模式

若是咱们对类中属性的访问不作限制,那任何代码均可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另外一方面来讲,过分灵活也意味着不可控,属性能够随意被以各类奇葩的方式修改,并且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。好比某个同事在不了解业务逻辑的状况下,在某段代码中“偷偷地”重设了 wallet 中的 balanceLastModifiedTime 属性,这就会致使 balancebalanceLastModifiedTime 两个数据不一致。数组

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

抽象(Abstraction)

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

在面向对象编程中,咱们常借助编程语言提供的接口类(好比 Java 中的 interface 关键字语法)或者抽象类(好比 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。ide

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 {
  // ...省略其余属性...
  public void savePicture(Picture picture) { ... }
  public Image getPicture(String pictureId) { ... }
  public void deletePicture(String pictureId) { ... }
  public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
}

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

实际上,抽象这个特性是很是容易实现的,并不须要非得依靠接口类或者抽象类这些特殊语法机制来支持。换句话说,并非说必定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫做抽象。即使不编写 IPictureStorage 接口类,单纯的 PictureStorage 类自己就知足抽象特性。性能

之因此这么说,那是由于,类的方法是经过编程语言中的“函数”这一语法机制来实现的。经过函数包裹具体的实现逻辑,这自己就是一种抽象。调用者在使用函数的时候,并不须要去研究函数内部的实现逻辑,只须要经过函数的命名、注释或者文档,了解其提供了什么功能,就能够直接使用了。好比,咱们在使用 C 语言的 malloc() 函数的时候,并不须要了解它的底层代码是怎么实现的。

抽象有时候会被排除在面向对象的四大特性以外,为何呢?

抽象这个概念是一个很是通用的设计思想,并不仅仅用在面向对象编程中,也能够用来指导架构设计等。并且这个特性也并不须要编程语言提供特殊的语法机制来支持,只须要提供“函数”这一很是基础的语法机制,就能够实现抽象特性、因此,它没有很强的“特异性”,有时候并不被看做面向对象编程的特性之一。

抽象的意义是什么?它能解决什么编程问题

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

除此以外,抽象做为一个很是宽泛的设计思想,在代码设计中,起到很是重要的指导做用。不少设计原则都体现了抽象这种设计思想,好比基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(下降代码的耦合性)等。咱们在讲到后面的内容的时候,会具体来解释。

换一个角度来考虑,咱们在定义(或者叫命名)类的方法的时候,也要有抽象思惟,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点须要改变方法的实现逻辑的时候,不用去修改其定义。举个简单例子,好比 getAliyunPictureUrl() 就不是一个具备抽象思惟的命名,由于某一天若是咱们再也不把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,若是咱们定义一个比较抽象的函数,好比叫做 getPictureUrl(),那即使内部存储方式修改了,咱们也不须要修改命名。

继承(Inheritance)

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

为了实现继承这个特性,编程语言须要提供特殊的语法机制来支持,好比 Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 paraentheses()Ruby 使用 <。不过,有些编程语言只支持单继承,不支持多重继承,好比 JavaPHPC#Ruby 等,而有些编程语言既支持单重继承,也支持多重继承,好比 C++PythonPerl 等。

为何 Java 不支持多重继承呢?

Java 不支持多重继承的缘由:
多重继承有反作用:钻石问题(菱形继承)。
假设类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,那么此时类 D 会继承 BC 的方法,那对于 BC 重写的 A 中的方法,类 D 会继承哪个呢?这里就会产生歧义。
考虑到这种二义性问题,Java 不支持多重继承。

继承存在的意义是什么?它能解决什么编程问题?

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

若是咱们再上升一个思惟层面,去思考继承这一特性,能够这么理解:咱们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来讲,是一种 is-a 关系。咱们经过继承来关联两个类,反应真实世界中的这种关系,很是符合人类的认知,并且,从设计的角度来讲,也有一种结构美感。

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

因此,继承这个特性也是一个很是有争议的特性。不少人以为继承是一种反模式,咱们应该尽可能少用,甚至不用。

多态(Polymorphism)

多态是指,子类能够替换父类,在实际的代码运行过程当中,调用子类的方法实现。对于多态这种特性,纯文字解释很差理解,咱们仍是看一个具体的例子。

public class DynamicArray {
  private static final int DEFAULT_CAPACITY = 10;
  private int size = 0;
  private int capacity = DEFAULT_CAPACITY;
  private 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();
    for (int 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[i]);
    }
  }
  
  public static void main(String args[]) {
    DynamicArray dynamicArray = new SortedDynamicArray();
    test(dynamicArray); // 打印结果:一、三、5
  }
}

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

  • 第一个语法机制是编程语言要支持父类对象能够引用子类对象,也就是能够将 SortedDynamicArray 传递给 DynamicArray

  • 第二个语法机制是编程语言要支持继承,也就是 SortedDynamicArray 继承了 DynamicArray,才能将 SortedDyamicArray 传递给 DynamicArray

  • 第三个语法机制是编程语言要支持子类能够重写(override)父类中的方法,也就是 SortedDyamicArray 重写了 DynamicArray 中的 add() 方法。

经过这三种语法机制配合在一块儿,咱们就实现了在 test() 方法中,子类 SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArrayadd() 方法,也就是实现了多态特性。

对于多态特性的实现方式,除了利用”继承加方法重写”这种实现方式以外,咱们还有其余两种比较常见的的实现方式,一个是利用接口类语法,另外一个是利用 duck-typing 语法。不过,并非每种编程语言都支持接口类或者 duck-typing 这两种语法机制,好比 C++ 就不支持接口类语法,而 duck-typing 只有一些动态语言才支持,好比 PythonJavaScript 等。

接下来,先来看如何利用接口类来实现多态特性。

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 是一个接口类,定义了一个能够遍历集合数据的迭代器。ArrayLinkedList 都实现了接口类 Iterator。咱们经过传递不一样类型的实现类(ArrayLinkedList)到 print(Iterator iterator) 函数中,支持动态的调用不一样的 next()hasNext() 实现。

具体点讲就是,当咱们往 print(Iterator iterator) 函数传递 Array 类型的对象的时候,print(Iterator iterator) 函数就会调用 Arraynext()hasNext() 的实现逻辑;当咱们往 print(Iterator iterator) 函数传递 LinkedList 类型的对象的时候,print(Iterator iterator) 函数就会调用 LinkedListnext()hasNext() 的实现逻辑。

刚刚讲的是用接口类来实现多态特性。再来看下,如何用 duck-typing 来实现多态特性。咱们仍是先来看一段代码。这是一段 Python 代码。

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 实现多态的方式很是灵活。LoggerDB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,可是只要它们都有定义了 record() 方法,就能够被传递到 test() 方法中,在实际运行的时候,执行对应的 record() 方法。

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

多态特性存在的意义是什么?它能解决什么编程问题?

多态特性能提升代码的可扩展性和复用性。为何这么说呢?咱们回过头去看讲解多态特性的时候,举的第二个代码实例(Iterator 的例子)。

在那个例子中,咱们利用多态的特性,仅用一个 print() 函数就能够实现遍历打印不一样类型(ArrayLinkedList)集合的数据。当再增长一种要遍历打印的类型的时候,好比 HashMap,咱们只需让 HashMap 实现 Iterator 接口,从新实现本身的 hasNext()next() 等方法就能够了,彻底不须要改动 print() 函数的代码。因此说,多态提升了代码的可扩展性。

若是咱们不使用多态特性,咱们就没法将不一样的集合类型(ArrayLinkedList)传递给相同的函数(print(Iterator iterator) 函数)。咱们须要针对每种要遍历打印的集合,分别实现不一样的 print() 函数,好比针对 Array,咱们要实现 print(Array array) 函数,针对 LinkedList,咱们要实现 print(LinkedList linkedList) 函数。而利用多态特性,咱们只须要实现一个 print() 函数的打印逻辑,就能应对各类集合数据的打印操做,这显然提升了代码的复用性。

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

重点回顾

1. 关于封装特性

封装也叫做信息隐藏或者数据访问保护。类经过暴露有限的访问接口,受权外部仅能经过类提供的方式来访问内部信息或者数据。它须要编程语言提供权限访问控制语法来支持,例如 Java 中的 privateprotectedpublic 关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提升代码的可维护性;另外一方面是仅暴露有限的必要接口,提升类的易用性。

2. 关于抽象特性

封装主要讲如何隐藏信息、保护数据,那抽象就是讲如何隐藏方法的具体实现,让使用者只须要关心方法提供了哪些功能,不须要知道这些功能是如何实现的。抽象能够经过接口类或者抽象类来实现,但也并不须要特殊的语法机制来支持。抽象存在的意义,一方面是提升代码的可扩展性、维护性,修改实现不须要改变定义,减小代码的改动范围;另外一方面,它也是处理复杂系统的有效手段,能有效地过滤掉没必要要关注的信息。

3. 关于继承特性

继承是用来表示类之间的 is-a 关系,分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类能够继承多个父类。为了实现继承这个特性,编程语言须要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。

4. 关于多态特性

多态是指子类能够替换父类,在实际的代码运行过程当中,调用子类的方法实现。多态这种特性也须要编程语言提供特殊的语法机制来实现,好比继承、接口类、duck-typing。多态能够提升代码的扩展性和复用性,是不少设计模式、设计原则、编程技巧的代码实现基础。

参考:理论二:封装、抽象、继承、多态分别能够解决哪些编程问题?

本文由博客一文多发平台 OpenWrite 发布!
更多内容请点击个人博客 沐晨

相关文章
相关标签/搜索