常見的編程範式或者說編程風格有三種,面向過程編程、面向對象編程、函數式編程,而面向對象編程又是這其中最主流的編程範式。現現在,大部分編程語言都是面向對象編程語言,大部分軟件都是基於面向對象編程這種編程範式來開發的。前端
不過,在實際的開發工做中,總覺得把全部代碼都塞到類裏,天然就是在進行面向對象編程了。實際上,這樣的認識是不正確的。有時候,從表面上看似是面向對象編程風格的代碼,從本質上看倒是面向過程編程風格的。java
在用面向對象編程語言進行軟件開發的時候,咱們有時候會寫出面向過程風格的代碼。有些是有意爲之,並沒有不妥;而有些是無心爲之,會影響到代碼的質量。mysql
1. 濫用 getter
、setter
方法算法
它違反了面向對象編程的封裝特性,至關於將面向對象編程風格退化成了面向過程編程風格。經過下面這個例子來給你解釋一下這句話。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
)屬性:itemsCount
、totalPrice
、items
。對於 itemsCount
、totalPrice
兩個屬性,咱們定義了它們的 getter
、setter
方法。對於 items
屬性,咱們定義了它的 getter
方法和 addItem()
方法。代碼很簡單,理解起來不難。那你有沒有發現,這段代碼有什麼問題呢?編程
咱們先來看前兩個屬性,itemsCount
和 totalPrice
。雖然咱們將它們定義成 private
私有屬性,可是提供了 public
的 getter
、setter
方法,這就跟將這兩個屬性定義爲 public
公有屬性,沒有什麼兩樣了。外部能夠經過 setter
方法隨意地修改這兩個屬性的值。除此以外,任何代碼均可以隨意調用 setter
方法,來從新設置 itemsCount
、totalPrice
屬性的值,這也會致使其跟 items
屬性的值不一致。小程序
而面向對象封裝的定義是:經過訪問權限控制,隱藏內部數據,外部僅能經過類提供的有限的接口訪問、修改內部數據。因此,暴露不該該暴露的 setter
方法,明顯違反了面向對象的封裝特性。數據沒有訪問權限控制,任何代碼均可以隨意修改它,代碼就退化成了面向過程編程風格的了。後端
看完了前兩個屬性,咱們再來看 items
這個屬性。對於 items
這個屬性,咱們定義了它的 getter
方法和 addItem()
方法,並無定義它的 setter
方法。這樣的設計貌似看起來沒有什麼問題,但實際上並非。安全
對於 itemsCount
和 totalPrice
這兩個屬性來講,定義一個 public
的 getter
方法,確實無傷大雅,畢竟 getter
方法不會修改數據。可是,對於 items
屬性就不同了,這是由於 items
屬性的 getter
方法,返回的是一個 List
集合容器。外部調用者在拿到這個容器以後,是能夠操做容器內部數據的,也就是說,外部代碼仍是能修改 items
中的數據。好比像下面這樣:前後端分離
ShoppingCart cart = new ShoppCart(); ... cart.getItems().clear(); // 清空購物車
你可能會說,清空購物車這樣的功能需求看起來合情合理啊,上面的代碼沒有什麼不妥啊。你說得沒錯,需求是合理的,可是這樣的代碼寫法,會致使 itemsCount
、totalPrice
、items
三者數據不一致。咱們不該該將清空購物車的業務邏輯暴露給上層代碼。正確的作法應該是,在 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異常
不過,這樣的實現思路仍是有點問題。由於當調用者經過 ShoppingCart
的 getItems()
獲取到 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
類,好比 FileUtils
、IOUtils
、StringUtils
、UrlUtils
等,不要設計一個過於大而全的 Utils
類。
3. 定義數據和方法分離的類
最後一種面向對象編程過程當中,常見的面向過程風格的代碼。那就是,數據定義在一個類中,方法定義在另外一個類中。
傳統的 MVC
結構分爲 Model
層、Controller
層、View
層這三層。不過,在作先後端分離以後,三層結構在後端開發中,會稍微有些調整,被分爲 Controller
層、Service
層、Repository
層。Controller
層負責暴露接口給前端調用,Service
層負責核心業務邏輯,Repository
層負責數據讀寫。而在每一層中,咱們又會定義相應的 VO
(View Object
)、BO
(Business Object
)、Entity
。通常狀況下,VO
、BO
、Entity
中只會定義數據,不會定義方法,全部操做這些數據的業務邏輯都定義在對應的 Controller
類、Service
類、Repository
類中。這就是典型的面向過程的編程風格。
實際上,這種開發模式叫做基於貧血模型的開發模式,也是咱們如今很是經常使用的一種 Web
項目的開發模式。
能夠聯想一下,在生活中,你去完成一個任務,你通常都會思考,應該先作什麼、後作什麼,如何一步一步地順序執行一系列操做,最後完成整個任務。面向過程編程風格偏偏符合人的這種流程化思惟方式。而面向對象編程風格正好相反。它是一種自底向上的思考方式。它不是先去按照執行流程來分解任務,而是將任務翻譯成一個一個的小的模塊(也就是類),設計類之間的交互,最後按照流程將類組裝起來,完成整個任務。咱們在上一節課講到了,這樣的思考路徑比較適合複雜程序的開發,但並非特別符合人類的思考習慣。
除此以外,面向對象編程要比面向過程編程難一些。在面向對象編程中,類的設計仍是挺須要技巧,挺須要必定設計經驗的。你要去思考如何封裝合適的數據和方法到一個類裏,如何設計類之間的關係,如何設計類之間的交互等等諸多設計問題。
因此,基於這兩點緣由,不少工程師在開發的過程,更傾向於用不太須要動腦子的方式去實現需求,也就情不自禁地就將代碼寫成面向過程風格的了。
前面咱們有講到,若是咱們開發的是微小程序,或者是一個數據處理相關的代碼,以算法爲主,數據爲輔,那腳本式的面向過程的編程風格就更適合一些。固然,面向過程編程的用武之地還不止這些。實際上,面向過程編程是面向對象編程的基礎,面向對象編程離不開基礎的面向過程編程。爲何這麼說?咱們仔細想一想,類中每一個方法的實現邏輯,不就是面向過程風格的代碼嗎?
除此以外,面向對象和麪向過程兩種編程風格,也並非非黑即白、徹底對立的。在用面向對象編程語言開發的軟件中,面向過程風格的代碼並很多見,甚至在一些標準的開發庫(好比 JDK
、Apache Commons
、Google Guava
)中,也有不少面向過程風格的代碼。
無論使用面向過程仍是面向對象哪一種風格來寫代碼,咱們最終的目的仍是寫出易維護、易讀、易複用、易擴展的高質量代碼。只要咱們能避免面向過程編程風格的一些弊端,控制好它的反作用,在掌控範圍內爲咱們所用,咱們就大可不用避諱在面向對象編程中寫面向過程風格的代碼。
1. 濫用 getter
、setter
方法
在設計實現類的時候,除非真的須要,不然儘可能不要給屬性定義 setter
方法。除此以外,儘管 getter
方法相對 setter
方法要安全些,可是若是返回的是集合容器,那也要防範集合內部數據被修改的風險。
2.Constants
類、Utils
類的設計問題
對於這兩種類的設計,咱們儘可能能作到職責單一,定義一些細化的小類,好比 RedisConstants
、FileUtils
,而不是定義一個大而全的 Constants
類、Utils
類。除此以外,若是能將這些類中的屬性和方法,劃分歸併到其餘業務類中,那是最好不過的了,能極大地提升類的內聚性和代碼的可複用性。
講了爲何這種開發模式是不折不扣的面向過程編程風格的。這是由於數據和操做是分開定義在 VO/BO/Entity
和 Controler/Service/Repository
中的。