新增一個OO設計原則:
要依賴抽象,不要依賴具體類
1 簡單工廠
- 針對接口編程,可以隔離掉以後系統可能發生的一大堆改變。爲什麼呢?如果代碼是針對接口而寫,那麼通過多態,它可以與任何新類實現該接口。但是,當代碼使用大量的具體類時,等於是自找麻煩,因爲一旦加入新的具體類,就必須改變代碼。也就是說,你的代碼並非“對修改關閉”。想用新的具體類型來擴展代碼,必須重新打開它。所以,當遇到這樣的問題時,就應該回到OO設計原則去尋找線索。我們的第一個設計原則用來處理改變,並幫助我們“找出會變化的方面,把它們從不改變的部分分離出來“。
如下面代碼示例暴露出的問題
/**
* @author hgl
* @data 2018年5月6日
* @description pizza抽象類
*/
public abstract class Pizza {
public String name;
//所有的比薩都需要做一些準備,如擀麪皮,醬汁,加上配料(如芝士等)
/**
* 麪糰
*/
public String dough;
/**
* 醬汁
*/
public String sauce;
/**
* 配料
*/
public List toppings = new ArrayList();
public void prepare(){
System.out.println("Preparing "+name);
System.out.println("Tossing dough...");
System.out.println("Adding sauce...");
System.out.println("Adding toppings:");
for (int i = 0; i < toppings.size(); i++) {
System.out.println(" "+toppings.get(i));
}
}
//所有的比薩都會有這些動作
/**
* void
* description:烤的動作
*/
public void bake(){
System.out.println("Bake for 25 minutes at 350");
}
/**
* void
* description:切的動作
*/
public void cut(){
System.out.println("Cutting the pizza into diagonal slices");
}
/**
* void
* description:包裝的動作
*/
public void box(){
System.out.println("Place piaaz in official PizzaStore box");
}
/**
* String
* @return
* description:返回這個pizza的名字
*/
public String getName(){
return name;
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 奶酪比薩
*/
public class Cheese extends Pizza {
public Cheese(){
name = "Cheese Pizza";
dough = "Thin Crust Dough";
toppings.add("cheese");
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 蛤蜊比薩
*/
public class Clam extends Pizza {
public Clam(){
name = "Clam Pizza";
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 希臘比薩
*/
public class Greek extends Pizza{
public Greek(){
name = "Greek Pizza";
dough = "Thin Crust Dough";
sauce = "Greek sauce";
toppings.add("unique toppings");
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 比薩店
*/
public class PizzaStore {
public Pizza orderPizza(String type){
Pizza pizza;
/*
if判斷中的部分是變化的部分。隨着時間過去,比薩菜單
會改變,比如加入新的比薩。這裏就必須一改再改。
*/
if(type.equals("cheese")){
pizza = new Cheese();
}else if(type.equals("greek")){
pizza = new Greek();
}else if(type.equals("clam")){
pizza = new Clam();
}else{
throw new RuntimeException("Error:invalid type");
}
/*
下面的是不會改變的,所有的比薩必須要經過這幾個程序。
*/
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
現在最好將創建對象移到orderPizza()之外,但怎麼做呢?
可以把創建比薩的代碼移到另外一個對象中,由這個新對象專職創建比薩。
我們稱這個新對象爲“工廠”
工廠處理創建對象的細節,一旦有了工廠,orderPizza就變成此對象的客戶。當需要比薩時,就叫比薩工廠做一個。那些orderPizza()方法需要知道希臘比薩或者蛤蜊比薩的日子一去不復返了。現在orderPizza()方法只關心從工廠得到了一個比薩,而這個比薩實現了Pizza接口,所以它可以調用prepare(),bake(),cut(),box()來分別進行準備,烘烤,切片,裝盒。
- 建立一個簡單比薩工廠
/**
* @author hgl
* @data 2018年5月6日
* @description 簡單比薩工廠
*/
public class SimplePizzaFactory {
public Pizza createPizza(String type){
Pizza pizza = null;
if(type.equals("cheese")){
pizza = new Cheese();
}else if(type.equals("greek")){
pizza = new Greek();
}else if(type.equals("clam")){
pizza = new Clam();
}else{
throw new RuntimeException("Error:invalid type");
}
return pizza;
}
}
這麼做有什麼好處?似乎把一個問題搬到另外一個對象罷了
答:別忘了,SimplePizzaFactory可以有許多的客戶。雖然目前只看到orderPizza()方法是它的客戶,然而,可能還有PizzaShopMenu(比薩菜單)類會利用這個工廠來取得比薩的價錢和描述。等等。所以,把創建比薩的代碼包裝進一個類,當以後實現改變時,只需要修改這個類即可。
新的PizzaStore的代碼:
/**
* @author hgl
* @data 2018年5月6日
* @description 比薩店
*/
public class PizzaStore {
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
super();
this.factory = factory;
}
public Pizza orderPizza(String type){
Pizza pizza;
pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
簡單工廠其實不是一個設計模式,反而比較像是一種編程習慣,但是經常會被使用。
2 工廠方法
現在出現了很多加盟店,而我們希望確保加盟店營運的質量,但是區域的差異呢?每家加盟店都可能想要提供不同風味的比薩(比方說紐約,芝加哥,加州)。質量怎麼控制?比如加盟店採用它們自創的流程:烘烤的做法有些差異,不要切片,使用其他廠商的盒子。
我們希望建立一個框架,把加盟店和創建比薩捆綁在一起的同時又保持一定的彈性。
有個做法可讓比薩製作活動侷限於PizzaStore類,而同時又能讓這些加盟店依然可以自由地製作該區域的風味。
所要做的事情,就是把createPizza()方法放回到PizzaStore中,不過要把它設置成“抽象方法”,然後爲每個區域風味創建一個PizzaStore的子類。
改變後的代碼:
public abstract class PizzaStore {
public Pizza orderPizza(String type){
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
/*
現在,實例化比薩的責任被移到一個方法中,此方法就如同是
一個“工廠”.
工廠方法是抽象的,所以依賴子類來處理對象的創建
工廠方法必須返回一個產品。超類中定義的方法,通常使用到工廠方法的返回值
工廠方法將客戶(也就是超類中的代碼,例如orderPizza())和實際創建
具體產品的代碼分割開來。
*/
public abstract Pizza createPizza(String type);
}
/**
* @author hgl
* @data 2018年5月6日
* @description 紐約店
*/
public class NYPizzaStore extends PizzaStore{
@Override
public Pizza createPizza(String type) {
/*
* 這裏我只寫了一個紐約pizza,其實有很多
*/
if(type.equals("cheese")){
return new NYStyleCheesePizza();
}else return null;
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 芝加哥店
*/
public class ChicagoStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
//這裏我也只寫了一個,當然還有很多中披薩
if(type.equals("cheese")){
return new ChicagoStyleCheesePizza();
}
return null;
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 紐約風味的pizza
*/
public class NYStyleCheesePizza extends Pizza {
public NYStyleCheesePizza(){
/*
* 紐約比薩有自己的大蒜番茄醬和薄餅
*/
name = "NY style Sauce and Cheese Pizza";
dough = "Thin Crust Dough";
sauce = "Marinara Sauce";
/*
* 上面覆蓋的是意大利reggiano高級奶酪
*/
toppings.add("Grated Reggiano Cheese");
}
}
public class ChicagoStyleCheesePizza extends Pizza {
public ChicagoStyleCheesePizza(){
/*
* 芝加哥比薩使用小番茄作爲醬料,並使用厚餅
*/
name = "Chicago Style Deep DIsh Cheese Pizza";
dough = "Extra Thick Crust Dough";
sauce = "Plum Tomato Sauce";
/*
* 芝加哥風味的深盤比薩使用許多mozzarella(意大利白乾酪)
*/
toppings.add("Shredded Mozzarella Cheese");
}
/*
* 這個芝加哥風味比薩覆蓋類cut()方法,將比薩切成正方形
*/
public void cut(){
System.out.println("Cutting the pizza into square slices");
}
}
public class PizzaTestDrive {
public static void main(String[] args) {
PizzaStore nyStore = new NYPizzaStore();
PizzaStore chicagoStore = new ChicagoStore();
Pizza pizza = nyStore.orderPizza("cheese");
System.out.println("Ethan ordered a "+pizza.getName()+"\n");
pizza = chicagoStore.orderPizza("cheese");
System.out.println("Joel ordered a "+pizza.getName()+"\n");
}
}
所有工廠模式都用來封裝對象的創建。工廠方法模式(Factory Method Pattern)通過讓子類決定該創建的對象是什麼,來達到將對象創建的過程封裝的目的。
工廠方法模式:定義了一個創建對象的接口,但由子類決定要實例化的類是哪一個。工廠方法讓類把實例化推遲到子類。
- 依賴倒置原則
要依賴抽象,不要依賴具體類
即:不能讓高層組件依賴低層組件,而且不管高層或低層組件,“兩者”都應該依賴於抽象。
所謂“高層”組件,是由其他低層組件定義其行爲的類。例如,PizzaStore是個高層組件,因爲它的行爲是由比薩定義的:PizzaStore創建所有不同的比薩對象,準備,烘烤,切片,裝盒,而比薩本身屬於低層組件。
看一段代碼:
/*
這個版本的PizzaStore依賴於所有的比薩對象,因爲它直接創建這些比薩對象
如果這些類的實現改變了,那麼可能必須修改PizzaStore
每新增一個比薩種類,就等於讓PizzaStore多了一個依賴
因爲對於比薩具體實現的任何改變都會影響到PizzaStore。我們說PizzaStore“依賴於”比薩的實現
*/
public class DependentPizzaStore {
public Pizza createPizza(String style,String type){
Pizza pizza = null;
if(style.equals("NY")){
if(type.equals("cheese")){
pizza = new NYStyleCheesePizza();
}
}else if (style.equals("Chicago")){
if(type.equals("cheese")){
pizza = new ChicagoStyleCheesePizza();
}
}else {
System.out.println("Erro:invalid type of pizza");
return null;
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
當直接實例化一個對象時,就是在依賴它的具體類。上面的這個比薩店,它由比薩店類創建所有的比薩對象,而不是委託給工廠。
現在,這個原則告訴我們,應該重寫代碼以便於我們依賴抽象類,而不依賴具體類。對於高層及低層模塊都應該如此。
之前運用工廠方法之後,高層組件PizzaStore和低層組件(也就是這些比薩)都依賴了Pizza對象。高層組件並沒有依賴低層組件,而且兩者都依賴了抽象,低層組件依賴高層的抽象。
- 幾個指導方針幫助你遵循此原則
*變量不可以持有具體類的引用:如果使用new,就會持有具體類的引用。你可以用工廠來避開這樣的做法。
*不要讓類派生自具體類:如果派生自具體類,就會依賴具體類。
*不要覆蓋基類中已實現的方法:如果覆蓋基類已實現的方法,那麼這個基類就不是一個真正適合被繼承的抽象。基類中已實現的方法,應該由所有的子類共享。
3 抽象工廠
生產原料,但是每個區域所使用的原料都不相同,比如紐約的紅醬料和芝加哥的紅醬料是不同的。該如何做呢?一下代碼解決了這個問題
/**
* @author hgl
* @data 2018年5月6日
* @description pizza抽象類
*/
public abstract class Pizza {
public String name;
public Dough dough;
public Sauce sauce;
public Veggies veggies[];
public Cheese cheese;
public Pepperoni pepperoni;
public Clams clam;
public abstract void prepare();
public void bake(){
System.out.println("Bake for 25 minutes at 350");
}
public void cut(){
System.out.println("Cutting the pizza into diagonal slices");
}
public void box(){
System.out.println("Place pizza in official PizzaStore box");
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
public String toString(){
return name;
}
}
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese CreateCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
public interface Dough {
}
public interface Sauce {
}
public interface Cheese {
}
public interface Clams {
}
public interface Pepperoni {
}
public interface Veggies {
}
public class ThinCrustDough implements Dough {
}
public class SlicedPepproni implements Pepperoni {
}
public class ReggianoCheese implements Cheese {
}
public class FreshClams implements Clams {
}
public class MarinaraSauce implements Sauce {
}
public class Mushroom implements Veggies {
}
public class Onion implements Veggies {
}
public class Garlic implements Veggies {
}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
@Override
public Dough createDough() {
// TODO Auto-generated method stub
return new ThinCrustDough();
}
@Override
public Sauce createSauce() {
// TODO Auto-generated method stub
return new MarinaraSauce();
}
@Override
public Cheese CreateCheese() {
// TODO Auto-generated method stub
return new ReggianoCheese();
}
@Override
public Veggies[] createVeggies() {
// TODO Auto-generated method stub
Veggies[] veggies = {new Garlic(),new Onion(),new Mushroom()};
return veggies;
}
@Override
public Pepperoni createPepperoni() {
// TODO Auto-generated method stub
return new SlicedPepproni();
}
@Override
public Clams createClam() {
// TODO Auto-generated method stub
return new FreshClams();
}
}
/**
* @author hgl
* @data 2018年5月6日
* @description 紐約店
*/
public class NYPizzaStore extends PizzaStore{
@Override
public Pizza createPizza(String type) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
if(type.equals("cheese")){//只寫了一個例子
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
}
return pizza;
}
}
我們引入新類型的工廠,也就是所謂的抽象工廠,來創建比薩原料家族。
通過抽象工廠所提供的接口,可以創建產品的家族,利用這個接口書寫代碼,我們的代碼將從實際工廠解藕,以便在不同上下文中實現各式各樣的工廠,製造出各種不同的產品。因爲代碼從實際的產品中解藕了,所以我們可以替換不同的工廠來取得不同的行爲。
抽象工廠模式:提供一個接口,用於創建相關或依賴對象的家族,而不需要明確指定具體類。