這是設計模式系列的第一篇,系列文章目錄以下:程序員
雖然不一樣的設計模式解決的問題各不相同,但從一個更高的抽象層次來看,它們經過相同的手段來實現相同的目的。本文將以更抽象的視角剖析工廠模式、策略模式、模版方法模式,以及這些模式所遵循的設計原則。先用一句話總結它們的共同點:bash
它們增長了一層抽象將變化封裝起來,而後對抽象編程,並利用多態應對變化。框架
應用這些設計模式的項目實戰能夠移步下面的連接:ide
對工廠模式來講,變化就是構建對象的方式,舉個例子:post
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
的細節耦合在一塊兒。ui
若是PizzaStore
一生只作這兩種Pizza
,上面的代碼就很好,不須要重構。但若是須要新增Pizza
類型,就不得不修改orderPizza()
,向其中增長if-else
。
經過將變化封裝在一層新的抽象中,實現了上層代碼和變化的隔離,達到解耦的目的。對於工廠模式來講,新建的抽象叫作工廠,它將對象的構建和對象的使用分隔開,讓使用對象的代碼不依賴於構建對象的細節。
解耦的好處是:「當變化發生時上層代碼不須要改動」,這句話也能夠表達成:「在不修改既有代碼的狀況下擴展功能」。這就是著名的 「開閉原則」 。
現實的問題來了,若是項目中的既有類不具有擴展性,甚至是牽一髮動全身的那種類。在一個時間較緊的迭代中須要往裏添加新功能,你會怎麼作?是違背「對修改關閉」,仍是咬牙重構?(歡迎討論~~)
既然目的是消除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
基類。
抽象工廠模式 和 工廠方法模式 的區別在於:
對策略模式來講,變化就是一組行爲,舉個例子:
public class Robot{
public void onStart(){
goWorkAt9Am();
}
public void onStop(){
goHomeAt9Pm();
}
}
複製代碼
機器人天天早上9點工做。晚上9點回家。公司推出了兩款新產品,一款早上8點開始工做,9點回家。另外一款早上9點工做,10點回家。
面對這樣的行爲變化,繼承是能夠解決問題的,不過你須要新建兩個Robot
的子類,重載一個子類的onStart()
,重載另外一個子類的onStop()
。若是每次行爲變動都經過繼承來解決,那子類的數量就會愈來愈多(膨脹的子類)。更重要的是,添加增子類是在編譯時新增行爲, 有沒有辦法能夠在運行時動態的修改行爲?
經過將變化的行爲封裝在接口中,就能夠實現動態修改:
//抽象行爲
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(new Action1());
robot1.setAction(new Action2());
}
}
複製代碼
策略模式將具體的行爲和行爲的使用者隔離,這樣的好處是,當行爲發生變化時,行爲的使用者不須要變更。
Android 中的各類監聽器都採用了策略模式,好比View.setOnClickListener()
,但下面這個更偏向於觀察者模式,他們的區別是策略模式的意圖在於動態替換行爲,當第二次調用setOnClickListener()
時,以前的行爲被替換,而觀察者模式是動態添加觀察者:
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);
}
}
}
}
複製代碼
列表滾動後的行爲各不相同,因此使用抽象類將其封裝起來(其實和接口是同樣的)。
策略模式的實戰應用能夠點擊這裏
從嚴格意義上講,模版方法模式並不能套用開篇的那句話:「它們增長了一層抽象將變化封裝起來,而後對抽象編程,並利用多態應對變化」。由於若是這樣說,就是在強調目的是 「應對變化」 。但模版方法的目的更像是 「複用算法」,雖然它也有應對變化的成分。
對模版方法模式來講,變化就是算法的某個步驟,舉個例子:
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
複用這套算法框架,就把它定義在了父類中,子類能夠經過重寫某一個步驟來定義不一樣的行爲。
模版方法模式一種經常使用的重構方法,它將子類的共用邏輯抽象到父類中,並將子類特有子邏輯設計成抽象方法供子類重寫。
從代碼層面看,模版方法的實現方式和工廠方法模式幾乎同樣,都是經過子類重寫父類的方法。惟一的不一樣是,工廠方法模式父類中的方法必須是抽象的,也就是說強制子類實現,由於子類不實現父類就沒法工做。而模版方法模式父類中的方法能夠是不抽象的,也就是說子類能夠不實現,父類照樣能工做。這種在父類中空實現的方法有一個專門的名字叫 「hook(鉤子)」 ,鉤子的存在,可讓子類有能力對算法的流程進行控制,好比ViewGroup.onInterceptTouchEvent()
。
從產出來看,模版方法模式和策略模式是一個陣營的,由於他們產出的都是一組行爲(算法),而工廠模式產出的是一個對象。但它們倆對算法的控制力不一樣,策略模式能夠輕鬆的替換掉整個算法,而模版方法模式只能替換掉算法中的某個步驟。從代碼層面來看,它們的實現方式也不一樣,策略模式使用組合,而模版方法模式使用繼承。組合比繼承更具備彈性,由於它能夠在運行時動態的替換行爲。
這個系列會持續分享對其餘設計模式的理解。
《Head First》是一本讓我對設計模式理解升級的書,強烈推薦(本篇中工廠模式的例子摘自其中)。