封裝、抽象、繼承、多態分別可以解決哪些編程問題?

 

面向對象編程的英文縮寫是 OOP,全稱是 Object Oriented Programming。對應地,面向對象編程語言的英文縮寫是 OOPL,全稱是 Object Oriented Programming Language。

 

1.封裝

封裝也叫作信息隱藏或者數據訪問保護。類通過暴露有限的訪問接口,授權外部僅能通過類提供的方式(或者叫函數)來訪問內部信息或者數據。我們通過一個簡單的例子來解釋一下。

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

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

從代碼中,我們可以發現,Wallet 類主要有四個屬性(也可以叫作成員變量),也就是我們前面定義中提到的信息或者數據。其中,id 表示錢包的唯一編號,createTime 表示錢包創建的時間,balance 表示錢包中的餘額,balanceLastModifiedTime 表示上次錢包餘額變更的時間。

 

之所以這樣設計,是因爲從業務的角度來說,id、createTime 在創建錢包的時候就確定好了,之後不應該再被改動,所以,我們並沒有在 Wallet 類中,暴露 id、createTime 這兩個屬性的任何修改方法,比如 set 方法。而且,這兩個屬性的初始化設置,對於 Wallet 類的調用者來說,也應該是透明的,所以,我們在 Wallet 類的構造函數內部將其初始化設置好,而不是通過構造函數的參數來外部賦值。

2.抽象

封裝主要講的是如何隱藏信息、保護數據,而抽象講的是如何隱藏方法的具體實現,讓調用者只需要關心方法提供了哪些功能,並不需要知道這些功能是如何實現的。

例如

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

在上面的這段代碼中,我們利用 Java 中的 interface 接口語法來實現抽象特性。調用者在使用圖片存儲功能的時候,只需要瞭解 IPictureStorage 這個接口類暴露了哪些方法就可以了,不需要去查看 PictureStorage 類裏的具體實現邏輯。

換一個角度來考慮,我們在定義(或者叫命名)類的方法的時候,也要有抽象思維,不要在方法定義中,暴露太多的實現細節,以保證在某個時間點需要改變方法的實現邏輯的時候,不用去修改其定義。舉個簡單例子,比如 getAliyunPictureUrl() 就不是一個具有抽象思維的命名,因爲某一天如果我們不再把圖片存儲在阿里雲上,而是存儲在私有云上,那這個命名也要隨之被修改。相反,如果我們定義一個比較抽象的函數,比如叫作 getPictureUrl(),那即便內部存儲方式修改了,我們也不需要修改命名。

 

3.繼承

繼承最大的一個好處就是代碼複用。假如兩個類有一些相同的屬性和方法,我們就可以將這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就可以重用父類中的代碼,避免代碼重複寫多遍。不過,這一點也並不是繼承所獨有的,我們也可以通過其他方式來解決這個代碼複用的問題,比如利用組合關係而不是繼承關係。

 

所以,繼承這個特性也是一個非常有爭議的特性。很多人覺得繼承是一種反模式。我們應該儘量少用,甚至不用。關於這個問題,在後面講到“多用組合少用繼承”這種設計思想的時候,我會非常詳細地再講解,這裏暫時就不展開講解了。

 

4.多態

多態是指,子類可以替換父類,在實際的代碼運行過程中,調用子類的方法實現。對於多態這種特性,純文字解釋不好理解,我們還是看一個具體的例子。

 

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() {
    //...如果數組滿了就擴容...代碼省略...
  }
}

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

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.get(i));
    }
  }
  
  public static void main(String args[]) {
    DynamicArray dynamicArray = new SortedDynamicArray();
    test(dynamicArray); // 打印結果:1、3、5
  }
}

接下來,我們先來看如何利用接口類來實現多態特性。我們還是先來看一段代碼。

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

 

傳統的 MVC 結構分爲 Model 層、Controller 層、View 層這三層。不過,在做前後端分離之後,三層結構在後端開發中,會稍微有些調整,被分爲 Controller 層、Service 層、Repository 層。Controller 層負責暴露接口給前端調用,Service 層負責核心業務邏輯,Repository 層負責數據讀寫。而在每一層中,我們又會定義相應的 VO(View Object)、BO(Business Object)、Entity。一般情況下,VO、BO、Entity 中只會定義數據,不會定義方法,所有操作這些數據的業務邏輯都定義在對應的 Controller 類、Service 類、Repository 類中。這就是典型的面向過程的編程風格。

實際上,這種開發模式叫作基於貧血模型的開發模式,也是我們現在非常常用的一種 Web 項目的開發模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章