SOLID設計原則之接口隔離
“接口隔離原則”的目標是通過將軟件分為多個獨立的部分來減少所需更改的副作用和頻率。
接口隔離原則是Robert C. Martin的SOLID設計原則之一。盡管這些原則已有多年歷史,但它們仍然與他首次出版時一樣重要。您甚至可能會爭辯說,微服務體系結構樣式增加了它們的重要性,因為您也可以將這些原理應用于微服務。
在前面的文章中,我已經解釋了單一責任原則,開放/封閉原則和Liskov替代原則。因此,讓我們集中討論接口隔離原則。
提示:使用Stackify Retrace立即發(fā)現(xiàn)應用程序錯誤和性能問題
借助集成的錯誤,日志和代碼級性能見解,可以輕松地對代碼進行故障排除和優(yōu)化。
接口隔離原則的定義
接口隔離原則是由Robert C. Martin在為Xerox咨詢時定義的,以幫助他們?yōu)樾碌拇蛴C系統(tǒng)構建軟件。他將其定義為:
“不應強迫客戶端依賴于不使用的接口?!?/span>
聽起來很明顯,不是嗎?好吧,正如我將在本文中向您展示的那樣,很容易違反此接口,尤其是在您的軟件不斷發(fā)展并且您必須添加越來越多的功能的情況下。但是稍后會更多。
與“單一職責原則”相似,“接口隔離原則”的目標是通過將軟件分為多個獨立的部分來減少所需更改的副作用和頻率。
正如我將在以下示例中向您展示的那樣,只有在定義接口以使其適合特定客戶端或任務的情況下,這才可以實現(xiàn)。
違反接口隔離原則
我們誰也不會無視通用的設計原則來編寫不良軟件。但是,經常會發(fā)生這樣的情況:一個應用程序被使用了很多年,并且它的用戶經常要求新功能。
從業(yè)務角度來看,這是一個很好的情況。但是從技術角度來看,每次更改的實施都存在風險。嘗試將新方法添加到現(xiàn)有接口很誘人,即使該方法實現(xiàn)了不同的職責,并且最好在新接口中進行分離。這通常是接口污染的開始,遲早會導致接口過大,其中包含實現(xiàn)多種職責的方法。
讓我們看一個發(fā)生此情況的簡單示例。
最初,該項目使用BasicCoffeeMachine類對基本的咖啡機進行建模。它使用咖啡粉沖泡出美味的過濾咖啡。
class BasicCoffeeMachine implements CoffeeMachine { private Map<CoffeeSelection, Configuration> configMap; private GroundCoffee groundCoffee; private BrewingUnit brewingUnit; public BasicCoffeeMachine(GroundCoffee coffee) { this.groundCoffee = coffee; this.brewingUnit = new BrewingUnit(); this.configMap = new HashMap<>(); this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480)); } @Override public CoffeeDrink brewFilterCoffee() { Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, this.groundCoffee, config.getQuantityWater()); } @Override public void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException { if (this.groundCoffee != null) { if (this.groundCoffee.getName().equals(newCoffee.getName())) { this.groundCoffee.setQuantity(this.groundCoffee.getQuantity() + newCoffee.getQuantity()); } else { throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection."); } } else { this.groundCoffee = newCoffee; } }}
那時,使用addGroundCoffee和brewFilterCoffee方法提取CoffeeMachine接口非常好。這是咖啡機的兩種基本方法,所有未來的咖啡機都應實施。
public interface CoffeeMachine { CoffeeDrink brewFilterCoffee() throws CoffeeException; void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException;}
用新方法污染接口
但是后來有人認為該應用程序還需要支持濃縮咖啡機。開發(fā)團隊將其建模為EspressoMachine類,您可以在下面的代碼片段中看到該類。它與BasicCoffeeMachine類非常相似。
public class EspressoMachine implements CoffeeMachine { private Map configMap; private GroundCoffee groundCoffee; private BrewingUnit brewingUnit; public EspressoMachine(GroundCoffee coffee) { this.groundCoffee = coffee; this.brewingUnit = new BrewingUnit(); this.configMap = new HashMap(); this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28)); } @Override public CoffeeDrink brewEspresso() { Configuration config = configMap.get(CoffeeSelection.ESPRESSO); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, this.groundCoffee, config.getQuantityWater()); } @Override public void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException { if (this.groundCoffee != null) { if (this.groundCoffee.getName().equals(newCoffee.getName())) { this.groundCoffee.setQuantity(this.groundCoffee.getQuantity() + newCoffee.getQuantity()); } else { throw new CoffeeException( "Only one kind of coffee supported for each CoffeeSelection."); } } else { this.groundCoffee = newCoffee; } } @Override public CoffeeDrink brewFilterCoffee() throws CoffeeException { throw new CoffeeException("This machine only brew espresso."); }}
開發(fā)人員認為濃縮咖啡機只是另一種咖啡機。因此,它必須實現(xiàn)CoffeeMachine接口。
唯一的區(qū)別是brewEspresso方法,該方法由EspressoMachine類而不是brewFilterCoffee方法實現(xiàn)?,F(xiàn)在讓我們忽略接口隔離原理,并執(zhí)行以下三個更改:
該EspressoMachine類實現(xiàn)CoffeeMachine接口及其brewFilterCoffee方法。
public CoffeeDrink brewFilterCoffee() throws CoffeeException {throw new CoffeeException("This machine only brews espresso.");}
我們將brewEspresso方法添加到CoffeeMachine界面,以便該界面允許您沖泡意式濃縮咖啡。
public interface CoffeeMachine {CoffeeDrink brewFilterCoffee() throws CoffeeException;void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException;CoffeeDrink brewEspresso() throws CoffeeException;}
您需要在BasicCoffeeMachine類上實現(xiàn)brewEspresso方法,因為它是由CoffeeMachine接口定義的。您還可以在CoffeeMachine接口上提供與默認方法相同的實現(xiàn)。
@Overridepublic CoffeeDrink brewEspresso() throws CoffeeException { throw new CoffeeException("This machine only brews filter coffee.");}
完成這些更改后,類圖應如下所示:
特別是第二次和第三次更改應該向您顯示CoffeeMachine界面不適用于這兩種咖啡機。該brewEspresso的方法BasicCoffeeMachine類和brewFilterCoffee的方法EspressoMachine類拋出CoffeeException,因為這些操作不會受到這些類型的機器支持。您僅需實現(xiàn)它們,因為CoffeeMachine接口需要它們。
但是,這兩種方法的實現(xiàn)不是真正的問題。問題是,如果BasicCoffeeMachine方法的brewFilterCoffee方法的簽名更改,則CoffeeMachine接口將更改。這也將需要在一個變化EspressoMachine類和使用的所有其他類EspressoMachine,即便如此,brewFilterCoffee方法不提供任何功能,他們不調用它。
遵循接口隔離原則
好的,那么您如何解決CoffeMachine接口及其實現(xiàn)BasicCoffeeMachine和EspressoMachine?
您需要將CoffeeMachine接口拆分為用于不同類型咖啡機的多個接口。接口的所有已知實現(xiàn)都實現(xiàn)addGroundCoffee方法。因此,沒有理由將其刪除。
public interface CoffeeMachine { void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException;}
brewFilterCoffee和brewEspresso方法不是這種情況。您應該創(chuàng)建兩個新接口以將它們彼此隔離。并且在此示例中,這兩個接口還應該擴展CoffeeMachine接口。但是,如果您重構自己的應用程序,則不必如此。請仔細檢查接口層次結構是正確的方法,還是應該定義一組接口。
完成此操作后,F(xiàn)ilterCoffeeMachine接口將擴展CoffeeMachine接口,并定義brewFilterCoffee方法。
public interface FilterCoffeeMachine extends CoffeeMachine { CoffeeDrink brewFilterCoffee() throws CoffeeException;}
和EspressoCoffeeMachine接口還擴展了CoffeeMachine接口,并定義brewEspresso方法。
public interface EspressoCoffeeMachine extends CoffeeMachine { CoffeeDrink brewEspresso() throws CoffeeException;}
恭喜,您隔離了界面,以便不同咖啡機的功能彼此獨立。結果,BasicCoffeeMachine和EspressoMachine類不再需要提供空方法實現(xiàn),并且彼此獨立。
現(xiàn)在,BasicCoffeeMachine類實現(xiàn)了FilterCoffeeMachine接口,該接口僅定義addGroundCoffee和brewFilterCoffee方法。
public class BasicCoffeeMachine implements FilterCoffeeMachine { private Map<CoffeeSelection, Configuration> configMap; private GroundCoffee groundCoffee; private BrewingUnit brewingUnit; public BasicCoffeeMachine(GroundCoffee coffee) { this.groundCoffee = coffee; this.brewingUnit = new BrewingUnit(); this.configMap = new HashMap<>(); this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480)); } @Override public CoffeeDrink brewFilterCoffee() { Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, this.groundCoffee, config.getQuantityWater()); } @Override public void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException { if (this.groundCoffee != null) { if (this.groundCoffee.getName().equals(newCoffee.getName())) { this.groundCoffee.setQuantity(this.groundCoffee.getQuantity() + newCoffee.getQuantity()); } else { throw new CoffeeException( "Only one kind of coffee supported for each CoffeeSelection."); } } else { this.groundCoffee = newCoffee; } }}
而EspressoMachine類實現(xiàn)EspressoCoffeeMachine接口,其方法addGroundCoffee和brewEspresso。
public class EspressoMachine implements EspressoCoffeeMachine { private Map configMap; private GroundCoffee groundCoffee; private BrewingUnit brewingUnit; public EspressoMachine(GroundCoffee coffee) { this.groundCoffee = coffee; this.brewingUnit = new BrewingUnit(); this.configMap = new HashMap(); this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28)); } @Override public CoffeeDrink brewEspresso() throws CoffeeException { Configuration config = configMap.get(CoffeeSelection.ESPRESSO); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, this.groundCoffee, config.getQuantityWater()); } @Override public void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException { if (this.groundCoffee != null) { if (this.groundCoffee.getName().equals(newCoffee.getName())) { this.groundCoffee.setQuantity(this.groundCoffee.getQuantity() + newCoffee.getQuantity()); } else { throw new CoffeeException( "Only one kind of coffee supported for each CoffeeSelection."); } } else { this.groundCoffee = newCoffee; } }}
擴展應用程序
分離了接口以便可以彼此獨立地發(fā)展兩個咖啡機實現(xiàn)之后,您可能想知道如何在應用程序中添加不同種類的咖啡機。通常,有四個選項:
新的咖啡機是FilterCoffeeMachine或EspressoCoffeeMachine。在這種情況下,您只需要實現(xiàn)相應的接口即可。
新的咖啡機沖泡過濾咖啡和濃縮咖啡。這種情況類似于第一種情況。唯一的區(qū)別是您的類現(xiàn)在同時實現(xiàn)了兩個接口。在FilterCoffeeMachine和EspressoCoffeeMachine。
新的咖啡機與其他兩個完全不同。也許這是這些便簽機之一,您也可以用來沖茶或其他熱飲。在這種情況下,您需要創(chuàng)建一個新接口并決定是否要擴展CoffeeMachine接口。在便簽機的示例中,您不應該這樣做,因為您無法將咖啡粉添加到便簽機中。因此,您的PadMachine類不需要實現(xiàn)addGroundCoffee方法。
新的咖啡機提供了新的功能,但您也可以使用它來沖泡過濾咖啡或濃縮咖啡。在這種情況下,您應該為新功能定義一個新接口。然后,您的實現(xiàn)類可以實現(xiàn)此新接口以及一個或多個現(xiàn)有接口。但是請確保將新接口與現(xiàn)有接口分開,就像對FilterCoffeeMachine和EspressoCoffeeMachine接口所做的那樣。
概要
SOLID設計原則可幫助您實施健壯且可維護的應用程序。在本文中,我們詳細研究了接口隔離原則,Robert C. Martin將其定義為:
“不應強迫客戶端依賴于不使用的接口?!?/span>
通過遵循此原則,可以防止為多個職責定義方法的接口過大。如“單一職責原則”中所述,您應該避免具有多種職責的類和接口,因為它們經常更改并且使您的軟件難以維護。