日常開發系統中通常需要對接多個系統,需要用到適配器模式。 例如:支付方式就涉及多個系統對接。 國際慣例,先引入概念。 適配器模式: 提到適配器自然就能想到手機用的電源適配器。 他的作用就是將220V交流電轉換成手機使用的5V直流電。 適配器作用:將一個介面轉換成另外一個介面,已符合客戶的期望。 軟體 ...
日常開發系統中通常需要對接多個系統,需要用到適配器模式。
例如:支付方式就涉及多個系統對接。
國際慣例,先引入概念。
適配器模式:
提到適配器自然就能想到手機用的電源適配器。
他的作用就是將220V交流電轉換成手機使用的5V直流電。
適配器作用:將一個介面轉換成另外一個介面,已符合客戶的期望。
軟體系統中,比如一期我們使用了阿裡雲的sdk包的一些功能介面。
但是二期我想換騰訊雲sdk相同的功能。但是他們相同的功能,介面參數確不同。
我們不想按照騰訊雲的介面修改我們的業務代碼,畢竟業務邏輯已經經過反覆測試驗證了。可以把騰訊雲的介面包裝起來,實現一個一期的介面。這個工作叫【適配】。
在例如:
在軟體系統中,你可能有很多種支付方式,微信支付,支付寶支付,各種銀行。
但是他們的支付介面肯定都是不一樣的,我不希望我新加一種支付方式就加去修改代碼。
此時就需要有一個統一支付的適配器服務幫我們屏蔽各個支付方式的不同。
我的業務服務只和統一支付交互。統一支付向業務系統提供統一介面,統一支付負責路由不同支付系統後臺,並屏蔽掉各系統差異。這個工作也叫【適配】
適配模式:
/** * 目標介面:提供5V電壓的一個介面 */ public interface V5Power { public int provideV5Power(); } /** * 被適配者、已有功能:家用220V交流電 */ public class V220Power { /** * 提供220V電壓 */ public int provideV220Power() { System.out.println("我提供220V交流電壓。"); return 220 ; } } /** * 適配器,有已有對象已有功能實現介面,把220V電壓變成5V */ public class V5PowerAdapter implements V5Power { /** * 組合的方式 */ private V220Power v220Power ; public V5PowerAdapter(V220Power v220Power) { this.v220Power = v220Power ; } @Override public int provideV5Power() { int power = v220Power.provideV220Power() ; //power經過各種操作-->5 System.out.println("適配器:我悄悄的適配了電壓。"); return 5 ; } }適配器
public class Mobile { // 使用目標介面功能 public void inputPower(V5Power power) { int provideV5Power = power.provideV5Power(); System.out.println("手機(客戶端):我需要5V電壓充電,現在是-->" + provideV5Power + "V"); } } //測試 public class Test { public static void main(String[] args) { Mobile mobile = new Mobile(); V5Power v5Power = new V5PowerAdapter(new V220Power()) ; mobile.inputPower(v5Power); } }Test
定義:
將一個類的介面,轉換成客戶期望的另一個介面。適配器讓原本介面不相容的類可以合作無間。
適配模式說的通俗點就是用一個已有的功能類,去實現一個介面。這樣該功能類,和原來的客戶端代碼都不需要改變,對於客戶端來說相當於換了一種實現方式。
好處就是讓客戶從實現中【解耦】,下次在換其他的功能類,我就再寫一個適配器。有點像策略模式中,我們換一個實現類一樣。
當然,我們適配器可以包裝很多被適配對象,即可以組合很多已有功能類。因為很多介面很複雜需要使用很多類。
同樣,適配器也可以不使用【組合】對象形式,而是使用【繼承】。
示例:
以前java集合類都實現了Enumeration枚舉介面,可以遍歷集合中每個元素,而無需知道它們在集合內元素是如何被管理。
之後推出了新的Iterator迭代器介面,這個介面和枚舉介面很像,都可以讓你遍歷集合中每個元素。
不同的是,迭代器還提供了刪除元素的能力。
面對遺留代碼,這些代碼暴露出來的是枚舉介面,他們是老版本的java只支持枚舉,但是我們希望新的代碼中使用迭代器。只能用枚舉實現一個迭代器。
public class EnumerationIterator implements Iterator{ Enumeration enum; public EnumerationIterator(Enumeration enum){ this.enum = enum; } public boolean hasNext(){ return enum.hasMoreElements(); } public Object next(){ return enum.nextElement(); } public void remove(){ throw new UnsupportedOperationException(); } } /* 枚舉介面是只讀的,適配器無法實現一個有實際功能的remove()方法我們的實現方式並不完美,客戶必須小心潛在的異常。只要客戶足夠小心,並且在適配器的文檔中作出說明,這也算是一個合理的解決方案。 */用枚舉實現迭代器
外觀模式:
外觀模式:提供一個統一介面,用來訪問子系統中的一群介面。外觀定義了一個高層介面,讓子系統更容易使用。
例如:我們遙控器點擊下自動就打開幕布、打開投影機、打開音響、開始放電影。而不是一步一步的自己操作。
不僅僅是簡化了介面,也將客戶從組建彙總解耦了出來。
目的就是讓系統更加容易使用,也符合【最少知道】原則,因為客戶只有【外觀角色】一個朋友。
【最少知道】原則就是不要讓太多類耦合在一起,免得修改系統時候,牽一發而動全身。對象儘量“少交朋友”。之和自己有關的交互即可。
就對象而言,在該對象的方法內,我們只應該調用屬於以下範圍的方法:
1.該對象本身
2.被當做方法的參數而傳遞進來的對象
3.此方法所創建或實例化的任何對象
4.對象的任何組建,即屬性變數引用的對象
如果調用返回對象的方法,相當於向另外一個對象的子部分發出請求。我們就多依賴了一個對象。
// 不應用【最少知道】原則 public float getTemp(){ Thermometer thermometer = station.getThermometer(); return thermometer.getTemperature(); // 我們從氣象站獲取了溫度計對象,然後從該對象獲取了溫度。 } |
// 應用【最少知道】原則 public float getTemp(){ return station.getTemperature(); // 應該直接讓氣象站給我溫度,我不想依賴溫度計對象。 } |
裝飾模式:
用面向對象的方式對飲料進行描述。這種描述方式的局限在於。
我們飲料可以添加輔料。例如:大杯、加冰、加奶、雙倍咖啡。
我們不可能一種組合就建一個類。那樣類數量就爆炸了。
此時計算價格就非常困難。
將各種調料放在基類中。調料可以是boolean或者數據或者枚舉類型。
每種飲料計算價錢的時候就看這些調料是否有,或者有多少。
public class Drink{ private boolean milk; private boolean sugar; public double cost(){ double price = 0d; if(milk){ price +=0.5; } if(sugar){price +=0.1; } return price; } public void addMilk(){ this.mocha=true; } public void addSugar(){ this.whip= true; } } public class DarkCoffee extends Drink{ public double cost(){ double price = super.cost(); price += 2.0; return price; } } 測試: public class Test{ public static void main(String[] args){ Drink coffee = new DarkCoffee(); coffee.addMocha(); coffee.addWhip(); System.out.println(coffee.cost()); } }通過設置屬性解決調料問題
這種設計存在問題:
1.如果調味品很多,Drink類非常龐大,且新加和刪除調味品都需要修改類,調料改價格需要調整價格。
2.雙倍調味料的情況boolean值無法滿足。
3.很多調料是互斥的,例如iceCoffee是不能加mocha的,但是他還是繼承了父類的addMocha()方法。這個方法他不適用,他必須覆蓋這個方法讓它什麼都不做。
不符合【開閉原則】
裝飾模式解釋:
調料可以包裝基礎飲料,因為裝飾者和被裝飾者有相同的超類型,所以可以套娃形式一直套下去。
裝飾者可以在所委托被裝飾者的行為前後,加上自己的行為,已達到特定目的。
計算價格類似於遞歸,不斷委托給父類,最後統一回溯。
abstract class Drink{ public String description = "Unknown beverage"; public String getDescription(){ return description; } public abstract double cost(); } // 裝飾類 abstract class CondimentDecorator extends Drink{ public abstract String getDescription(); } // 飲料 class DarkCoffee extends Drink{ public DarkCoffee(){ this.description = "DarkCoffee"; } public double cost(){ return 1.99; } } // 包裝類:調料 class Sugar extends CondimentDecorator{ Drink drink; public Sugar(Drink drink){ this.drink = drink; } public String getDescription(){ return drink.getDescription() + ", add sugar"; } public double cost(){ return .20 + drink.cost(); } } //測試 class Test{ public static void main(String[] args){ Drink drink = new DarkCoffee(); drink = new Sugar(drink); // 加雙份糖 drink = new Sugar(drink); System.out.println(drink.getDescription() + " ,$"+drink.cost()); //輸出:DarkCoffee, add sugar, add sugar ,$2.39 } }裝飾模式
public interface Drink{ public double cost(); } public class DarkCoffee implements Drink{ public double cost(){ return 2.5; } } public class Decorator implements Drink{ private Drink drink; public Decorator (Drink drink){ this.drink= drink; } public double cost(){ return drink.cost(); } } public class Sugar extends Decorator { public Sugar (Drink drink){ super(drink); } public double cost(){ return super.cost() + 0.1; } }裝飾模式2
裝飾模式:動態將責任附加到對象上,若要拓展功能,裝飾者提供了比繼承更有彈性的替代方案。
裝飾者模式容易造成設計中大量的小類。數量太多。容易把人看懵。例如:java.io
/* 編寫一個裝飾者,將輸入流內所有大寫字元轉小寫。 我們需要拓展InputStream。 */ public class LowerCaseInputStream extends FilterInputStream { public LowerCaseInputStream(InputStream in) { super(in); } public int read() throws IOException { int c = in.read(); return (c == -1 ? c : Character.toLowerCase((char)c)); } public int read(byte[] b, int offset, int len) throws IOException { int result = in.read(b, offset, len); for (int i = offset; i < offset+result; i++) { b[i] = (byte)Character.toLowerCase((char)b[i]); } return result; } } public class InputTest { public static void main(String[] args) throws IOException { int c; InputStream in = null; try { in = new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt"))); while((c = in.read()) >= 0) { System.out.print((char)c); } } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { in.close(); } } System.out.println(); try (InputStream in2 = new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt")))) { while((c = in2.read()) >= 0) { System.out.print((char)c); } } catch (IOException e) { e.printStackTrace(); } } }自己包裝IO
例如:我們對HttpServletRequest進行攔截進行處理,實現過濾請求參數。
1). Servlet API 中提供了一個 HttpServletRequestWrapper 類來包裝原始的 request 對象, HttpServletRequestWrapper 類實現了 HttpServletRequest 介面中的所有方法, 這些方法的內部實現都是僅僅調用了一下所包裝的的 request 對象的對應方法 //包裝類實現 ServletRequest 介面. public class ServletRequestWrapper implements ServletRequest { //被包裝的那個 ServletRequest 對象 private ServletRequest request; //構造器傳入 ServletRequest 實現類對象 public ServletRequestWrapper(ServletRequest request) { if (request == null) { throw new IllegalArgumentException("Request cannot be null"); } this.request = request; } //具體實現 ServletRequest 的方法: 調用被包裝的那個成員變數的方法實現。 public Object getAttribute(String name) { return this.request.getAttribute(name); } public Enumeration getAttributeNames() { return this.request.getAttributeNames(); } //... } 2). 作用: 用於對 HttpServletRequest 或 HttpServletResponse 的某一個方法進行修改或增強. public class MyHttpServletRequest extends HttpServletRequestWrapper{ public MyHttpServletRequest(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { String val = super.getParameter(name); if(val != null && val.contains(" fuck ")){ val = val.replace("fuck", "****"); } return val; } } 3). 使用: 在 Filter 中, 利用 MyHttpServletRequest 替換傳入的 HttpServletRequest HttpServletRequest req = new MyHttpServletRequest(request); filterChain.doFilter(req, response);增強HttpServletRequest
例如:在一個方法前後列印時間
// 介面 public interface Dao { public void insert(); public void delete(); public void update(); } // 基礎實現類 public class DaoImpl implements Dao { @Override public void insert() { System.out.println("DaoImpl.insert()"); } @Override public void delete() { System.out.println("DaoImpl.delete()"); } @Override public void update() { System.out.println("DaoImpl.update()"); } } // 包裝類 public class LogDao implements Dao { private Dao dao; public LogDao(Dao dao) { this.dao = dao; } @Override public void insert() { System.out.println("insert()方法開始時間:" + System.currentTimeMillis()); dao.insert(); System.out.println("insert()方法結束時間:" + System.currentTimeMillis()); } @Override public void delete() { dao.delete(); } @Override public void update() { System.out.println("update()方法開始時間:" + System.currentTimeMillis()); dao.update(); System.out.println("update()方法結束時間:" + System.currentTimeMillis()); } } // 調用時候 Dao dao = new LogDao(new DaoImpl()); // 對於調用方來說,只知道調用了dao,不知道加上了日誌功能 // 問題:1.輸出日誌的邏輯無法復用;2.輸入日誌和業務邏輯有耦合。裝飾模式方法前後打日誌
其實裝飾模式和適配器模式很像。都是包裝一個對象,然後利用這個對象的功能搞出點對外提供的方法。
裝飾模式 |
適配器模式 |
外觀模式 |
目的:不改變介面,加入責任 |
目的:將一個介面轉成另一個介面 |
目的:讓介面跟簡單 |
裝飾者模式可以讓新的行為和責任要加入到設計中。而無需修改現有的代碼。 |
可以整合若幹類來提供客戶需要的介面。 |
|
當一個方法調用被委托給裝飾者的時候, 裝飾者有點像遞歸套娃,你不知道當前是第幾層。 |
可以使用新的庫和子集合,而無需改變任何代碼。適配器會按照原介面給你包好。 |
|
拓展包裝對象的行為和責任,不是簡單的傳送 |
一定進行介面的裝換。 |
示例:多支付系統解決方案
通過加入適配器模式,訂單 Service 在進行支付時調用的不再是外部的支付介面,而是“支付方式”介面,與外部系統解耦。
只要保證“支付方式”介面是穩定的,那麼訂單 Service 就是穩定的。比如:
當支付寶支付介面發生變更時,影響的只限於支付寶 Adapter;
當微信支付介面發生變更時,影響的只限於微信支付 Adapter;
當要增加一個新的支付方式時,只需要再寫一個新的 Adapter。
日後不論哪種變更,要修改的代碼範圍縮小了,維護成本自然降低了,代碼質量就提高了。
問題:
在劃分微服務過程中,經常糾結,對外的功能有沒有必要抽象出一個代理服務。專門負責與某廠商對接。
根據適配器的理念,如果這個代理服務如果能抽象出標準的介面,那他就有獨立的必要。核心業務服務只和代理服務交互,代理服務屏蔽外部廠商的不同。
日後廠商有變動,我們修改範圍控制在代理服務內,不會涉及到核心業務服務。
例如:我們一般會開發一個統一支付的服務,這個服務對接各個支付系統,對內提供標準支付介面,業務服務只和統一支付交互。
同時統一支付還能幫助我們處理對賬、過期自動退款等功能。讓業務系統穩定,輕便。
本文來自博客園,作者:wanglifeng,轉載請註明原文鏈接: https://www.cnblogs.com/wanglifeng717/p/16348529.html