自從接觸 java 之後,修了資訊安全,開始寫了些密碼學的東西。
其中很多可以藉由 java 的物件導向概念讓程式更為強健、不紊亂。
這算是小筆記,先介紹物件導向的概念(沒有附程式碼也沒有圖)。
物件導向程式設計
1. 封裝(encapsulation)
在物件導向的概念中第一個講的一定是封裝。
當我今天要寫一隻龜兔賽跑的模擬程式。
在以往的結構化程式中大概會把 race()
、run()
等等動作寫成一個函式。
至於跑了幾秒、速度各是多少等等就會把這些參數寫到 main()
當中。
物件導向提供了不一樣的方法來描述這個問題。
物件導向最主要提供的是相近於人類的思考方式。
然而在軟體工程觀點,物件導向最大的優點是程式碼的重複利用(code reusability)。
我們可以把烏龜當成一個物件,對這個物件描述他的屬性(attribute)跟方法(method)。
屬性可以是烏龜跑步的速度、烏龜的名字或是烏龜的成績等等。
只要是關於烏龜的特性都可以藉由定義成屬性加以使用。
方法則是描述烏龜的行為,他可以是 run()
、rush()
、isWin()
等等。
run()
可以讓烏龜開始賽跑,rush()
可以讓烏龜進行衝刺,用 isWin()
來看烏龜是否贏得比賽等等。
等到我們把屬性及方法都定義好之後就要進行封裝。
到這裡我要來介紹一個原則,要依據這個原則進行封裝。
2. 最小權限原則(Principle of least privilege)
當我們把物件的屬性跟方法定義好之後,物件是需要與程式互動的。
所以程式以可能會需要用到物件的屬性或是方法,但是直接存取物件的屬性是危險的。
因為我們不知道什麼時候物件的屬性會遭到外界竄改而發生錯誤。
所以在這裡引用最小權限原則的原文:
Every program and every privileged user of the system should operate using the least amount of privilege necessary to complete the job. – Jerome Saltzer
「在每個程式跟系統的使用者應該使用最小的必要權限去完成工作」
換言之,物件要開放讓其他人存取的東西應該是最少且必要的,如此才能保障物件內部的運作可以順利,免於受到外界程式修改屬性的影響。
所以通常會把物件的屬性設成私有(private),將方法設成公開(public)。
當然這只是通常的作法,物件應該視不同情況需求有所變動。
那如果某天我想要存取物件的屬性怎麼辦?
我們會設一個 setter 跟一個 getter 的方法,利用 setter 去對某屬性加以設定,利用 getter 去得到屬性的值。
與原本直接存取屬性不同是,我們可以在 setter 中加入限制,以防輸入錯誤的值。
像剛剛的烏龜的速度 speed
,可以藉由 setSpeed()
去限制烏龜的速度在什麼範圍。
要是烏龜的速度被設成跟飛機一樣快那不是糟了。
同樣可以寫一個 getSpeed()
去得到烏龜的速度。
如此一來,其他程式在使用烏龜這個物件的時候就不必考慮到內部的運作。
3. 類別(class)
當我們把烏龜寫好,他會被 java 存成一個類別。
外界程式會經由宣告這個類別,將烏龜變成一個實例(instance),我們可以叫他小龜。
我們可以藉由這種方式將很多的烏龜實例加以宣告,造出很多的烏龜,有紅的、有藍的(誤)。
所以類別只是描述烏龜這個物件的一個藍圖,真正將他宣告出來才能加以使用。
4. 繼承(inheritance)
繼承是物件導向當中一個重要的概念。
在表現上類似於生物的分類表,就像貓跟狗都是哺乳類一樣。
所以延續上面的例子,龜兔都是動物,所以龜兔都可以繼承自"動物"這個類別。
我們可以設計動物這個類別,他可含有名字這個屬性,也可以對他 get 或 set。
所以當烏龜或兔子類別繼承自動物的時候。
這時候我們說動物是烏龜跟兔子的父類別(superclass),烏龜就是動物的子類別(subclass)。
(在 C++ 中稱為基礎類別 base class 及衍生類別 derived class)
這時動物的屬性及方法就會被繼承到子類別中,如此可以避免重複的程式碼出現也好維護程式。
這時我們會說繼承關係是is-a關係,是一種關係。
也就是「烏龜**is-a(是一種)**動物」的關係,這也是重要的概念。
5. 多型(polymorphism)
來接續講講多型。
多型這個字的英文其實跟生物的物種多型性是同一個字。
生物的多型性是指同一物種當中的同一族群擁有兩種以上的表現型。
程式的多型也是如此,繼承於同樣的父類別下的子類別可以有不同的表現。
就像之前的例子,動物類別可以有 run()
,當烏龜跟兔子類別繼承了動物類別的 run()
。
烏龜的 run()
有自己的執行方式,兔子也是。
所以當需要執行烏龜自己的 run()
時,可以多型地使用,而不會繼承到動物類別的。
更進一步,可以將烏龜的 run()
及兔子的 run()
中相同的部份加以抽象化並提升到動物類別中。
以避免重複的程式碼,也好維護,同時可以達到多種表現的效果。
多型必須利用**覆寫(override)**父類別的方法來達到效果。
覆寫父類別的方法時,區塊內可以使用父類別的方法來加以建構。
多型當中還有很多的細節,我只在這邊做粗略的介紹而已。
6. 抽象類別(abstract class)及介面(interface)
到這邊為止應該是在程式設計上有點基礎之後的進階說明。
很多人一開始都會有疑問,抽象類別跟介面有什麼差別?
在語法上似乎是差不多的,抽象類別允許宣告抽象方法,介面的方法全是抽象方法。
其實還有很多,我就不贅述,但是在設計觀點,這兩者是不一樣的。
抽象類別仍然還是類別(class),所以他跟其他類別的關係是繼承關係,是is-a關係。
就像「烏龜是一種動物」一樣,動物是抽象的概念,可被寫成抽象類別。
但介面並不一樣,介面跟其他類別的關係是實現(realization),是契約關係。
他會規範其他類別遵守介面定下的標準,必須要實作介面的抽象方法。
舉個例,鳥會飛,飛機也會飛,但是鳥跟飛機是不同的東西,也不會繼承於相同的父類別之下,所以我們可以寫一個"會飛的"介面,這個介面定義抽象方法 fly()
。
當鳥與飛機都實現"會飛的"介面時,就必須要把 fly()
實作為具體方法(concrete method)。
所以鳥跟"會飛的"介面的關係是has-a (擁有, 會)。
這兩者在設計概念上是不同的。
物件導向設計原則
7. 單一職責原則(single responsibility principle)
接下來講講物件導向設計(OOD)中著名的 SOLID 原則。
第一個 S 就是單一職責原則。
這個原則很簡單,他是說一個類別只要有一種職責(功能)就好。
有時候工程師會追求一個類別的強大加了許多附加的功能。
這會使得這個類別難以維護,也失去類別的意義。
所以這個原則強調一個類別只要顧好他自己的職責所在,有簡單而呼應職責的方法即可。
相對也避免單一職責分散在各個類別中。
最極端的例子就是,集各種功能為一身的上帝類別(God class)。
8. 開放封閉原則(open/closed principle)
第二個 O 指的是開放封閉原則,直接引用原文:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. – Bertrand Meyer
在軟體的類別、模組、函式中應該對擴充開放,而對修改關閉。
每當我們要將既有的軟體中加入新的功能,我們應該是將新的元素加入而不是修改既有的程式碼讓他擁有新的功能。
修改會讓程式碼變得越來越龐雜,加入並延伸是讓軟體具有更多的小部份各自運作。
軟體具有這樣的特性的同時,也遵守單一職責原則會讓軟體更好。
9. 芮氏替換原則(Liskov substitution principle)
Let $q(x)$ be a property provable about objects $x$ of type $T$. Then $q(y)$ should be provable for objects $y$ of type $S$ where $S$ is a subtype of $T$. – Barbara Liskov and Jeannette Wing
一開始就把抽象的原文拉進來,這是個關於抽象化的部份。
在敘述一個繼承關係的時候,子類別是一種父類別,所以子類別可以替換父類別被使用。
將原文解釋一下: 假設 $S$ 是 $T$ 的 subtype,$x$ 是 $T$ 中的物件,$q(x)$ 是成立的,那對 $y$ 是 $S$ 的物件,$q(y)$ 也是成立的。
這是用來避免一些狀況,當有一個鳥的類別被老鷹類別及鴕鳥類別繼承。
通常鳥都會飛,所以我們在鳥類別中加入 fly()
,但是鴕鳥卻不會飛。
這種狀況阻礙了多型的操作,鴕鳥無法飛,也無法 override 父類別的 fly()
。
比較好的作法是做一個"會飛的"介面,並讓會飛的鳥去實現,將鳥類別中的 fly()
移除。
10. 介面隔離原則(interface segregation principle)
在物件導向設計當中,介面(interfaces)提供了一個抽象化層,可以在提供概念上的解釋,同時避免依賴性的屏障。
物件的功能會隨需求越多,類別會越來越龐大,繼承的層數也會增多。
當底層的類別還要在被子類別繼承時,子類別會依賴於父類別的方法。
或許子類別並不會用到父類別的某些方法,這些方法會變成子類別的累贅。
這時應該定義新的介面,讓需要的類別去實現介面,這樣就不會有多餘的方法被繼承。
當然,介面所定義的方法也要盡可能的少,才能被廣泛的使用。
11. 依賴反轉原則(dependency inversion principle)
在傳統的應用架構中,低層次的模組被高層次的模組使用,進而構成一個複雜的系統。
在這種複合關係(composition)下,高層次的模組直接依賴於低層次的模組去執行一些任務。
這個時候低層次模組的依賴限制了高層次模組的重複使用性。
在這種情況下,為了使得高層次的模組不依賴於低層次的模組,規定:
- 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象體
- 抽象體不應該依賴於具體細節,而具體細節則應該依賴於抽象體
12. Don’t repeat yourself(DRY)
接下來講講簡單的。
如字面上的意思一樣,盡量不要寫重複的程式碼。
13. Keep it simple, stupid(KISS)
如以上縮寫,寫程式需要親親(誤),快結尾了不說笑XD
程式碼越簡單越"笨"越好。
當然並不是把簡單可以完成的工作使用了很多 if else 完成。
單一的函式只處理他"分內的工作"就好,不要去理外界需要什麼。
14. Program to an interface, not an implementation.
這算是一句至理名言。寫程式到了一定的程度之後,比起實作,更需要關注的是那些介面。
對介面的設計,比起對實作的設計要來得重要的多。
參考:PrinciplesOfOod
留言與分享