面向對象編程的英文縮寫是 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 項目的開發模式。