在最初學習Java的時候,我們都聽到過一句話,Java是物件導向語言。每當提到物件導向的時候,許多開發者也嗤之以鼻:都什麼年代了,誰還不知道物件導向。
重學設計模式後,請回答,你真的物件導向了嗎?
一般情況下,我們會將物件導向的特性分為四大特性,分別是:封裝、抽象、繼承、多型。以這四大特性作為程式碼設計規範的程式設計風格我們一般稱之為物件導向程式設計。
我們都知道Java語言是物件導向語言,那麼用Java語言實現的程式碼就是物件導向程式設計嗎?答案是否定的。在瞭解這個原因之前,首先我們需要需要知道物件導向四大特性分別可以解決什麼問題。
封裝特性說白了就是資料存取限制或者叫資料存取保護,這一特性需要依賴語言本身具有存取許可權機制。比如在Java中 使用private、public、protect等修飾符修復變數來控制變數讀、寫的許可權控制,這一點是最容易被開發者忽略也是開發者最不在意或者容易使用錯誤的一點。這一點我們後續會詳細講解。
抽象特性主要用來隱藏方法的具體實現。也有一種說法將上面提到的四大特性中的抽象這一特性排除在外,這是因為函數本身就是一種抽象,函數內部包含具體的實現邏輯對呼叫者來說是不需要關注具體實現方式的。在Java語言中除了函數本身,通常使用interface介面和abstract抽象關鍵字來實現,抽象更像是一種理論指導,許多程式碼設計原則都是基於抽象理論來實現的。
舉個具體的例子🌰,在Android開發中我們經常會使用到地圖業務,以使用百度地圖為例,開發者可能為了模組的通用性,會定義一系列的介面,程式碼如下所示:
public interface BaiduMapApi {
/**
* 載入地圖
*/
void loadBaiduMap();
/**
* 銷燬地圖
*/
void destoryBaiduMap();
}
按照抽象特性和程式碼設計原則來說,其實這套設計是有些瑕疵的。抽象要將具體實現隱藏起來,如果以後業務中的百度地圖更改成了高德地圖,那麼這一套介面命名設計就會產生歧義。並且可能會為後人埋坑。
較為合理的設計程式碼如下所示:
public interface MapApi {
/**
* 載入地圖
*/
void loadMap();
/**
* 銷燬地圖
*/
void destoryMap();
}
這樣一來,介面的設計遵循了抽象原則,更便於開發者後續的擴充套件和維護。
繼承用來表示類之間is-a的關係,比如:貓是動物、狗是動物,動物都會吃飯、睡覺,我們則會建立一個動物類,程式碼如下所示:
public class Animal {
private void eat(){
System.out.println("--eat--");
}
private void sleep(){
System.out.println("--eat---");
}
}
然後再建立兩個子類繼承自Animal類,程式碼如下所示:
public class Brid extends Animal{
@Override
public void eat() {
super.eat();
}
@Override
public void sleep() {
super.sleep();
}
}
public class Dog extends Animal{
@Override
public void eat() {
super.eat();
}
@Override
public void sleep() {
super.sleep();
}
}
繼承的最大好處就是實現程式碼複用,Java語言中一個類是無法繼承多個父類別的,那麼原因是什麼呢?這是因為繼承多個問題會出現」鑽石問題「,感興趣的可自行了解,這裡不做過多解釋了。
繼承雖然可以實現程式碼複用,但是過度使用繼承會導致巢狀過深,程式碼難以閱讀和維護,所以在設計原則中也會說組合方式優於繼承。
接著來看最後一個特性:多型。多型是許多設計模式和設計原則實現的基礎,比如常用的策略模式和裡式替換原則等。簡單的說,多型就是子類可以替換父類別,舉個例子:
比如在業務中,需要提供一個方法實現裝置資訊列印功能,裝置中類有A、B等多種,程式碼如下所以:
public class PrintUtil {
private void print(A a){
}
private void print(B b){
}
}
按照一般實現方式,每增加一種裝置型別,都需要在PrintUtil新增一個列印方法,且邏輯都在PrintUtil類中使得難以擴充套件和維護。依賴多型的特性,我們可以這樣來實現,首先定義一個介面,程式碼如下所示:
public interface PrintInterface {
void print();
}
使A、B類都繼承PrintInterface介面,程式碼如下所示:
public class A implements PrintInterface{
@Override
public void print() {
System.out.println("-A裝置的列印-");
}
}
public class B implements PrintInterface {
@Override
public void print() {
System.out.println("-B裝置的列印-");
}
}
修改PrintUtil類中的方法如下所示:
public class PrintUtil {
public void print(PrintInterface printInterface) {
printInterface.print();
}
}
需要列印裝置資訊時,可直接採用如下方式:
public static void main(String[] args) {
PrintUtil printUtil = new PrintUtil();
A a = new A();
printUtil.print(a);
B b = new B();
printUtil.print(b);
}
這樣,當增加一種裝置時,我們只需要將裝置類繼承自PrintInterface介面,並在類內部實現自己的列印規則即可,不需要改動PrintUtil中的程式碼,提高了程式碼的可延伸性。
瞭解了物件導向的四大特性後,接著來看你真的物件導向了嗎?
與物件導向並列的是程式導向,很多時候,我們使用物件導向語言寫出來的程式碼可能都是程式導向的,但如果想讓專案中完全沒有程式導向風格的程式碼,這一點是非常不切實際的。但瞭解錯誤的使用方式可以指導我們在以後的編碼過程中寫出更易理解、更易擴充套件的程式碼。
在Android開發中,相信每個每個專案中都有一推Util工具類,這一些工具類也常被我們認為是好用的輪子,比如經常設計的UserUtil、FileUtil、DeviceUtil,用來在不同類之間呼叫相同的方法。如果一個Util工具類中僅有若干靜態方法沒有任何屬性,那麼這個工具類我們完全可以稱之為是程式導向的。
在設計工具類的時候,我們要儘量保持」單一職責「原則,比如一個DeviceUtil中定義了各種獲取裝置引數的方法也定義了和檔案有關的方法,那麼這個類就沒有遵循單一職責原則,所以我們要儘量避免設計大而全的工具類,要按照實際功能,讓類的職責儘可能的保持單一。
除了Util工具類之外,Config檔案也是Android開發者經常會使用到的,在元件化的開發中,我們會為每個模組設定路由檔案,寫出的程式碼可能如下所示:
public class ArouteConfig {
public String AModuleMainActivity = "A/MainActivity";
public String AModuleSetActivity = "A/SetActivity";
public String BModuleMainActivity = "B/MainActivity";
public String BModuleSetActivity = "B/SetActivity";
}
ArouteConfig類中定義了A、B等module的路由設定變數,這樣設計在功能實現中是完全沒問題的,但是設想一下,一來 元件化的目的就是為了模組解耦開發,不同模組的負責人都會修改這個組態檔,很有可能導致衝突和難以維護,二來 如果另一個專案中同樣用到了B module,這個時候我們會把B moudle和ArouteConfig類遷移到另一個專案中,如此一來,ArouteConfig中便定義了許多冗餘的變數且不符合單一職責原則。
所以在設計中,我們可以考慮將組態檔拆分更細粒,分別新建AMoudleArouteConfig與BModuleArouteConfig,這樣對應模組的負責人只需維護對應模組的路由設定不會導致衝突,也提高了類設計的內聚性和程式碼的複用性。
對Android開發工程師而言,我們可能會比較排斥 將一些靜態變數定義在Activity中,都會直接抽取一個組態檔,寫在組態檔中,如果這些靜態變數僅在某一個Activity中使用到了,那完全沒有必要單獨定義一個組態檔的,如果你確定需要,那就儘快去定義吧!只要適合專案需要即可。
Android開發工程師或Java開發工程師經常會使用編輯器中複寫方法,給所有的變數生成get、set方法,尤其是Android開發工程師,拿到後臺返回的json資料後,直接使用GsonFormat生成對應的實體類,簡直不要太爽~
比如,伺服器返回使用者資料結構如下所示:
{
"userName":"HuangLinqing",
"age":27,
"birthday":"819561600"
}
使用GsonFormat或編輯器快捷鍵自動生成的實體類如下所示:
public class User {
private String userName;
private int age;
private String birthday;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
}
一般情況下,這樣編寫也不會有什麼問題。但仔細來看,這段程式碼顯然違反了物件導向中的封裝特性,這是因為出生日期、和年齡是相關聯的,而出生日期和年齡都暴露了set方法,如果某個開發的同事在使用錯誤的情況呼叫了 setBirthday方法,會導致通過出生日期計算的年齡和返回年齡不符的情況。所以正確的做法是,如果給出生日期提供了對外設定的方法,那麼年齡就不應該對外暴露設定的方法,且要自動計算,修改後的程式碼如下所示:
public class User {
private String userName;
private int age;
private String birthday;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
age = (當前時間 - birthday);
}
}
我猜你肯定會說,誰閒著沒事會設定那個方法,我們確保都不用不就行了嗎?是的,沒錯,但團隊間的共同作業標準需要用規範去衡量而不能以口頭的保證作為依據,萬一那個大廢就是你自己呢?
除了本文中所提到的,其實還有好多經常遇到卻不以為意的坑。好的程式碼需要使用規範標準去說話,當然這裡的規範只要適合你們的專案就是最好的。重學設計模式之後,請回答,你真的物件導向了嗎?
就像近期在Android圈經常討論到的,Google官方推薦的架構由MVVM變成MVI,大家就都去說MVI怎麼怎麼好,MVVM的缺陷是怎樣的。就像MVVM剛出來時,大家對MVP的評判是一樣的。在業務開發中可以這麼說:只要適合專案本身,所有的架構都是值得學習和使用的!