設計模式之美筆記二
本文極客時間課程設計模式之美的個人閱讀筆記,有不盡詳細之處請抱歉,文未可以訂閱極客時間該課程。
- OOP 優勢
- 哪些代碼設計看似是面向對象,實際是面向過程的?
- 濫用 getter、setter 方法
- 濫用全局變量和全局方法
- 定義資料和方法分離的類
- 接口vs抽象類
- 如何決定該用抽象類還是接口?
- 為什麼基于接口而非實作程式設計?有必要為每個類都定義接口嗎?
OOP 優勢
- OOP 更加能夠應對大規模複雜程式的開發
- OOP 風格的代碼更易複用、易擴充、易維護
- OOP 語言更加人性化、更加進階、更加智能
Unix、Linux 等複雜系統是使用C面向過程的語言,如何看?
- 作業系統是業務無關的,它更接近于底層計算機,是以更适合用面向過程的語言編寫。而接近業務的也就是接近人的軟體,則更适合用面向對象的語言編寫。
- 作業系統雖然是用面向過程的C語言實作的 但是其設計邏輯是面向對象的。C語言沒有類和對象的概念,但是用結構體(struct)同樣實作了資訊的封裝,核心源碼中也不乏繼承和多态思想的展現。面向對象思想,不局限于具體語言。
哪些代碼設計看似是面向對象,實際是面向過程的?
濫用 getter、setter 方法
而面向對象封裝的定義是:通過通路權限控制,隐藏内部資料,外部僅能通過類提供的有限的接口通路、修改内部資料。是以,暴露不應該暴露的 setter 方法,明顯違反了面向對象的封裝特性。資料沒有通路權限控制,任何代碼都可以随意修改它,代碼就退化成了面向過程程式設計風格的了。
在設計實作類的時候,除非真的需要,否則,盡量不要給屬性定義 setter 方法。除此之外,盡管 getter 方法相對 setter 方法要安全些,但是如果傳回的是集合容器(比如例子中的 List 容器),也要防範集合内部資料被修改的危險。
濫用全局變量和全局方法
在面向對象程式設計中,常見的全局變量有單例類對象、靜态成員變量、常量等,常見的全局方法有靜态方法。
單例類對象在全局代碼中隻有一份,是以,它相當于一個全局變量。
靜态成員變量歸屬于類上的資料,被所有的執行個體化對象所共享,也相當于一定程度上的全局變量。
常量是一種非常常見的全局變量,比如一些代碼中的配置參數,一般都設定為常量,放到一個 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:";
// ...省略更多的常量定義...
}
缺點:
- 影響代碼的可維護性
- 增加代碼的編譯時間
- 影響代碼的複用性
Utils 類 :
隻包含靜态方法不包含任何屬性的 Utils 類,是徹徹底底的面向過程的程式設計風格。
我們設計 Utils 類的時候,最好也能細化一下,針對不同的功能,設計不同的 Utils 類,比如 FileUtils、IOUtils、StringUtils、UrlUtils 等,不要設計一個過于大而全的 Utils 類。
定義資料和方法分離的類
接口vs抽象類
抽象類:
- 抽象類不允許被執行個體化,隻能被繼承。也就是說,你不能 new 一個抽象類的對象出來(Logger logger = new Logger(…); 會報編譯錯誤)。
- 抽象類可以包含屬性和方法。方法既可以包含代碼實作(比如 Logger 中的 log() 方法),也可以不包含代碼實作(比如 Logger 中的 doLog() 方法)。不包含代碼實作的方法叫作抽象方法。
- 子類繼承抽象類,必須實作抽象類中的所有抽象方法。對應到例子代碼中就是,所有繼承 Logger 抽象類的子類,都必須重寫 doLog() 方法。
接口:
- 接口不能包含屬性(也就是成員變量)。
- 接口隻能聲明方法,方法不能包含代碼實作。
- 類實作接口的時候,必須實作接口中聲明的所有方法。
c++ 抽象類
class Box
{
public:
// 純虛函數
virtual double getVolume() = 0;
private:
double length; // 長度
double breadth; // 寬度
double height; // 高度
};
設計抽象類(通常稱為 ABC)的目的,是為了給其他類提供一個可以繼承的适當的基類。抽象類不能被用于執行個體化對象,它隻能作為接口使用。如果試圖執行個體化一個抽象類的對象,會導緻編譯錯誤。
是以,如果一個 ABC 的子類需要被執行個體化,則必須實作每個虛函數,這也意味着 C++ 支援使用 ABC 聲明接口。如果沒有在派生類中重寫純虛函數,就嘗試執行個體化該類的對象,會導緻編譯錯誤。
可用于執行個體化對象的類被稱為具體類。
C++ 隻有抽象類,并沒有接口
c++ 實作接口
class Strategy { // 用抽象類模拟接口
public:
~Strategy();
virtual void algorithm()=0;
protected:
Strategy();
};
抽象類 Strategy 沒有定義任何屬性,并且所有的方法都聲明為 virtual 類型(等同于 Java 中的 abstract 關鍵字),這樣,所有的方法都不能有代碼實作,并且所有繼承這個抽象類的子類,都要實作這些方法。從文法特性上來看,這個抽象類就相當于一個接口。
如何決定該用抽象類還是接口?
如果要表示一種 is-a 的關系,并且是為了解決代碼複用的問題,我們就用抽象類;
如果我們要表示一種 has-a 關系,并且是為了解決抽象而非代碼複用的問題,那我們就可以使用接口。
為什麼基于接口而非實作程式設計?有必要為每個類都定義接口嗎?
基于接口而非實作程式設計:Program to an interface, not an implementation
“接口”就是一組“協定”或者“約定”,是功能提供者提供給使用者的一個“功能清單”。
“基于接口而非實作程式設計”這條原則中的“接口”,可以了解為程式設計語言中的接口或者抽象類。
在軟體開發中,最大的挑戰之一就是需求的不斷變化,這也是考驗代碼設計好壞的一個标準。越抽象、越頂層、越脫離具體某一實作的設計,越能提高代碼的靈活性,越能應對未來的需求變化。好的代碼設計,不僅能應對當下的需求,而且在将來需求發生變化的時候,仍然能夠在不破壞原有代碼設計的情況下靈活應對。而抽象就是提高代碼擴充性、靈活性、可維護性最有效的手段之一。
“基于接口而非實作程式設計”的實作原則:
- 函數的命名不能暴露任何實作細節。
- 封裝具體的實作細節。
- 為實作類定義抽象的接口。
如果在我們的業務場景中,某個功能隻有一種實作方式,未來也不可能被其他實作方式替換,那我們就沒有必要為其設計接口,也沒有必要基于接口程式設計,直接使用實作類就可以了。
設計模式之美
