發表文章

[Design Pattern] 代理模式 (Proxy Pattern)

圖片
1. 案例需求


圖片來源:http://www.horace.org/blog/2015/11/22/jojo-%E5%A5%87%E5%A6%99%E5%86%92%E9%9A%AA-%E7%AC%AC%E4%BA%8C%E5%AD%A3%EF%BC%88%E5%8B%95%E7%95%AB%E7%89%88%EF%BC%89/ 知名動畫-JoJo的奇妙冒险,描述喬斯達家族歷代主人翁與各種敵人戰鬥的故事。在《星塵鬥士》篇幅,引入了替身能力,可以讓人產生另一個戰鬥角色,依照主人的意志代替主人戰鬥。

2. 程式設計 建立一個角色(Character)介面,有一個攻擊(Attack)的方法。以主角承太郎(Jotaro)為例,他擁有替身白金之星(Star Platinum),特點是快速又堅硬的攻擊。承太郎白金之星實作Character介面,且白金之星關連了承太郎,如下UML類別圖:



  這三個類別的實作的程式碼:


  模擬承太郎戰鬥的場景程式和輸出結果:

圖片來源:http://knowyourmeme.com/photos/1202911-jojos-bizarre-adventure
3. 代理模式介紹3.1 代理模式定義 根據書上的介紹,代理模式的定義:Provide a surrogate or placeholder for another object to control access to it.。意思是給一個物件新增一個代理,這代理能控制這物件的引用。

  代理模式的通用UML類別圖:


  而代理模式包含三大角色:

Subject:是一個抽象類別或介面,定義Proxy和RealSubject的共同介面,當任何有使用RealObject的地方皆能使用Proxy。依照前述例子,是Character類別。

RealSubject:被代理的角色,也是實際執行商業邏輯的單位。依照前述例子,是Jotaro類別。

Proxy:代理角色,且引用RealSubject,客戶要與RealSubject互動得透過Proxy。依照前述例子,是StarPlatinum類別,引用了Jotaro物件。


3.2 代理模式優缺點 優點
真實角色(RealSubject)完成實際的商業邏輯,其他非它的職責交由代理完成當真實角色商業邏輯變化時,只要Subject定義的介面不變,Proxy仍不用修…

[Design Pattern] 裝飾者模式 (Decorator Pattern)

圖片
1. 案例需求

圖片來源:https://www.rentcafe.com/blog/cities/renting-a-house/
  在外地求學、工作時,大部分需要找租屋來住,每種租屋提供的設施、環境都不同,有的靠近捷運、有電視、有冰箱、有洗衣機等。假如有房東要介紹自己的租屋內容,那程式如何設計呢?

2. 程式設計(錯誤)

  建立一個建築物(Building)抽象類別,有一個描述(Show)的方法,另外有公寓和透天屋需透過繼承它並實作Show方法,如下UML類別圖:


  在Building裡定義三個bool值,可設定該租屋是否有這些環境設施,並在Show方法依據這些值做環境設施描述,程式碼:
  Apartment和Townhouse具體類別:

  模擬租屋宣傳的場景程式和輸出結果:

  程式很完美的宣傳租屋環境,但是,如果要環境設施不只上述三點呢?有電視、有網路、有桌子、有沙發...多了這麼多條件,是不是需要改底層Building抽象類別?這樣會違反開閉原則

(正確)

  為了解決上述問題,新增一個裝飾者(Decorator)抽象類別,將租屋的環境設施個別繼承這類別。其UML類別圖如下:


  Building抽象類別:
  Apartment和Townhouse具體類別:
  Decorator抽象類別,該類別繼承Building,且用組合的方式將Building作為自己的屬性。實做Show方法是呼叫組合的Building物件的Show:   WashingMachine、Refrigerator和CloseToMRT繼承Decorator,實做自己的Show方法,且要呼叫父類別(Decorator)的Show:
  模擬使用裝飾者模式的租屋宣傳的場景程式和輸出結果:
3. 裝飾者模式介紹3.1 裝飾者模式定義 根據書上的介紹,裝飾者模式的定義:Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality。意思是動態地給一個物件新增一些額外的責任,以增加功能來說,裝飾者比衍…

[Design Pattern] 迪米特法則 (Law of Demeter)

1. 迪米特法則介紹1.1 迪米特法則定義 迪米特法則又稱為最少知識原則(Least Knowledge Principle),其發表的原意包含這項解釋:Only talk to your immediate friends。
  兩物件之間的耦合稱為朋友關係,包含組合、聚合、依賴等,而迪米特法則要求只與直接的朋友溝通。
1.2 迪米特法則案例(錯誤)

  現在有很多語音助理,能跟它用口語方式下達命令,做撥打電話、查天氣、找聯絡人等功能。假如設計出一個語音助理類別Assistant,叫它幫我計算數學式,其呼叫的程式碼如下:
  上方程式碼是透過assistant取得calculator物件,再由calculator計算數學式,並回傳運算結果。但是,為何計算數學會需要知道calculator這個物件的存在呢?

(正確)
  我只要語音助理做我要的功能,不要再傳我不認識的物件給我。透過迪米特法則,可改成下列程式碼,只讓Human呼叫public的Calculate函式。

1.3 迪米特法則的實踐適當的解耦:迪米特法則的核心概念是為了類別之間能高內聚、低耦合,達到類別可重複使用。但是套用此原則時,需要做跳轉,才能呼叫需要的類別。以前述套用迪米特法則的計算數學例子,中間經過了一次跳轉,最後呼叫Calculator的Calculate函式。如果設計的跳轉次數太多,會造成系統更複雜而維護更困難,因此盡量跳轉在2次內就好。參考文獻程杰 大話設計模式 第11章|無熟人難辦事?—迪米特法則秦小波 設計模式之禪 第5章 迪米特法則

[Design Pattern] 介面隔離原則 (Interface Segregation Principle)

圖片
1. 介面隔離原則介紹1.1 介面隔離原則定義 根據書上的介紹,介面隔離原則的定義:Clients should not be forced to depend upon interfaces that they don't use.。其意思是客戶端不應該依賴它不需要的介面。


1.2 介面隔離原則案例

圖片來源:https://support.apple.com/zh-tw/iphone   
(錯誤)

  現在的智慧型手機有許多的功能,除了原有通話功能外,還有照相、USB傳輸、上網、記憶卡等。如果有一位上班族擁有一款智慧型手機,其UML類別圖如下:




Worker類別定義一個建構函數和一個方法Call,透過建構函數將IPhone物件給Worker,再透過call可用手機打電話;IPhone介面定義四個方法,包含撥號、USB傳輸、拍照和記憶卡。而IPhoneX是實作IPhone介面的具體類別。
  Worker的程式碼如下,其中call裡面呼叫IPhone的Dial方法:
  IPhone的程式碼如下:   IPhoneX的程式碼如下:   完成這三項程式碼,可以執行上班族打電話的應用程式:
  問題來了,假如這位上班族要出差,需要到一個限定手機只能撥打電話功能的工作環境,那麼他的潮牌IPhoneX就不能使用了!

(正確)
  要解決前述的問題,需使用介面隔離原則,將Dial這功能放置別的介面,新的UML類別圖如下:



  建立兩個介面:ISimplePhone和ISmartPhone,分別定義基本手機的功能和智慧型手機的功能,這兩個介面程式碼如下:
  實作ISimplePhone的具體類別StupidPhone如下:   由於手機的介面改了,因此也要修改Worker的依賴介面為ISimplePhone,修改後的程式碼:
  用手機打電話的應用程式:
  如此一來,上班族能依工作換環境來替換智障型手機或智慧型手機。
1.3 使用介面隔離原則的實踐介面功能盡量少:要避免Fat Interface的狀況,能拆除功能就拆,達到一個介面只服務一個子模組/商業邏輯。但有可能會與單一職責原則有衝突,比如已經拆成最仔細但功能還是很多,此時要以單一職責原則為優先。介面若被設計得太汙染,可使用轉接器模式處理。參考文獻秦小波 設計模式之禪 第4章 接口隔離原則Rob…

[Design Pattern] 依賴倒轉原則 (Dependency Inversion Principle)

圖片
1. 依賴倒轉原則介紹1.1 依賴倒轉原則定義 根據書上的介紹,依賴倒轉原則的定義:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.。其意思包含三項

高層模組不應該依賴低層模組,兩者都應該依賴抽象抽象不應該依賴細節細節應該依賴抽象

1.2 依賴倒轉原則案例
圖片來源:https://itadakimasuanime.wordpress.com/2016/12/14/12-days-of-anime-2016-food-edition-day-1-breakfast-year-in-review/   
(錯誤)

  許多民眾的一天主要能量來源,來自於早餐店,如果早餐店想賣個漢堡做生意,其UML類別圖如下:




  BreakfastShop類別定義一個方法cook,可以製作漢堡;Hamburger類別定義一個方法recipe,可以提供做漢堡的食材。
  BreakfastShop的程式碼如下,其中cook裡面呼叫Hamburger的recipe方法:
  Hamburger的程式碼如下:   定義了BreakfastShop和Hamburger後,可以執行做漢堡的應用程式:
  問題來了,早餐店怎可能只做漢堡當生意來源呢?需要提供個飲料給客人喝解渴吧!
  此時再多做奶茶作為飲品,新增一個奶茶類別:   新增奶茶類別後,再看看BreakfastShop類別,發現早餐店沒有做奶茶的方法!這樣的設計是有問題的,變成早餐店和漢堡是緊耦合的關係,被依賴者的變更會需要修改依賴者,是個沒有穩定性的設計。

(正確)
  要解決前述的問題,需使用依賴倒轉原則,新的早餐店UML設計如下:

  建立兩個介面:IBreakfastShop和IBreakfast,分別定義早餐店和早餐的各個職務,早餐店要會做早餐,需實作cook方法,IBreakfastShop程式碼如下:
  此介面是一個抽象化,而實作的具體類別BreakfastShop如下:   在IBreakfastShop中…

[Design Pattern] 里氏替換原則 (Liskov Substitution Principle)

圖片
1. 里氏替換原則介紹1.1 里氏替換原則定義 根據書上的介紹,里氏替換原則的定義:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.。
比較一般的解釋是只要有父類別出現的地方,則子類別也能出現,且替換為子類別時,不會產生錯誤或異常。但反過來則不行,子類別出現的地方,父類別未必能使用。

1.2 里氏替換原則案例

圖片來源:http://timewarpgamer.com/features/box_art_disparity_nes.html  
  知名2D橫向射擊遊戲-洛克人,主角洛克人的武器除了使用自身的光束砲,還可以藉由打贏關卡BOSS,取得他們的能力作為自己的武器。所以,假設洛克人的武器之UML類別圖如下:




  武器(AbstractWeapon)的主要職責是射擊,削切剪刀 (Cut)是從剪刀人取得的能力、超級手臂(Guts)是從氣力人取得的能力、冰天箭(Ice)是從冰人取得的能力。
  在洛克人類別(Rockman)定義了一個方法killEnemy,使用武器來殺敵人,而具體要用什麼武器殺敵人,呼叫時才知道。AbstractWeapon抽象類別與介面的程式碼如下:
  削切剪刀、超級手臂和冰天箭的實現類別如下:

  洛克人的實現類別如下:   洛克人的武器是抽象的,當遇到敵人時,需藉由setWeapon來確認要使用哪種武器。模擬洛克人遇到敵人的程式碼和執行結果如下:

  以上述場景,洛克人使用剪刀人的削切剪刀做射擊,如果想用其他武器,則可呼叫setWeapon(new Guts())或setWeapon(new Ice())做武器替換。

1.3 里氏替換原則的含意以上述洛克人為例,當每個子類別(武器)可以替換掉父類別(AbstractWeapon),軟體功能不受影響,此父類別才能真正地被重複使用,且子類別也能增加新的方法和屬性。子類別的可替換性,代表不需修改父類別情況下做延展,成功實踐開閉原則。如果子類別不能…

[Design Pattern] 開閉原則 (The Open-Closed Principle)

圖片
1. 開閉原則介紹1.1 開閉原則定義 根據書上的介紹,開閉原則的定義:Software entities like classes, modules and functions should be open for extension but closed for modifications。意思是軟體實體像是類別、模組和函式,應該只對延展開放,但對修改關閉。此原則是設計模式六大原則的核心原則,其他五原則都是以此原則做具體化。

1.2 開閉原則案例 圖片來源:http://www.alphr.com/games/1006124/steam-sales   
  以世界知名的遊戲平台Steam為例,此平台的主要功能包含賣遊戲。假設Steam賣遊戲的UML類別圖如下:



  IGame介面定義了三個方法,遊戲名稱 (getName)、價錢(getPrice)和開發者(getDeveloper)。Steam類別是Steam平台。FPSGame是實作IGame的具體類別,是所有第一人稱射擊(FPS)遊戲的總稱。三種類別與介面的程式碼如下:
  IGame介面:   FPSGame類別:   Steam類別,並模擬賣出遊戲的紀錄:   執行Main函式的結果:

  以上模擬Steam賣遊戲的一般情況,但是Steam最有名的折扣活動是季節特賣,每款遊戲幾乎會有打折的優惠。假如原價450元以上的遊戲打8折,其他遊戲打6折,那麼要實現此打折的功能,該如何處理這樣的變化?有三種方法:

修改介面(錯誤):在IGame介面上新增getOffPrice()的方法,讓實作的具體類別都要實作此方法。但這樣修改的話,會造成FPSGame要修改、Steam類別也要修改。而IGame身為介面,應該是要穩定可靠的,不應該常有變化。因此這方法不考慮。修改實作的具體類(錯誤):在FPSGame的getPrice()中,將打折的功能實作在這函式,這是常見的處理方式。但是這方法還是有缺點,如果有想買FPS遊戲的人要看價格,透過已被修改的getPrice(),是打折後的價格,無法比較打折前後的差異,資訊會有落差。因此這方法不是最好的。透過延展實作變化(正確):將FPSGame的getPrice()改為virutal,並增加一個子類別OffFPSGame繼承FPSGame,覆寫getPrice()方法,而…