真正開發中使用最頻繁的模式基本就是【策略】和【工廠】這個兩個模式。
按照"國際慣例"先引入些模式的概念和範例。(範例參考Head First,但是力求比它講的簡潔且清晰)
之後在詳細講解優惠券的設計和模式應用。
所有物件導向入門的時候都是以人、動物為範例。講解什麼是【繼承】等相關概念。這個是符合直覺的。
但是在實際應用中,繼承用到的地方有限,它有它的問題,它是一種【強耦合】方式,一般使用【策略模式】【裝飾模式】代替繼承。
以鴨子動物設計為例,講解繼承方式存在哪些問題:
所有鴨子都有quack和swim能力,所以超類實現這兩個功能。
display是抽象方法,每個子類鴨子自己負責實現自己的display功能。
這樣很好的使用了父類別繼承能【複用】的特性。
(符合直覺的第一想法,而且還是物件導向學習的不錯的情況)
有些功能很好界定,有些功能很「尷尬」,例如fly功能。
fly不能加在超類上,因為不是所有鴨子都有fly功能。
如果加在超類上就導致所有的子類都要實現或者繼承這個可能不適用的方法。
而且也不是所有鴨子都會quack(例如木頭玩具鴨子),那些沒有quack的鴨子,同樣要實現或繼承quack。
想利用繼承來達到程式碼複用的目的有以下問題:
設計升級:
通過介面的形式,讓「某些」(而非全部)鴨子型別可飛可叫。
誰有需要誰就去實現相應的介面。
例如:你可以飛你就實現flyable介面,你不能飛,你就什麼都不做。
通過介面的形式解決了部分問題,因為不是所有子類鴨子都具有fly,和quack行為。沒必要繼承或實現自己不適用的功能。
但是程式碼無法【複用】的問題還是存在。
我們每個子類中都維護了display,quack功能,可能很多子類的功能都是一致的,沒有複用起來,修改一類相同行為,要每個類去找,逐個修改。
同時這些程式碼都散在每個實現類中,不知道全部的行為。
設計思路與原則:
軟體專案唯一的共性:【需求不斷變化】
我們要做的就是【識別變化】【隔離變化】,每次迭代或者需求變化的時候,修改範圍可控,模組之間【鬆耦合】。
主要最好不要動到那些成熟的已經經過測試和生產驗證的程式碼,儘量遵循【開閉原則】。
是否進行隔離有個【單一職責】原則判斷,如果兩個模組修改的原因是不同的,彼此的修改不一定牽涉到對方的修改。那他們應該隔離。
所謂隔離即代表,他們程式碼在不同方法中、或在不同類中、或者不同服務模組中、甚至是不同系統中。
範例中,每個鴨子的fly和quack會隨著鴨子的不同而不同。我們建立兩組類,一組和fly相關,一組和quack相關。
fly類裡面有各種fly的實現方式。例如:用翅膀飛是一個實現類。用火箭飛是另外一個實現類。
這樣對於使用翅膀飛的一類鴨子,我想辦法把相應的fly類給到它,就實現了fly方法的【複用】和【集中管理】
下面我們要解決的就是如何將這個用翅膀飛的實現類「給到」這個具體的鴨子類。
插播一條概念:
【針對介面程式設計】
什麼是介面?
介面就是約定好的規範、口令、圖紙。
就好比,各個地方的人,都聽得懂「滾」這個語言介面命令,也有相應的實現。 大家雖然各不相同、想法各異、體能差異。
但是聽到你跟他說「滾」,大家都會執行邁腿這個動作,根據人種不同,有的地方人可能邁腿上步揍你,有的地方的人是邁腿跑路。
這種不同人種的不同反應方式,我們稱為【多型】。
雖然語言介面相同,都是一個「滾」的語音輸入。但是具體實現類不同,反應也是不同的。
例如:電腦主機板上有很多介面,這些介面是有明文規定,例如電壓、時序、通訊協定、功能等的。
這些就是規範。你按照這個規範走,就能拿到規範定義的結果和返回。
不同的記憶體廠商都有自己的記憶體條。他們的記憶體晶片、板子方案都是不同的,但是他們的插槽是相同的,他們都是實現了記憶體介面規範。
電腦只要按照記憶體介面規範,發出同樣的指令。任何廠商的記憶體條都能進行儲存操作。
以前經常聽說一句話,一流公司定規範,二流公司做產品。
其實規範就是介面,大公司定義實現方案和方案要實現的介面,其他公司根據自己的原材料實現這些介面,這個產品就落地了。
所謂【要針對介面程式設計,不要針對實現程式設計】
你學習如何讓一個人滾,一定要學習普通話,因為大多數地方的人都能聽懂,只不過反應不同。
如果你針對某個特定的人群學習,那你這個技能就限定在少數人上,例如閩南語只有福建那塊的人能聽懂。
再比如,你這個電腦主機板記憶體介面是針對三星獨家的開發的,指令也只有三星認識,其他品牌的記憶體條甚至都插不上去。
這樣的主機板誰會買,綁死在三星上,他說漲價你就要掏錢。不然整個電腦都不能執行。
針對介面實現的板子。我可以換同樣介面的國產便宜的記憶體。還是那句「又不是不能用,李姐萬歲」。
解釋完概念,我們看程式設計上如何應用。
我們以一個人的一天活動為例子。
class PersonDayAct{ DayAct act = new 碼農(); act.dayAct(); act.nightAct(); }
act.dayAct();
act.nightAct();
我們都用的介面方法,都是使用介面在程式設計。好處是如果我們想列印富二代的一天。
DayAct act = new 富二代(); 只需要修改這一行程式碼即可。
通過多型,我們就能列印富二代的一天活動。
而且這個new操作,我們能通過稍後的工廠模式代替。如果以後要列印其他人的一天活動。
我們只要新建新的實現類即可。不需要改動以前寫好的經過測試的程式碼。符合【開閉原則】
講完【面向介面程式設計】,我們繼續講如何完善鴨子範例。
替代繼承的方式就是【組合】,多用組合,少用繼承。
「有一個」比「是一個」更好,每一個鴨子都有一個FlyBehavior和一個QuackBehavior,好將飛行和咕咕叫委託給他們處理。
鴨子的行為不是繼承來的,而是和「適當」的物件「組合」而來。
組合的好處:
1.將一類行為封裝成類
2.執行時動態改變行為。
public abstract class Duck{ FlyBehavior flyBehavior; QuackBehavior quackBehavior; public Duck(){ } public abstract void dispaly(); public void performQuack(){ quackBehavior.quack(); } public void performFly(){ flyBehavior.fly(); } public void swim(){ System.out.println("all ducks float,even decoys"); } }
public class Bduck extends Duck{ public Bduck(){ quackBehavior = new Quack(); flyBehavior = new FlyWithWings(); } public void setFlyBehavior( FlyBehavior fb){ flyBehavior = fb; } public void setQuackBehavior( QuackBehavior qb){ quackBehavior = qb; } public void display(){ System.out.println("i am Bduck"); } }
public class Test{ public static void main(String[] args){ Duck d = new Bduck(); d.performFly(); d.setFlyBehavior(new FlyRocketPowered()); d.performFly(); } }
總結:
策略模式:定義演演算法族,分別封裝起來,讓他們之間可以互相替換,此模式讓演演算法的變化獨立於使用演演算法的客戶。
解釋:範例中鴨子的飛行就有不同的策略,有的用翅膀飛,有的用火箭飛。
不同的人對於「滾」這個指令也有自己不同的應對策略,有的是跑,有的是上前揍你。
而這些策略是可以【複用】和【統一管理】的。我們通過【組合】的方式,將策略「放入」到類中,執行時可以更換不同策略。
而不是通過繼承來獲得這個行為。組合比繼承更加靈活,和方便。
但是策略模式還留下了一個問題就是,如何「放入」這個策略物件到類中,如果是new物件的形式,這個就和new的那個策略繫結死了。
我們希望的是,在程式執行過程中,通過輸入引數的不同,動態組合不同的實現類。從而實現不同的行為。
例如:我們通過優惠券的型別欄位獲取不同的優惠券實現類。有的是滿減,有的是折扣,但是程式不關心這些型別。
他只要將價格計算委託到不同的策略上計算出最終價格即可。
簡單工廠模式:
工廠的職責就是新建產品。
以下單匹薩為例。pizza介面定義了pizza的製作方法。不同種類的pizza負責各自的實現,不同pizza有的烤的時間長,有的切的塊小。
以下是典型的面向介面程式設計,甚至還有點策略模式的味道。
Pizza orderPizza(String type){ Pizza pizza; if(type.equals("cheese")){ pizza = new CheesePizza(); }else if(type.equals("greek")){ pizza = new GreekPizza(); }else if(type.equals("pepperoni")){ pizza = new PepperoniPizza(); } pizza.prepare(); pizza.babke(); pizza.cut(); pizza.box(); return pizza; } |
唯一的問題是,如果我pizza的種類有了增刪,我需要修改if-else這塊程式碼。這個就違反了【開閉原則】
我們應該將變化的地方【隔離變化】。
簡單工廠:
public class PizzaStore{ SimplePizzaFactory factory; public PizzaStore(SimplePizzaFactory factory){ this.factory = factory; } Pizza orderPizza(String type){ Pizza pizza = factory.createPizza(type); pizza.prepare(); pizza.babke(); pizza.cut(); pizza.box(); return pizza; } }
|
public class SimplePizzaFactory{ public Pizza createPizza(String type){ Pizza pizza; if(type.equals("cheese")){ pizza = new CheesePizza(); }else if(type.equals("greek")){ pizza = new GreekPizza(); }else if(type.equals("pepperoni")){ pizza = new PepperoniPizza(); } return pizza; } } |
simplePizzaFactory就幹一件事,就是新建比薩。
對於需要單例的我們可以選用單例模式:
1.單例模式的餓漢式[可用] public class Singleton { private static Singleton instance=new Singleton(); private Singleton(){}; public static Singleton getInstance(){ return instance; } } 存取方式 Singleton instance = Singleton.getInstance(); 2.單例模式懶漢式雙重校驗鎖[推薦用] class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } } 存取方式 Singleton instance = Singleton.getInstance(); 3.內部類[推薦用] public class Singleton{ private Singleton() {}; private static class SingletonHolder{ private static Singleton instance=new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; } } 存取方式 Singleton instance = Singleton.getInstance(); 需要範例化時,呼叫getInstance方法,才會裝載SingletonHolder類,從而完成Singleton的範例化。 4.列舉形式 public enum Singleton { INSTANCE; public void doSomething() { System.out.println("doSomething"); } } 呼叫方法: public class Main { public static void main(String[] args) { Singleton.INSTANCE.doSomething(); } } 直接通過Singleton.INSTANCE.doSomething()的方式呼叫即可。方便、簡潔又安全。 懶漢式單例
工廠封裝的好處:
工廠封裝的缺點:
為了遵守【開閉原則】,有兩種方式:升級簡單工廠、工廠方法模式。
升級簡單工廠:
工廠也可以是一個介面或者抽象類,我們工廠也可能有很多種實現方式。
我們先實現了一種AStyleSimplePizzaFactory,如果後續需求變更,pizza種類有新增,我們可以在新建一個BStyleSimplePizzaFactory。
你可以認為這是一種分類方式。例如在中國,豆腐腦廠家。南方和北方都是生產豆腐腦,但是一個甜口一個鹹口。
pizza店可以按照風味分類:
|
交通工具也可以通過型別分類: |
其實你也可以不按照這個分類。 but,但是。。。。 個人以為: 老法師都是想著簡潔高效,新手才想著一定要高階有逼格。 |
public interface Moveable { void run(); } public class Car implements Moveable{ @Override public void run() { System.out.println("driving....."); } } public class Plane implements Moveable{ @Override public void run() { System.out.println("flying..."); } } //交通工具工廠 public abstract class VehicleFactory { //具體生成什麼交通工具由子類決定,這裡是抽象的。 public abstract Moveable create(); } //Car工廠類 public class CarFactory extends VehicleFactory{ @Override public Moveable create() { //單例、多例、條件檢查自己控制 return new Car(); } } //飛機工廠類 public class PlaneFactory extends VehicleFactory { @Override public Moveable create() { //單例、多例、條件檢查自己控制 return new Plane(); } } public class Test{ public static void main(String[] args){ VehicleFactory factory = new PlaneFactory(); Moveable m = factory.create(); m.run(); //換成Car工廠 factory = new CarFactory(); m = factory.create(); m.run(); } } 交通工具工廠 |
工廠方法模式:
|
public abstract class PizzaStore{ public Pizza orderPizza(String type){ Pizza pizza; pizza = createPizza(type); pizza.prepare(); pziza.bake(); pizza.cut(); pizza.box(); return pizza; } abstract Pizza createPizza(String type); } public class AStylePizzaStore extends PizzaStore{ public Pizza createPizza(String type){ if(type.equals("chesse")){ pizza = new AStyleChessePizza(); }else if(type.equals("peperoni")){ pizza = new AStylePepperoniPizza(); } } }
PizzaStore store = new AStylePizzaStore();
store.orderPizza("cheese");
|
工廠方法模式:
|
工廠方法範例:
|
工廠方法好處:
1.將很多方法和流程固化在父類別中,有利於標準化操作,將產品的實現和使用【解耦】。
2.當我們新增產品的時候,或者產品有其他風格和實現時,我們能根據【開閉原則】,新加新的子類即可。
3.工廠方法可以不是抽象的,相當於給了一個預設的實現方式。
工廠方法的缺點:
1.隨著業務增長,可能子類越來越多,難於管理(有抽象工廠管理)。
2.無論是簡單工廠升級版,還是工廠方法。我們很多時候升級不是非黑即白,用新工廠代替舊工廠那麼簡單,或者新工廠就舊工廠各管各的,而是兩個工廠同時存在。
例如:我原來要做甜豆花,現在有要做鹹豆花,但是主體業務邏輯不動。如果是新加一個子類。
我們如何動態的指定工廠呢?在搞一個工廠的工廠嗎?突然感覺簡單工廠YYDS了。
其實我們還是要分清,這個新的產品新增,是原來的業務邏輯不動,還是原來的業務邏輯程式碼需要變動。
如果原來的主邏輯程式碼不動,我們應該需要修改if-else的,因為本質是引數有增加。
如果是拓展的,我們應該是要新建子類,然後拓展新加的程式碼使用新加的子類。
至於什麼時候用介面,什麼時候用抽象類:
假如這個概念在我們腦子是確確實實存在的,就用抽象類。或者你有可複用的方法希望子類繼承直接用。
假如這個概念只是某些方面的特性:比如會飛的,會跑的,就用介面
假如兩個概念模糊的時候,不知道選擇哪個的時候,就用介面,原因是java是單繼承,多介面實現,這個繼承能力很寶貴,從實現了這個介面後,還能從其它的抽象類繼承,更靈活。
抽象工廠:
為了控制工廠子類的數量。不必給每一個產品分配一個工廠類。可以將產品分組,每組中的不同產品有同一個工廠類的不同方法來建立。
這個和簡單工廠的升級版本很像。但是注意抽象工廠是一個工廠生成不同的東西。是按照系列生產。
我們裝備美式裝備,裡面是含有手槍、大炮等一系列的。
我們裝備德式裝備,裡面又是一套手槍、大炮、汽車等。
//交通工具 public abstract class Vehicle { //實現由子類決定 public abstract void run(); } //食物 public abstract class Food { public abstract void printName(); } //武器 public abstract class Weapon { // public abstract void shoot(); }
//抽象工廠 public abstract class AbstractFactory { //生產 交通工具 public abstract Vehicle createVehicle(); //生產 武器 public abstract Weapon createWeapon(); //生產食物 public abstract Food createFood(); } //哈利波特的魔法工廠 public class MagicFactory extends AbstractFactory { //交通工具:掃把 public Vehicle createVehicle(){ return new Broom(); } //武器:魔法棒 public Weapon createWeapon(){ return new MagicStick(); } //食物:毒蘑菇 public Food createFood(){ return new MushRoom(); } } //預設的工廠 public class DefaultFactory extends AbstractFactory{ @Override public Food createFood() { return new Apple(); } @Override public Vehicle createVehicle() { return new Car(); } @Override public Weapon createWeapon() { return new AK47(); } }
public class Car extends Vehicle{ @Override public void run() { System.out.println("冒著煙奔跑中..."); } } //掃帚 public class Broom extends Vehicle{ @Override public void run() { System.out.println("掃帚搖著尾巴呼呼呼..."); } } //食物:毒蘑菇 public class MushRoom extends Food { @Override public void printName() { System.out.println("mushroom"); } } public class Apple extends Food { @Override public void printName() { System.out.println("apple"); } } public class AK47 extends Weapon{ public void shoot(){ System.out.println("噠噠噠...."); } } //武器:魔法棒 public class MagicStick extends Weapon { @Override public void shoot() { System.out.println("fire hu hu hu ..."); } }
//換一個工廠,只需要改動這一處,就可以了,換一個工廠,就把生產的系列產品都換了 AbstractFactory factory = new DefaultFactory(); //new DefaultFactory(); //換一個工廠 Vehicle vehicle = factory.createVehicle(); vehicle.run(); Weapon weapon = factory.createWeapon(); weapon.shoot(); Food food = factory.createFood(); food.printName();
抽象工廠類圖:
抽象工廠允許客戶使用抽象介面來建立一組相關的產品,而不需要關心實際產出的具體產品是什麼。
這樣客戶從具體的產品中【解耦】
抽象工廠的createProductA這種方法看起來很像工廠方法。父類別定義,子類實現。
總結:
簡單工廠:唯一工廠類,一個產品抽象類,工廠類的建立方法依據入參判斷並建立具體產品物件。
工廠方法:多個工廠類,一個產品抽象類,利用多型建立不同的產品物件,避免了大量的if-else判斷。
抽象工廠:多個工廠類,多個產品抽象類,產品子類分組,同一個工廠實現類建立同組中的不同產品,減少了工廠子類的數量。
實際應用舉例:
策略和工廠應用的範圍實在太頻繁了,不用特別舉例子。
以優惠券為例。
優惠券分型別:滿減券、折扣券、等等。這些券型別就是決定了算價格的時候如何核銷。這就是一個策略。和不同的鴨子怎麼飛是一樣道理。
同樣優惠券還有適用範圍。到底適用於那些商品、門店、等等。
優惠券有很多投放,這個投放可能在很多渠道和活動是共用的。例如:A券就投放100張,在主頁活動中心、線下掃碼同時領取。領完為止。
思路:
優惠券最主要的:優惠方式及計算、有效期方式及計算、適用範圍及計算。
將優惠打折方式作為一種策略。組合到優惠券的屬性中。就如同鴨子組合了一個飛行的策略。
同理優惠券有效期計算,有的是立即生效,有的是固定時間生效等。
優惠券適用範圍目前只有預設方式。
通過簡單引數化工廠:
通過券型別code來獲取不同打折優惠策略範例,
通過券validity_type獲取不同有效期計算的策略範例。
適用範圍,目前只有預設計算方式。無須引數化工廠。
氣氛都哄到這了,就順道講下剩下的兩種建立型模式:原型模式、建造者模式。
原型模式:
|
public abstract class Shape implements Cloneable { private String id; protected String type; abstract void draw(); public String getType(){ return type; } public String getId() { return id; } public void setId(String id) { this.id = id; } public Object clone() { Object clone = null; try { // 淺拷貝 clone = super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } } public class Rectangle extends Shape { public Rectangle(){ type = "Rectangle"; } @Override public void draw() { System.out.println("Inside Rectangle::draw() method."); } } public class Square extends Shape { public Square(){ type = "Square"; } @Override public void draw() { System.out.println("Inside Square::draw() method."); } } public class Circle extends Shape { public Circle(){ type = "Circle"; } @Override public void draw() { System.out.println("Inside Circle::draw() method."); } } public class ShapeCache { private static Hashtable<String, Shape> shapeMap = new Hashtable<String, Shape>(); public static Shape getShape(String shapeId) { Shape cachedShape = shapeMap.get(shapeId); return (Shape) cachedShape.clone(); } // 對每種形狀都執行資料庫查詢,並建立該形狀 // shapeMap.put(shapeKey, shape); // 例如,我們要新增三種形狀 public static void loadCache() { Circle circle = new Circle(); circle.setId("1"); shapeMap.put(circle.getId(),circle); Square square = new Square(); square.setId("2"); shapeMap.put(square.getId(),square); Rectangle rectangle = new Rectangle(); rectangle.setId("3"); shapeMap.put(rectangle.getId(),rectangle); } } public class PrototypePatternDemo { public static void main(String[] args) { ShapeCache.loadCache(); Shape clonedShape = (Shape) ShapeCache.getShape("1"); System.out.println("Shape : " + clonedShape.getType()); Shape clonedShape2 = (Shape) ShapeCache.getShape("2"); System.out.println("Shape : " + clonedShape2.getType()); Shape clonedShape3 = (Shape) ShapeCache.getShape("3"); System.out.println("Shape : " + clonedShape3.getType()); } }
|
原型模式,顧名思義,給你個原型,你根據原型能獲得大量相同或相似的物件,該步驟通過克隆物件完成。
對於高淨值,建立過程極其複雜的物件,可以使用這種模式大量建造,不用重新new,那樣效率太差。
(1)淺克隆
在淺克隆中,如果原型物件的成員亦量是8大基本資料型別(byte、short、int、long、float、double、char、boolean、除這8種,全部是參照型別,尤其String 底層是字元陣列,不是基本資料型別)將複製一份給克降物件,如果原型物件的成員變數是參照型別(如類、介面、陣列等複雜資料型別),則將參照物件的地址複製一份給克降物件,也就是說,原型物件和克隆物件的成員變數指向相同的記憶體地址。簡單來說,在淺克隆中,當原型物件被複制時,只複製它本身和其中包含的值型別的成員變數,而參照型別的成員變數並沒有複製。
範例:
org.springframework.beans.BeanUtils.copyProperties(source,target);
(2)深克隆
在深克隆中,無論原型物件的成員變數是值型別還是參照型別,都將複製一份給克隆物件,深克隆將原型物件的所有參照物件也複製一份給克隆物件。簡單來說,在深克隆中,除了物件本身被複制外,物件所包含的所有成員變數也將被複制。
範例:
org.apache.commons.lang3.SerializationUtils.clone(source);
建造者模式:
|
class Product { private String partA; private String partB; private String partC; public void setPartA(String partA) { this.partA = partA; } public void setPartB(String partB) { this.partB = partB; } public void setPartC(String partC) { this.partC = partC; } public void show() { //顯示產品的特性 } } abstract class Builder { //建立產品物件 protected Product product = new Product(); public abstract void buildPartA(); public abstract void buildPartB(); public abstract void buildPartC(); //返回產品物件 public Product getResult() { return product; } } public class ConcreteBuilder extends Builder { public void buildPartA() { product.setPartA("建造 PartA"); } public void buildPartB() { product.setPartB("建造 PartB"); } public void buildPartC() { product.setPartC("建造 PartC"); } } class Director { private Builder builder; public Director(Builder builder) { this.builder = builder; } //產品構建與組裝方法 public Product construct() { builder.buildPartA(); builder.buildPartB(); builder.buildPartC(); return builder.getResult(); } } public class Client { public static void main(String[] args) { Builder builder = new ConcreteBuilder(); Director director = new Director(builder); Product product = director.construct(); product.show(); } }
|
建造者模式,主要針對物件建造過程複雜,一般由很多子部件按一定步驟組合而成。產品的組成部分是不變的,但是每部分都是可以靈活選擇的。
例如:我們攢電腦的時候,都是將各種部件的要求告訴組裝店,電腦組成就那些,但是硬碟,cpu可以有很多種,他幫我們組裝好電腦(然後就被坑了。。。。)
本文來自部落格園,作者:wanglifeng,轉載請註明原文連結:https://www.cnblogs.com/wanglifeng717/p/16339222.html