設計模式提供了一種可靠和簡單的方法來遵循經過驗證的設計原則,並編寫結構良好且可維護的代碼。適配器模式是面向對象軟體開發中流行和常用的模式之一。它遵循Robert C. Martin的依賴倒置原則,並使您可以重用現有的類,即使它不實現預期的接口。
如果您對適配器模式進行了一些研究,您會發現它有兩個不同的版本:
使用繼承實現適配器的類適配器模式。使用組合引用適配器內包裝類的實例的對象適配器模式。
您可能知道關於繼承與組合的所有討論。當您需要更改現有代碼時,Composition提供了更大的靈活性並避免了意想不到的副作用。由於這個原因,對象適配器模式是目前較為流行的方法,也是我在本文中將重點討論的方法。
適配器模式
軟體開發中適配器的一般概念與物理世界中的相同。如果你去過不同的國家,你可能會認識到他們中的很多人使用不同形狀的電源插座。很多時候,它們的形狀使得電子設備的插頭不適合。那麼,您如何將手機或筆記本電腦的充電器連接到這些電源插座?
答案很簡單。您將獲得一個適配器,您可以將其放入電源插座,然後將其插入適配器的另一端。適配器更改了插頭的形狀,以便您可以使用電源插座。在那個例子和大多數其他情況下,適配器不提供任何附加功能。它只是讓您將插頭連接到電源插座。
適配器模式通過在接口和現有類之間引入額外的適配器類,將相同的想法應用於面向對象的編程。
適配器類實現期望的接口並保持對要重用的類的對象的引用。由接口定義的方法調用被引用對象上的一個或多個方法並返回預期類型的值。通過這樣做,適配器類通過實現接口來實現預期的契約,並使您能夠重用現有的,不兼容的實現。
讓我們將這個模式應用於一個例子。
使用適配器模式衝泡咖啡
我喜歡以新鮮的一杯咖啡開始我的早晨。唯一的問題是,我需要起床前準備咖啡,然後才可以喝它。當我的鬧鈴響起時,它會自動準備好。所以,讓我們為它構建一個小應用程式。
一款應用來釀造咖啡
該FilterCoffeeApp正是這麼做的。它期望FilterCoffeeMachine接口的實現作為構造參數,並在prepareCoffee方法中使用它來衝泡一杯過濾咖啡。
publicclassFilterCoffeeApp {
privateLoggerlog=Logger.getLogger(
FilterCoffeeApp.class.getSimpleName());
privateFilterCoffeeMachinecoffeeMachine;
publicFilterCoffeeApp(FilterCoffeeMachinecoffeeMachine) {
this.coffeeMachine=coffeeMachine;
}
publicCoffeeprepareCoffee() {
Coffeecoffee=this.coffeeMachine.brewCoffee();
log.info("Coffee is ready!");
log.info(" -> "+coffee);
returncoffee;
}
}
該FilterCoffeeMachine界面比較簡單。它只是定義了brew咖啡的方法,你可以打電話來做一杯咖啡。
publicinterfaceFilterCoffeeMachine {
CoffeebrewCoffee();
}
這似乎是一個很好的方法,可以讓您在應用程式中使用不同的咖啡機。唯一的要求是代表咖啡機的所有類實現FilterCoffeeMachine接口。
一臺基本的咖啡機
所述BasicCoffeeMachine類實現該接口,並且可以通過使用FilterCoffeeApp。
publicclassBasicCoffeeMachineimplementsFilterCoffeeMachine {
privateConfigurationconfig;
privateMap<CoffeeSelection, GroundCoffee>groundCoffee; privateBrewingUnitbrewingUnit;
publicBasicCoffeeMachine(Map<CoffeeSelection, GroundCoffee>coffee) {
this.groundCoffee=coffee;
this.brewingUnit=newBrewingUnit();
this.config=newConfiguration(30, 480);
}
@Override
publicCoffeebrewCoffee() {
// get the coffee
GroundCoffeegroundCoffee=this.groundCoffee.get(
CoffeeSelection.FILTER_COFFEE);
// brew a filter coffee
returnthis.brewingUnit.brew(
CoffeeSelection.FILTER_COFFEE, groundCoffee,
this.config.getQuantityWater());
}
publicvoidaddGroundCoffee(
CoffeeSelectionsel, GroundCoffeenewCoffee)
throwsCoffeeException {
GroundCoffeeexistingCoffee=this.groundCoffee.get(sel);
if (existingCoffee!=null) {
if (existingCoffee.getName().equals(newCoffee.getName())) {
existingCoffee.setQuantity(existingCoffee.getQuantity() +newCoffee.getQuantity());
}
else {
thrownewCoffeeException(
"Only one kind of coffee supported for each CoffeeSelection.");
}
} else {
this.groundCoffee.put(sel, newCoffee);
}
}
}
優質咖啡機
但是如果你得到一臺不具備FilterCoffeeMachine界面的新型咖啡機,會發生什麼?
publicclassPremiumCoffeeMachine {
privateMap<CoffeeSelection, Configuration>configMap;
privateMap<CoffeeSelection, CoffeeBean>beans;
privateGrindergrinder;
privateBrewingUnitbrewingUnit;
publicPremiumCoffeeMachine(Map<CoffeeSelection, CoffeeBean>beans) {
this.beans=beans;
this.grinder=newGrinder();
this.brewingUnit=newBrewingUnit();
this.configMap=newHashMap<>();
this.configMap.put(
CoffeeSelection.FILTER_COFFEE, newConfiguration(30, 480));
this.configMap.put(
CoffeeSelection.ESPRESSO, newConfiguration(8, 28));
}
publicCoffeebrewCoffee(CoffeeSelectionselection)
throwsCoffeeException {
switch (selection) {
caseFILTER_COFFEE:
returnbrewFilterCoffee();
caseESPRESSO:
returnbrewEspresso();
default:
thrownewCoffeeException(
"CoffeeSelection "+selection+" not supported");
}
}
privateCoffeebrewEspresso() {
Configurationconfig=configMap.get(
CoffeeSelection.ESPRESSO);
// grind the coffee beans
GroundCoffeegroundCoffee=this.grinder.grind(
this.beans.get(CoffeeSelection.ESPRESSO),
config.getQuantityCoffee());
// brew an espresso
returnthis.brewingUnit.brew(
CoffeeSelection.ESPRESSO, groundCoffee,
config.getQuantityWater());
}
privateCoffeebrewFilterCoffee() {
Configurationconfig=configMap.get(
CoffeeSelection.FILTER_COFFEE);
// grind the coffee beans
GroundCoffeegroundCoffee=this.grinder.grind(
this.beans.get(CoffeeSelection.FILTER_COFFEE),
config.getQuantityCoffee());
// brew a filter coffee
returnthis.brewingUnit.brew(
CoffeeSelection.FILTER_COFFEE, groundCoffee,
config.getQuantityWater());
}
publicvoidaddCoffeeBeans(CoffeeSelectionsel, CoffeeBeannewBeans) throwsCoffeeException {
CoffeeBeanexistingBeans=this.beans.get(sel);
if (existingBeans!=null) {
if (existingBeans.getName().equals(newBeans.getName())) {
existingBeans.setQuantity(existingBeans.getQuantity() +newBeans.getQuantity());
} else {
thrownewCoffeeException(
"Only one kind of coffee supported for each CoffeeSelection.");
}
} else {
this.beans.put(sel, newBeans);
}
}
}
您可以使用咖啡brewCoffee(CoffeeSelection選擇)拋出CoffeeException的方法PremiumCoffeeMachine準備過濾咖啡或咖啡。如您所見,該方法的名稱與FilterCoffeeMachine接口定義的名稱相同,但方法籤名不兼容。它期望一個參數並聲明一個異常。
該PremiumCoffeeMachine代表一個咖啡機,但它沒有實現FilterCoffeeMachine接口。所以,你不能在FilterCoffeeApp中使用它。
我不會改變這個類,以便它實現所需的接口。通常不可能改變現有的類,因為它們是由不同的團隊實現的,或者這些類在其他不包含所需接口的項目中使用。我也不想更改FilterCoffeeMachine接口。該BasicCoffeeMachine實現該接口和我需要改變類每當我更改界面。在這些情況下,最好應用適配器模式。
實現適配器
通過引入實現FilterCoffeeMachine接口並包裝PremiumCoffeeMachine類的適配器類,您可以啟用FilterCoffeeApp以使用咖啡機。
在這個例子中,適配器類需要完成兩項任務:
它必須提供FilterCoffeeMachine接口的實現。該brewCoffee方法需要縮小之間的間隙brewCoffee由接口和所定義的方法brewCoffee由實現的方法PremiumCoffeeMachine類。
界面和現有的類沒有太大的不同。這使得適配器類的實現相對簡單。
publicclassFilterCoffeeAdapterimplementsFilterCoffeeMachine {
privateLoggerlog=Logger.getLogger(
FilterCoffeeAdapter.class.getSimpleName());
privatePremiumCoffeeMachinemachine;
publicFilterCoffeeAdapter(PremiumCoffeeMachinemachine) {
this.machine=machine;
}
@Override
publicCoffeebrewCoffee() {
try {
returnmachine.brewCoffee(
CoffeeSelection.FILTER_COFFEE);
} catch (CoffeeExceptione) {
log.severe(e.toString());
returnnull;
}
}
}
正如你在代碼片段中看到的那樣,FilterCoffeeAdapter類實現了FilterCoffeeMachine接口,並且期望PremiumCoffeeMachine對象作為構造器參數。它將該對象保存在專用欄位中,以便它可以在brewCoffee方法中使用它。
brewCoffee方法的實現對於大多數適配器類來說是非常關鍵的,也是最困難的部分。在這種情況下,PremiumCoffeeMachine類提供了一個可以調用以執行任務的方法。但它更靈活,需要一個CoffeeSelection枚舉值來定義它應該生成哪種咖啡。
該方法PremiumCoffeeMachine類會引發CoffeeException如果它得到了一個名為CoffeeSelection不同於值FILTER_COFFEE和ESPRESSO。FilterCoffeeMachine接口的brewCoffee方法不會聲明此異常,您需要在方法實現中處理它。
在這個例子中,沒有完美的方法來做到這一點。您可以編寫日誌消息並返回null,就像我在代碼片段中所做的一樣,也可以拋出RuntimeException。
在你的應用程式中,你可能有更好的方法來處理異常。您可能能夠執行重試或觸發不同的業務操作。這將使您的應用程式更健壯並改進適配器的實現。
概要
適配器模式是面向對象程式語言中經常使用的模式。與物理世界中的適配器類似,您可以實現一個類來彌補預期接口和現有類之間的差距。這使您可以重用未實現所需接口的現有類,並使用多個類的功能,否則這些類將不兼容。
適配器模式的一個優點是您不需要更改現有的類或接口。通過引入一個作為接口和類之間的適配器的新類,可以避免對現有代碼進行任何更改。這限制了對軟體組件的更改範圍,並避免了其他組件或應用程式中的任何更改和副作用。
適配器模式提供了依賴倒置設計原理的實現。如果您還不熟悉它,我建議閱讀有關不同的SOLID設計原則。