雖然不同的設計模式解決的問題各不相同,但從一個更高的抽象層次來看,它們通過相同的手段來實現相同的目的,用一句話總結:
它們增加了一層抽象將變化封裝起來,然後對抽象程式設計,並利用多型應對變化。
本文將以更抽象的視角剖析工廠模式、策略模式、模版方法模式,以及這些模式所遵循的設計原則。
工廠模式
1. 變化是什麼
對工廠模式來說,變化就是構建物件的方式,舉個例子:
public class PizzaStore{
public Pizza orderPizza(String type){
Pizza pizza ;
//構建具體pizza物件
if(type.equals("cheese")){
pizza = new CheesePizza();
}else if(type.equals("bacon")){
pizza = new BaconPizza();
}
//使用pizza物件
pizza.prepare();
pizza.bake();
return pizza ;
}
}
複製程式碼
抽象的反義詞是具體,對於工廠模式,具體就是用new
來構建物件。這樣做的後果是PizzaStore
不僅需要引入具體的Pizza
類,而且和構建Pizza
的細節耦合在一起。
如果PizzaStore
一輩子只做這兩種Pizza
,上面的程式碼就很好,不需要重構。但如果需要新增Pizza
型別,就不得不修改orderPizza()
,向其中增加if-else
。
2. 如何應對變化
通過將變化封裝在一層新的抽象中,實現了上層程式碼和變化的隔離,達到解耦的目的。對於工廠模式來說,新建的抽象叫做工廠,它將物件的構建和物件的使用分隔開,讓使用物件的程式碼不依賴於構建物件的細節。
解耦的好處是:“當變化發生時上層程式碼不需要改動”,這句話也可以表達成:“在不修改既有程式碼的情況下擴充套件功能”。這就是著名的 “開閉原則” 。
- “對修改關閉”的意思是:當需要為類擴充套件功能時,不要想著去修改類的既有程式碼,這是不允許的! 為啥不允許?因為既有程式碼是由數位程式設計師的努力,歷經了多個版本的迭代,好不容易才得到的正確程式碼。其中蘊含著博大精深的知識,和你不曾瞭解的細節,修改它一定會出bug的!
- “對擴充套件開放”的意思是:類的程式碼應該具備良好的抽象,使得擴充套件類的時候,不需要修改類的既有程式碼。
現實的問題來了,如果專案中的既有類不具備擴充套件性,甚至是牽一髮動全身的那種類。在一個時間較緊的迭代中需要往裡新增新功能,你會怎麼做?是違背“對修改關閉”,還是咬牙重構?(歡迎討論~~)
3. 三種封裝變化方式
- 簡單工廠模式
既然目的是消除orderPizza()
中構建具體pizza
的細節,那最直接的做法是,將他們提取出來放到另一個類中:
public class PizzaFactory{
public static Pizza createPizza(String type){
Pizza pizza ;
if(type.equals("cheese")){
pizza = new CheesePizza();
}else if(type.equals("bacon")){
pizza = new BaconPizza();
}
return pizza ;
}
}
複製程式碼
然後使用Pizza
的程式碼就變成:
public class PizzaStore{
public Pizza orderPizza(String type){
Pizza pizza = PizzaFactory.createPizza(type);
pizza.prepare();
pizza.bake();
return pizza ;
}
}
複製程式碼
等等,這和我們平時將一段常用程式碼抽離出來放到Util
類中有什麼區別嗎?
是的,沒有任何區別。從嚴格意義上說,這不是一個設計模式,更像是一種程式設計習慣。雖然只是程式碼搬家,但這種習慣的好處是:它隱藏了構建物件的細節,因為構建物件是經常會發生變化的,所以它還封裝了變化,最後它還可以被複用,比如選單類也需要構建 pizza 物件並獲取他們的價格。
使用靜態方法是這類封裝常用的技巧,它的好處是不需要新建工廠物件就可以實現呼叫,但缺點是不具備擴充套件性(靜態方法不能被重寫)。
- 工廠方法模式
簡單工廠模式中,工廠能夠構建幾種物件是在編譯之前就定義好的,如果想要新增另一種新物件,必須修改既有的工廠類。這不符合開閉原則。 所以簡單工廠模式對於新增物件型別這個場景來說顯得不夠有彈性。
有沒有辦法不修改既有類就新增物件型別?
工廠方法模式就可以做到,因為它採用了繼承:
//抽象pizza店
public abstract class PizzaStore{
public Pizza orderPizza(String type){
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
return pizza ;
}
//不同地區的pizza店可以推出地方特色的pizza
protected abstract Pizza createPizza(String type) ;
}
//A商店提供芝士和培根兩種pizza
public class PizzaStoreA extends PizzaStore{
@Override
protected Pizza createPizza(String type){
Pizza pizza ;
if(type.equals("cheese")){
pizza = new CheesePizza();
}else if(type.equals("bacon")){
pizza = new BaconPizza();
}
return pizza ;
}
}
複製程式碼
簡單工廠模式將構建物件的細節封裝在一個靜態方法中(靜態方法無法被繼承),而工廠方法模式將其封裝在一個抽象方法中,這樣子類可以通過重寫抽象方法新增 pizza。
現在是介紹另一個設計原則的絕佳時機,它就是 “依賴倒置原則” :上層元件不能依賴下層元件,並且它們都不能依賴具體,而應該依賴抽象。
上面的例子中PizzaStore
是上層元件,CheesePizza
是下層元件,如果直接在PizzaStore
中構建CheesePizza
就違反了依賴倒置原則,經過工廠模式的重構,PizzaStore
依賴於Pizza
這個抽象,同時CheesePizza
也依賴於這個抽象。所以違反依賴倒置會讓程式碼缺乏彈性,不易擴充套件。
Android 中RecyclerView.Adapter
就運用了工廠方法模式:
public abstract static class Adapter<VH extends ViewHolder> {
//封裝了各式各樣ViewHolder的構建細節,延遲實現構建細節到子類中
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
}
複製程式碼
- 抽象工廠模式
如果需要構建一組物件怎麼辦?
抽象工廠模式用來處理這種情況,它將構建一組物件的細節封裝在一個介面中:
//抽象原料工廠(原材料構建者)
public interface IngredientFactory{
void Flour createFlour() ;
void Sause createSause();
}
//原材料使用者
public class Pizza{
private Flour flour;
private Sause sause;
//使用組合持有構建者
private IngredientFactory factory;
//注入一個具體構建者
//同一種pizza,在不同地區可能會有不同口味
//那是因為雖然用的是同類原材料(抽象),當產地不同味道就不同(具體)
public Pizza(IngredientFactory factory){
this.factory = factory;
}
//使用具體工廠構建原材料(發生多型的地方)
public void prepare(){
flour = factory.createFlour();
sause = factory.createSause();
}
public void bake(){}
public void cut(){}
}
//具體工廠
public class FactoryA implements IngredientFactory{
public Flour createFlour(){
return new FlourA();
}
public Sause createSause(){
return new SauseA();
}
}
//構建pizza的時候傳入具體工廠
public class PizzaStoreA extends PizzaStore{
@Override
protected Pizza createPizza(String type){
Pizza pizza ;
FactoryA factory = new FactoryA();
if(type.equals("cheese")){
pizza = new CheesePizza(factory);
}else if(type.equals("bacon")){
pizza = new BaconPizza(factory);
}
return pizza ;
}
}
複製程式碼
如果地區B開了一家新pizza店,只需要新建FactoryB
並在其中定義地區B原材料的構建方式,然後傳入Pizza
類,整個過程不需要修改Pizza
基類。
抽象工廠模式 和 工廠方法模式 的區別在於:
- 前者適用於構建多個物件,並且使用組合。
- 後者適用於構建單個物件,並且使用繼承。
策略模式
1. 變化是什麼
對策略模式來說,變化就是一組行為,舉個例子:
public class Robot{
public void onStart(){
goWorkAt9Am();
}
public void onStop(){
goHomeAt9Pm();
}
}
複製程式碼
機器人每天早上9點工作。晚上9點回家。公司推出了兩款新產品,一款早上8點開始工作,9點回家。另一款早上9點工作,10點回家。
面對這樣的行為變化,繼承是可以解決問題的,不過你需要新建兩個Robot
的子類,過載一個子類的onStart()
,過載另一個子類的onStop()
。如果每次行為變更都通過繼承來解決,那子類的數量就會越來越多(膨脹的子類)。更重要的是,新增增子類是在編譯時新增行為, 有沒有辦法可以在執行時動態的修改行為?
2. 如何應對變化
通過將變化的行為封裝在介面中,就可以實現動態修改:
//抽象行為
public interface Action{
void doOnStart();
void doOnStop();
}
public class Robot{
//使用組合持有抽象行為
private Action action;
//動態改變行為
public void setAction(Action action){
this.action = action;
}
public void onStart(){
if(action!=null){
action.doOnStart();
}
}
public void onStop(){
if(action!=null){
action.doOnStop();
}
}
}
//具體行為1
public class Action1 implements Action{
public void doOnStart(){
goWorkAt8Am();
}
public void doOnStop(){
goHomeAt9Pm();
}
}
//具體行為2
public class Action2 implements Action{
public void doOnStart(){
goWorkAt9Am();
}
public void doOnStop(){
goHomeAt10Pm();
}
}
//將具體行為注入行為使用者(執行時動態改變)
public class Company{
public static void main(String[] args){
Robot robot1 = new Robot();
robot1.setAction(robot1);
Robot robot2 = new Robot();
robot2.setAction(robot2);
}
}
複製程式碼
策略模式將具體的行為和行為的使用者隔離,這樣的好處是,當行為發生變化時,行為的使用者不需要變動。
Android 中的各種監聽器都採用了策略模式,比如:
public class RecyclerView{
//使用組合持有抽象滾動行為
private List<OnScrollListener> mScrollListeners;
//抽象滾動行為
public abstract static class OnScrollListener {
public void onScrollStateChanged(RecyclerView recyclerView, int newState){}
public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
}
//動態修改滾動行為
public void addOnScrollListener(OnScrollListener listener) {
if (mScrollListeners == null) {
mScrollListeners = new ArrayList<>();
}
mScrollListeners.add(listener);
}
//使用滾動行為
void dispatchOnScrollStateChanged(int state) {
if (mLayout != null) {
mLayout.onScrollStateChanged(state);
}
onScrollStateChanged(state);
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(this, state);
}
if (mScrollListeners != null) {
for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
mScrollListeners.get(i).onScrollStateChanged(this, state);
}
}
}
}
複製程式碼
列表滾動後的行為各不相同,所以使用抽象類將其封裝起來(其實和介面是一樣的)。
模版方法模式
1. 變化是什麼
從嚴格意義上講,模版方法模式並不能套用開篇的那句話:“它們增加了一層抽象將變化封裝起來,然後對抽象程式設計,並利用多型應對變化”。因為如果這樣說,就是在強調目的是 “應對變化” 。但模版方法的目的更像是 “複用演算法”,雖然它也有應對變化的成分。
對模版方法模式來說,變化就是演算法的某個步驟,舉個例子:
public class View{
public void draw(Canvas canvas) {
...
// skip step 2 & 5 if possible (common case)
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we’re done...
return;
}
...
}
protected void dispatchDraw(Canvas canvas) {
}
}
複製程式碼
節選了View.draw()
方法中的某一片段,從註釋中可以看出draw()
定義了一個繪圖的演算法框架,一共有七個步驟,所有步驟都被抽象成一個方法,其中的變化在於,每個步驟對於不同型別的View
都可能是不同的。所以為了讓不同View
複用這套演算法框架,就把它定義在了父類中,子類可以通過重寫某一個步驟來定義不同的行為。
模版方法模式一種常用的重構方法,它將子類的共用邏輯抽象到父類中,並將子類特有子邏輯設計成抽象方法供子類重寫。
2. 對比
- 模版方法模式 vs 工廠方法模式
從程式碼層面看,模版方法的實現方式和工廠方法模式幾乎一樣,都是通過子類重寫父類的方法。唯一的不同是,工廠方法模式父類中的方法必須是抽象的,也就是說強制子類實現,因為子類不實現父類就無法工作。而模版方法模式父類中的方法可以是不抽象的,也就是說子類可以不實現,父類照樣能工作。這種在父類中空實現的方法有一個專門的名字叫 “hook(鉤子)” ,鉤子的存在,可以讓子類有能力對演算法的流程進行控制,比如ViewGroup.onInterceptTouchEvent()
。
- 模版方法模式 vs 策略模式
從產出來看,模版方法模式和策略模式是一個陣營的,因為他們產出的都是一組行為(演算法),而工廠模式產出的是一個物件。但它們倆對演算法的控制力不同,策略模式可以輕鬆的替換掉整個演算法,而模版方法模式只能替換掉演算法中的某個步驟。從程式碼層面來看,它們的實現方式也不同,策略模式使用組合,而模版方法模式使用繼承。組合比繼承更具有彈性,因為它可以在執行時動態的替換行為。
後續
這個系列會持續分享對其他設計模式的理解。
《Head First》是一本讓我對設計模式理解升級的書,強烈推薦(本篇中工廠模式的例子摘自其中)。