作者: 點先生, 時間: 2018.6.26
前言
不知道是否有許多萌新跟我一樣,在看java原始碼的時候,腦袋容易暈。通常查一個方法,要跳幾個類出來,有些類動不動就上千行。像我這樣血氣方剛的少年,哪靜得下心來理解這麼多結構複雜的程式碼!還不如看點番劇,喝點快樂肥宅水!
看吧,不知道該如何下手去啃原始碼。不看吧,心裡也急得很。總不能一直這樣放著吧,既然選擇了做這行, 還是得好好學習,畢竟面向工資程式設計。於是在某位大神的指導下,推薦我先學習設計模式。okk!那接下來我們就一起來學習設計模式!什麼是設計模式
- 設計模式(Design pattern)代表了最佳的實踐,通常被有經驗的物件導向的軟體開發人員所採用。
- 設計模式是軟體開發人員在軟體開發過程中面臨的一般問題的解決方案。這些解決方案是眾多軟體開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
- 設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、程式碼設計經驗的總結
為何要學習設計模式
- 別人都學你不學,是想當鹹魚嗎?大佬都懂你不懂,不想混進大佬圈裝逼了?
- 當你用模式描述的時候,其他開發人員很容易知道你對設計的想法。
- 使用模式談論軟體系統,可以讓你保持在設計層次上,而不會壓低到物件與類這種瑣碎的事情上。
- 當用模式名稱交流時,你們之間交流的不只是模式名稱,而是一整套模式背後所象徵的質量,特性,約束。
設計原則
提倡使用設計模式的根本原因是為了程式碼複用,增加可維護性。現在被命名的23種設計模式就是遵守了以下六大設計原則,才達到了程式碼複用,增加可維護性的目的。
- 單一職責原則: There should never be more than one reason for a class to change. 不要存在多於一個導致類變更的原因,一個類只承擔一個職責
- 開閉原則: Software entities like classes,modules and functions should be open for extension but closed for modifications. 類、模組、函式,對擴充套件是開放的,對修改是封閉的。
- 裡式替換原則: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. 子類可以擴充套件父類的功能,但不能改變父類原有的功能
- 迪米特原則(最少知識原則): Only talk to you immediate friends. 儘量減少物件之間的互動,從而減小類之間的耦合。
- 介面隔離原則: The dependency of one class to another one should depend on the smallest possible interface. 不要對外暴露沒有實際意義的介面。
- 依賴倒置原則: High level modules should not depends upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions. 核心思想:面向介面程式設計
走進設計原則
看了這麼多理論東西了,再不來點程式碼刺激刺激神經,我都要點右上角了。接下來,我們就通過一個例子,小寫一點程式碼,讓我們更好的理解設計模式! 現在我們來設計一個遊戲人物。(為了突出重點,更好的理解,只講部分功能,理解要表達的意思即可,並不會真正的把所有功能實現。)
class Character {
//使用武器
fun useWeapon() {
Log.i("TAG", "fist")
}
}
複製程式碼
現在有了一個初步人物模型的設計,實現了攻擊的方法。當人物到了一定等級,可以轉職為魔法師,道士,戰士的話,我們可以這樣寫:
class Character {
//使用武器
fun useWeapon(profession : String) {
if(profession.equals("magician")){
Log.i("TAG", "火牆風咆哮")
}
if(profession.equals("taoist")){
Log.i("TAG", "召喚4級寶寶")
}
if(profession.equals("warrior")){
Log.i("TAG", "刀刀烈火")
}
}
}
複製程式碼
如果每個方法都這樣寫,當方法數量增多的時候,這樣的寫法就變得很雜亂無章。**還導致了影響類變化原因不止一個,也就違反了“單一職責原則”。**那我們現在換一種方式來寫:建立Magician,Taoist,Warrior三個類。在父類中採用過載的方式來實現useWeapon()。
//魔法師使用武器
fun useWeapon(magician: Magician) {
Log.i("TAG", "火牆風咆哮")
}
//道士使用武器
fun useWeapon(taoist: Taoist) {
Log.i("TAG", "召喚4級寶寶")
}
//戰士使用武器
fun useWeapon(warrior: Warrior) {
Log.i("TAG", "刀刀烈火")
}
複製程式碼
這樣做更傻,不僅沒有遵守單一職責原則,還違反了迪米特法則。 實際上,當我們一看到這種需求,有點經驗的都知道,應該寫三個類,分別對應魔法師,道士,戰士,而不是把所有東西寫進一個類裡面來。useWeapon方法寫在父類中的缺點已經暴露出來了,直覺告訴我們,這個方法是應該寫在子類中的。那父類中的這個方法留不留呢?裡式替換原則告訴我們,子類可以擴充套件父類的功能,但不能改變父類原有的功能。於是乎……
class Magician : Character(){
fun useWeapon(){
Log.i("TAG", "火牆風咆哮")
}
}
複製程式碼
class Taoist : Character(){
fun useWeapon(){
Log.i("TAG", "召喚4級寶寶")
}
}
複製程式碼
class Warrior : Character() {
fun useWeapon(){
Log.i("TAG", "刀刀烈火")
}
}
複製程式碼
現在來看,好像沒什麼問題哦。但是卻沒有遵守依賴倒置原則。我們應該儘量的針對介面程式設計,而不是針對實現程式設計。而現在我們把實際的行為都寫在子類當中!這樣的壞處的是你必須去在每一個子類中手寫useWeapon方法,修改起來相當麻煩!而且在程式碼執行時沒辦法更改具體行為(除非寫更多程式碼,但那樣並划不來)。
現在這樣寫還有點怪怪的,明明每一個類有useWeapon(),卻不能寫進父類中。很氣有沒有! 那我們把character寫成一個介面?把方法寫進去? 牛逼!真是太聰明瞭!既不違反單一職責原則,也不違反迪米特法則,還遵守了裡式替換原則。
牛逼個雞兒!
現在我們只考慮了玩家的角色,遊戲裡的NPC怎麼辦? 你打得過NPC? 假如我們已經把父類寫成了介面,再建立一個npc類,看看吧。
interface Character {
//使用武器
fun useWeapon()
}
複製程式碼
class NPC :Character {
override fun useWeapon() {
//空方法
}
}
複製程式碼
這樣造成了npc類裡面有一個空方法。你可能永遠都不會去用它。那放這兒有什麼意思?這樣寫還違反了介面隔離原則:不要對外暴露沒有實際意義的介面!不要對外暴露沒有實際意義的介面!不要對外暴露沒有實際意義的介面! 問題不大!只需要打一個響指!我們重新理一理思緒!
現在問題在於我們要讓某些子類實現useWeapon(),而不是全部子類都要去實現useWeapon()。 okk的!useWeapon()既然不能放進介面裡面,也不能放進父類裡面,那我們就把這個方法單獨提出來,新寫一個useWeapon介面!
interface IUseWeapon {
fun useWeapon()
}
複製程式碼
然後讓有攻擊功能的子類來實現IUseWeapon介面。 emmmmmmmmm…… 這樣使用介面還是得一個個去寫子類實現的具體方法,而且也沒有遵守依賴倒置原則,在上面我已經寫過了,依賴倒置原則的核心思想就在於面向介面程式設計,那面向介面程式設計是個啥意思呢?
“針對介面程式設計”真正的意思是“針對超型別程式設計”。
- “針對介面程式設計”,關鍵就在於多型!利用多型,程式可以針對超型別程式設計,執行時會根據實際狀況執行到真正的行為,不會被綁死咋超型別的行為上。
- “針對超型別程式設計”這句話,可以更明確地說成變數的宣告型別應該是超型別,通常是一個抽象類或者是一個介面。
- 只要是具體實現此超型別的類,所產生的物件,都可以指定給這個變數。這也意味著,宣告類時不用理會以後會執行時的真正物件!
//針對實現程式設計
var magician : Magician = Magician()
//針對介面\超型別程式設計
var character : Character = Magician()
複製程式碼
此時我們已經有了一個IUseWeapon介面,裡面只有一個useWeapon()方法。我們不能用子類直接實現IUseWeapon,也不能用父類直接實現IUseWeapon,那我們就專門建立一個“行為類”來實現行為介面!
class UseFireWall : IUseWeapon {
override fun useWeapon() {
Log.i("TAG", "火牆風咆哮")
}
}
複製程式碼
class UseDogBaby : IUseWeapon{
override fun useWeapon() {
Log.i("TAG", "召喚4級寶寶")
}
}
複製程式碼
class UseFireKnife : IUseWeapon{
override fun useWeapon() {
Log.i("TAG", "刀刀烈火")
}
}
複製程式碼
這樣的設計,就讓使用武器這個行為跟character類無關了,還可以被其他物件服複用。而新增一些使用武器行為時候,不會影響到既有的行為類,也不會影響使用到行為類的character類。
現在我們在整合一下整個人物的設計:
- 擁有一個父類Character
- 擁有四個子類,Magician, Taoist, Warrior, NPC
- 有一個行為介面IUseWeapon
- 有三個行為類實現了行為介面
目標:遵守六大設計原則的條件下,使Magician, Taoist, Warrior 才有useWeapon()
要實現攻擊的功能,那父類肯定得有呼叫useWeapon()的方法,也必須得擁有行為介面。所以……
open class Character {
lateinit var iUseWeapon :IUseWeapon
fun coverUseWeapon(){
iUseWeapon.useWeapon()
}
}
複製程式碼
在編譯時,已經能通過charater.coverUseWeapon()呼叫使用武器的方法。在程式碼真正執行時,子類還沒對iUseWeapon進行宣告,所以在子類中要做的只是宣告iUseWeapon而已。這個時候,行為類實現行為介面的好處就體現出來了,我們希望子類做什麼樣的攻擊,就可以宣告為什麼樣的行為類,要想有新的新的攻擊動作,再建立一個行為類去實現IUseWeapon就可以了。反正實現的程式碼沒有寫在子類中,而是在行為類中,不用更改之前寫的所有程式碼。
class Magician : Character(){
init {
iUseWeapon = UseFireWall()
}
}
複製程式碼
美滋滋!就這麼簡單的一行程式碼!但這樣還是不夠靈活,我們還是在子類中做了一小部分的具體實現(建立iUseWeapon的例項),也就是沒有完全的做到針對介面程式設計,所以我們需要有一個可以更改iUseWeapon例項的方法。
java程式碼中,我們可以在父類中加入set方法。
public void setIUseWeapon(iUseWeapon : IUseWeapon) {
this.iUseWeapon = iUseWeapon ;
}
複製程式碼
kotlin程式碼中,直接在宣告character物件處
//讓魔法師召喚4級寶寶
var character : Character = Magician()
character.iUseWeapon = UseDogBaby()
複製程式碼
子類裡面的init方法,就可以完全不寫了。
最後
相信各位看到這裡對六大原則其中五個都有了一定的理解,剩下一個開閉原則沒有提到,是因為,這玩意兒不好講! 設計模式就是個經驗性的東西,你完全可以不照著這樣去寫你的程式碼,只要遵守六大設計原則,都是好的設計。
以下是我“設計模式系列”文章,歡迎大家關注留言投幣丟香蕉。