設計模式之美學習(五):哪些代碼設計看似是面向對象,實際是面向過程的?

常見的編程範式或者說編程風格有三種,面向過程編程、面向對象編程、函數式編程,而面向對象編程又是這其中最主流的編程範式。現現在,大部分編程語言都是面向對象編程語言,大部分軟件都是基於面向對象編程這種編程範式來開發的。前端

不過,在實際的開發工做中,總覺得把全部代碼都塞到類裏,天然就是在進行面向對象編程了。實際上,這樣的認識是不正確的。有時候,從表面上看似是面向對象編程風格的代碼,從本質上看倒是面向過程編程風格的。java

哪些代碼設計看似是面向對象,實際是面向過程的?

在用面向對象編程語言進行軟件開發的時候,咱們有時候會寫出面向過程風格的代碼。有些是有意爲之,並沒有不妥;而有些是無心爲之,會影響到代碼的質量。mysql

1. 濫用 gettersetter 方法算法

它違反了面向對象編程的封裝特性,至關於將面向對象編程風格退化成了面向過程編程風格。經過下面這個例子來給你解釋一下這句話。sql

public class ShoppingCart {
  private int itemsCount;
  private double totalPrice;
  private List<ShoppingCartItem> items = new ArrayList<>();
  
  public int getItemsCount() {
    return this.itemsCount;
  }
  
  public void setItemsCount(int itemsCount) {
    this.itemsCount = itemsCount;
  }
  
  public double getTotalPrice() {
    return this.totalPrice;
  }
  
  public void setTotalPrice(double totalPrice) {
    this.totalPrice = totalPrice;
  }

  public List<ShoppingCartItem> getItems() {
    return this.items;
  }
  
  public void addItem(ShoppingCartItem item) {
    items.add(item);
    itemsCount++;
    totalPrice += item.getPrice();
  }
  // ...省略其餘方法...
}

在這段代碼中,ShoppingCart 是一個簡化後的購物車類,有三個私有(private)屬性:itemsCounttotalPriceitems。對於 itemsCounttotalPrice 兩個屬性,咱們定義了它們的 gettersetter 方法。對於 items 屬性,咱們定義了它的 getter 方法和 addItem() 方法。代碼很簡單,理解起來不難。那你有沒有發現,這段代碼有什麼問題呢?編程

咱們先來看前兩個屬性,itemsCounttotalPrice。雖然咱們將它們定義成 private 私有屬性,可是提供了 publicgettersetter 方法,這就跟將這兩個屬性定義爲 public 公有屬性,沒有什麼兩樣了。外部能夠經過 setter 方法隨意地修改這兩個屬性的值。除此以外,任何代碼均可以隨意調用 setter 方法,來從新設置 itemsCounttotalPrice 屬性的值,這也會致使其跟 items 屬性的值不一致。小程序

而面向對象封裝的定義是:經過訪問權限控制,隱藏內部數據,外部僅能經過類提供的有限的接口訪問、修改內部數據。因此,暴露不該該暴露的 setter 方法,明顯違反了面向對象的封裝特性。數據沒有訪問權限控制,任何代碼均可以隨意修改它,代碼就退化成了面向過程編程風格的了。後端

看完了前兩個屬性,咱們再來看 items 這個屬性。對於 items 這個屬性,咱們定義了它的 getter 方法和 addItem() 方法,並無定義它的 setter 方法。這樣的設計貌似看起來沒有什麼問題,但實際上並非。安全

對於 itemsCounttotalPrice 這兩個屬性來講,定義一個 publicgetter 方法,確實無傷大雅,畢竟 getter 方法不會修改數據。可是,對於 items 屬性就不同了,這是由於 items 屬性的 getter 方法,返回的是一個 List集合容器。外部調用者在拿到這個容器以後,是能夠操做容器內部數據的,也就是說,外部代碼仍是能修改 items 中的數據。好比像下面這樣:前後端分離

ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空購物車

你可能會說,清空購物車這樣的功能需求看起來合情合理啊,上面的代碼沒有什麼不妥啊。你說得沒錯,需求是合理的,可是這樣的代碼寫法,會致使 itemsCounttotalPriceitems 三者數據不一致。咱們不該該將清空購物車的業務邏輯暴露給上層代碼。正確的作法應該是,在 ShoppingCart 類中定義一個 clear() 方法,將清空購物車的業務邏輯封裝在裏面,透明地給調用者使用。ShoppingCart 類的 clear() 方法的具體代碼實現以下:

public class ShoppingCart {
  // ...省略其餘代碼...
  public void clear() {
    items.clear();
    itemsCount = 0;
    totalPrice = 0.0;
  }
}

你可能還會說,我有一個需求,須要查看購物車中都買了啥,那這個時候,ShoppingCart 類不得不提供 items 屬性的 getter 方法了,那又該怎麼辦纔好呢?

若是你熟悉 Java 語言,那解決這個問題的方法仍是挺簡單的。咱們能夠經過 Java 提供的 Collections.unmodifiableList() 方法,讓 getter 方法返回一個不可被修改的 UnmodifiableList 集合容器,而這個容器類重寫了 List 容器中跟修改數據相關的方法,好比 add()clear() 等方法。一旦咱們調用這些修改數據的方法,代碼就會拋出 UnsupportedOperationException 異常,這樣就避免了容器中的數據被修改。具體的代碼實現以下所示。

public class ShoppingCart {
  // ...省略其餘代碼...
  public List<ShoppingCartItem> getItems() {
    return Collections.unmodifiableList(this.items);
  }
}

public class UnmodifiableList<E> extends UnmodifiableCollection<E>
                          implements List<E> {
  public boolean add(E e) {
    throw new UnsupportedOperationException();
  }
  public void clear() {
    throw new UnsupportedOperationException();
  }
  // ...省略其餘代碼...
}

ShoppingCart cart = new ShoppingCart();
List<ShoppingCartItem> items = cart.getItems();
items.clear();//拋出UnsupportedOperationException異常

不過,這樣的實現思路仍是有點問題。由於當調用者經過 ShoppingCartgetItems() 獲取到 items 以後,雖然咱們無法修改容器中的數據,但咱們仍然能夠修改容器中每一個對象(ShoppingCartItem)的數據。聽起來有點繞,看看下面這幾行代碼你就明白了。

ShoppingCart cart = new ShoppingCart();
cart.add(new ShoppingCartItem(...));
List<ShoppingCartItem> items = cart.getItems();
ShoppingCartItem item = items.get(0);
item.setPrice(19.0); // 這裏修改了item的價格屬性

總結一下,在設計實現類的時候,除非真的須要,不然,儘可能不要給屬性定義 setter 方法。除此以外,儘管 getter 方法相對 setter 方法要安全些,可是若是返回的是集合容器(好比例子中的 List 容器),也要防範集合內部數據被修改的危險。

2. 濫用全局變量和全局方法

在面向對象編程中,常見的全局變量有單例類對象、靜態成員變量、常量等,常見的全局方法有靜態方法。單例類對象在全局代碼中只有一份,因此,它至關於一個全局變量。靜態成員變量歸屬於類上的數據,被全部的實例化對象所共享,也至關於必定程度上的全局變量。而常量是一種很是常見的全局變量,好比一些代碼中的配置參數,通常都設置爲常量,放到一個 Constants 類中。靜態方法通常用來操做靜態變量或者外部數據。你能夠聯想一下咱們經常使用的各類 Utils 類,裏面的方法通常都會定義成靜態方法,能夠在不用建立對象的狀況下,直接拿來使用。靜態方法將方法與數據分離,破壞了封裝特性,是典型的面向過程風格。

在剛剛介紹的這些全局變量和全局方法中,Constants 類和 Utils 類最經常使用到。如今,咱們就結合這兩個幾乎在每一個軟件開發中都會用到的類,來深刻探討一下全局變量和全局方法的利與弊。

咱們先來看一下,在我過去參與的項目中,一種常見的 Constants 類的定義方法。

public class Constants {
  public static final String MYSQL_ADDR_KEY = "mysql_addr";
  public static final String MYSQL_DB_NAME_KEY = "db_name";
  public static final String MYSQL_USERNAME_KEY = "mysql_username";
  public static final String MYSQL_PASSWORD_KEY = "mysql_password";
  
  public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
  public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
  public static final int REDIS_DEFAULT_MAX_IDLE = 50;
  public static final int REDIS_DEFAULT_MIN_IDLE = 20;
  public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
  
  // ...省略更多的常量定義...
}

在這段代碼中,咱們把程序中全部用到的常量,都集中地放到這個 Constants 類中。不過,定義一個如此大而全的 Constants 類,並非一種很好的設計思路。爲何這麼說呢?緣由主要有如下幾點。

首先,這樣的設計會影響代碼的可維護性。

若是參與開發同一個項目的工程師有不少,在開發過程當中,可能都要涉及修改這個類,好比往這個類裏添加常量,那這個類就會變得愈來愈大,成百上千行都有可能,查找修改某個常量也會變得比較費時,並且還會增長提交代碼衝突的機率。

其次,這樣的設計還會增長代碼的編譯時間。

Constants 類中包含不少常量定義的時候,依賴這個類的代碼就會不少。那每次修改 Constants 類,都會致使依賴它的類文件從新編譯,所以會浪費不少沒必要要的編譯時間。不要小看編譯花費的時間,對於一個很是大的工程項目來講,編譯一次項目花費的時間多是幾分鐘,甚至幾十分鐘。而咱們在開發過程當中,每次運行單元測試,都會觸發一次編譯的過程,這個編譯時間就有可能會影響到咱們的開發效率。

最後,這樣的設計還會影響代碼的複用性。

若是咱們要在另外一個項目中,複用本項目開發的某個類,而這個類又依賴 Constants 類。即使這個類只依賴 Constants 類中的一小部分常量,咱們仍然須要把整個 Constants 類也一併引入,也就引入了不少無關的常量到新的項目中。

那如何改進 Constants 類的設計呢?這裏有兩種思路能夠借鑑。

第一種是將 Constants 類拆解爲功能更加單一的多個類,好比跟 MySQL 配置相關的常量,咱們放到 MysqlConstants 類中;跟 Redis 配置相關的常量,咱們放到 RedisConstants 類中。固然,還有一種以爲更好的設計思路,那就是並不單獨地設計 Constants 常量類,而是哪一個類用到了某個常量,咱們就把這個常量定義到這個類中。好比,RedisConfig 類用到了 Redis 配置相關的常量,那咱們就直接將這些常量定義在 RedisConfig 中,這樣也提升了類設計的內聚性和代碼的複用性。

講完了 Constants 類,咱們再來討論一下 Utils 類。首先,想問你這樣一個問題,咱們爲何須要 Utils 類?Utils 類存在的意義是什麼?

在講面向對象特性的時候,講過繼承能夠實現代碼複用。利用繼承特性,咱們把相同的屬性和方法,抽取出來,定義到父類中。子類複用父類中的屬性和方法,達到代碼複用的目的。可是,有的時候,從業務含義上,A 類和 B 類並不必定具備繼承關係,好比 Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但並不具備繼承關係(既不是父子關係,也不是兄弟關係)。僅僅爲了代碼複用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。若是不熟悉背後設計思路的同事,發現 Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的倒是 URL 相關的操做,會以爲這個代碼寫得莫名其妙,理解不了。

既然繼承不能解決這個問題,咱們能夠定義一個新的類,實現 URL 拼接和分割的方法。而拼接和分割兩個方法,不須要共享任何數據,因此新的類不須要定義任何屬性,這個時候,咱們就能夠把它定義爲只包含靜態方法的 Utils 類了。

實際上,只包含靜態方法不包含任何屬性的 Utils 類,是不折不扣的面向過程的編程風格。但這並非說,咱們就要杜絕使用 Utils 類了。實際上,從剛剛講的 Utils 類存在的目的來看,它在軟件開發中仍是挺有用的,能解決代碼複用問題。因此,這裏並非說徹底不能用 Utils 類,而是說,要儘可能避免濫用,不要不加思考地隨意去定義 Utils 類。

在定義 Utils 類以前,你要問一下本身,你真的須要單獨定義這樣一個 Utils 類嗎?是否能夠把 Utils 類中的某些方法定義到其餘類中呢?若是在回答完這些問題以後,你仍是以爲確實有必要去定義這樣一個 Utils 類,那就大膽地去定義它吧。由於即使在面向對象編程中,咱們也並非徹底排斥面向過程風格的代碼。只要它能爲咱們寫出好的代碼貢獻力量,咱們就能夠適度地去使用。

除此以外,類比 Constants 類的設計,咱們設計 Utils 類的時候,最好也能細化一下,針對不一樣的功能,設計不一樣的 Utils 類,好比 FileUtilsIOUtilsStringUtilsUrlUtils 等,不要設計一個過於大而全的 Utils 類。

3. 定義數據和方法分離的類

最後一種面向對象編程過程當中,常見的面向過程風格的代碼。那就是,數據定義在一個類中,方法定義在另外一個類中。

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

實際上,這種開發模式叫做基於貧血模型的開發模式,也是咱們如今很是經常使用的一種 Web 項目的開發模式。

在面向對象編程中,爲何容易寫出面向過程風格的代碼?

能夠聯想一下,在生活中,你去完成一個任務,你通常都會思考,應該先作什麼、後作什麼,如何一步一步地順序執行一系列操做,最後完成整個任務。面向過程編程風格偏偏符合人的這種流程化思惟方式。而面向對象編程風格正好相反。它是一種自底向上的思考方式。它不是先去按照執行流程來分解任務,而是將任務翻譯成一個一個的小的模塊(也就是類),設計類之間的交互,最後按照流程將類組裝起來,完成整個任務。咱們在上一節課講到了,這樣的思考路徑比較適合複雜程序的開發,但並非特別符合人類的思考習慣。

除此以外,面向對象編程要比面向過程編程難一些。在面向對象編程中,類的設計仍是挺須要技巧,挺須要必定設計經驗的。你要去思考如何封裝合適的數據和方法到一個類裏,如何設計類之間的關係,如何設計類之間的交互等等諸多設計問題。

因此,基於這兩點緣由,不少工程師在開發的過程,更傾向於用不太須要動腦子的方式去實現需求,也就情不自禁地就將代碼寫成面向過程風格的了。

面向過程編程及面向過程編程語言就真的無用武之地了嗎?

前面咱們有講到,若是咱們開發的是微小程序,或者是一個數據處理相關的代碼,以算法爲主,數據爲輔,那腳本式的面向過程的編程風格就更適合一些。固然,面向過程編程的用武之地還不止這些。實際上,面向過程編程是面向對象編程的基礎,面向對象編程離不開基礎的面向過程編程。爲何這麼說?咱們仔細想一想,類中每一個方法的實現邏輯,不就是面向過程風格的代碼嗎?

除此以外,面向對象和麪向過程兩種編程風格,也並非非黑即白、徹底對立的。在用面向對象編程語言開發的軟件中,面向過程風格的代碼並很多見,甚至在一些標準的開發庫(好比 JDKApache CommonsGoogle Guava)中,也有不少面向過程風格的代碼。

無論使用面向過程仍是面向對象哪一種風格來寫代碼,咱們最終的目的仍是寫出易維護、易讀、易複用、易擴展的高質量代碼。只要咱們能避免面向過程編程風格的一些弊端,控制好它的反作用,在掌控範圍內爲咱們所用,咱們就大可不用避諱在面向對象編程中寫面向過程風格的代碼。

重點回顧

1. 濫用 gettersetter 方法

在設計實現類的時候,除非真的須要,不然儘可能不要給屬性定義 setter 方法。除此以外,儘管 getter 方法相對 setter 方法要安全些,可是若是返回的是集合容器,那也要防範集合內部數據被修改的風險。

2.Constants 類、Utils 類的設計問題

對於這兩種類的設計,咱們儘可能能作到職責單一,定義一些細化的小類,好比 RedisConstantsFileUtils,而不是定義一個大而全的 Constants 類、Utils 類。除此以外,若是能將這些類中的屬性和方法,劃分歸併到其餘業務類中,那是最好不過的了,能極大地提升類的內聚性和代碼的可複用性。

  1. 基於貧血模型的開發模式

講了爲何這種開發模式是不折不扣的面向過程編程風格的。這是由於數據和操做是分開定義在 VO/BO/EntityControler/Service/Repository 中的。

參考:哪些代碼設計看似是面向對象,實際是面向過程的?

本文由博客一文多發平臺 OpenWrite 發佈!
更多內容請點擊個人博客 沐晨

相關文章
相關標籤/搜索