SOLID设计原则依赖倒置原则
提供复杂逻辑的高级模块应易于重用,并且不受提供实用程序功能的低级模块的更改的影响。
依赖倒置原则的定义
该原理的基本思想很简单,即很重要:提供复杂逻辑的高级模块应易于重用,并且不受提供实用程序功能的低级模块的更改的影响。为此,您需要引入一个抽象,该抽象将高级模块和低级模块彼此分离。
基于此思想,Robert C. Martin对依赖倒置原则的定义包括两个部分:
高级模块不应依赖于低级模块。两者都应依赖抽象。
抽象不应依赖细节。细节应取决于抽象。
该定义的一个重要细节是,高层和低层模块取决于抽象。设计原理不仅会改变依赖关系的方向,就像您第一次阅读依赖关系的名称时所期望的那样。通过在高级模块和低级模块之间引入抽象,它在高级模块和低级模块之间划分了依赖关系。因此,最后,您得到两个依赖项:
高级模块取决于抽象,并且低层依赖于相同的抽象。
基于其他SOLID原则
这听起来可能比通常要复杂得多。因此,如果您在代码中应用了“打开/关闭原则”和“ Liskov替换原则”,则它也将遵循“依赖倒置原则”。
打开/关闭原则要求打开软件组件以进行扩展,但关闭软件组件以进行修改。您可以通过引入可以提供不同实现的接口来实现这一点。接口本身已关闭以进行修改,您可以通过提供新的接口实现轻松地对其进行扩展。
您的实现应遵循Liskov替换原理,以便您可以在不破坏应用程序的情况下将它们替换为同一接口的其他实现。
让我们看一下CoffeeMachine项目,我将在其中应用所有这三个设计原理。
用依赖倒置原理冲泡咖啡
您可以购买许多不同的咖啡机。较为简单的是使用水和磨碎的咖啡来冲泡过滤咖啡,而高级的则包括用研磨机新鲜研磨所需量的咖啡豆,并且可以用来冲泡不同种类的咖啡。
如果您构建了一个咖啡机应用程序,该应用程序会在早上自动为您冲泡一杯新鲜的咖啡,则可以将这些咖啡机建模为BasicCoffeeMachine和PremiumCoffeeMachine类。
依赖反转原理与代码示例
实施BasicCoffeeMachine
BasicCoffeeMachine的实现非常简单。它仅实现一个构造函数和两个公共方法。您可以调用addGroundCoffee方法来重新填充研磨咖啡,并调用brewFilterCoffee方法来冲泡一杯过滤咖啡。
import java.util.Map;public class BasicCoffeeMachine implements CoffeeMachine { private Configuration config; private Map<CoffeeSelection, GroundCoffee> groundCoffee; private BrewingUnit brewingUnit; public BasicCoffeeMachine(Map<CoffeeSelection, GroundCoffee> coffee). this.groundCoffee = coffee; this.brewingUnit = new BrewingUnit(); this.config = new Configuration(30, 480); } @Override public Coffee brewFilterCoffee() { // get the coffee GroundCoffee groundCoffee = this.groundCoffee.get(CoffeeSelection.FILTER_COFFEE); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, this.config.getQuantityWater()); } public void addGroundCoffee(CoffeeSelection sel, GroundCoffee newCoffee) throws CoffeeException { GroundCoffee existingCoffee = this.groundCoffee.get(sel); if (existingCoffee != null) { if (existingCoffee.getName().equals(newCoffee.getName())) { existingCoffee.setQuantity(existingCoffee.getQuantity() + newCoffee.getQuantity()) } else { throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection.") } } else { this.groundCoffee.put(sel, newCoffee) } } }
要实现遵循依赖倒置原则的类并可以使用BasicCoffeeMachine或PremiumCoffeeMachine类来冲泡咖啡,您需要应用“打开/关闭”和“ Liskov替换”原理。这需要少量的重构,在此期间您将为两个类引入接口抽象。
引入抽象
这两种咖啡机的主要任务是冲泡咖啡。但是它们使您可以冲泡不同种类的咖啡。如果使用BasicCoffeeMachine,则只能冲泡过滤咖啡,而使用PremiumCoffeeMachine,则可以冲泡过滤咖啡或浓缩咖啡。那么,哪种接口抽象最适合两个类?
正如所有咖啡爱好者都会同意的那样,过滤咖啡和浓缩咖啡之间存在巨大差异。这就是为什么我们使用不同的机器来酿造它们的原因,即使如此,某些机器也可以做到。因此,我建议创建两个独立的抽象:
所述FilterCoffeeMachine接口定义了咖啡brewFilterCoffee()方法,并得到由能够冲泡过滤咖啡的所有咖啡机类实现的。
您可以用来酿造意式浓缩咖啡的所有类均实现EspressoMachine接口,该接口定义了Coffee brewEspresso()方法。
如下面的代码片段所示,两个接口的定义都非常简单。
public interface CoffeeMachine { Coffee brewFilterCoffee();}public interface EspressoMachine { Coffee brewEspresso();}
在下一步中,您需要重构两个咖啡机类,以便它们实现这两个接口中的一个或两个。
重构BasicCoffeeMachine类
让我们从BasicCoffeeMachine类开始。您可以使用它来冲泡过滤咖啡,因此它应该实现CoffeeMachine接口。该类已经实现了brewFilterCoffee()方法。您只需要将实现CoffeeMachine添加到类定义中。
public class BasicCoffeeMachine implements CoffeeMachine { private Configuration config; private Map<CoffeeSelection, GroundCoffee> groundCoffee; private BrewingUnit brewingUnit; public BasicCoffeeMachine(Map<CoffeeSelection, GroundCoffee> coffee) { this.groundCoffee = coffee; this.brewingUnit = new BrewingUnit(); this.config = new Configuration(30, 480); } @Override public Coffee brewFilterCoffee() { // get the coffee GroundCoffee groundCoffee = this.groundCoffee.get(CoffeeSelection.FILTER_COFFEE); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, this.config.getQuantityWater()); } public void addGroundCoffee(CoffeeSelection sel, GroundCoffee newCoffee) throws CoffeeException { GroundCoffee existingCoffee = this.groundCoffee.get(sel); if (existingCoffee != null) { if (existingCoffee.getName().equals(newCoffee.getName())) { existingCoffee.setQuantity(existingCoffee.getQuantity() + newCoffee.getQuantity()); } else { throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection."); } } else { this.groundCoffee.put(sel, newCoffee); } } }
重构PremiumCoffeeMachine类
PremiumCoffeeMachine的重构也不需要很多工作。您可以使用咖啡机冲煮过滤咖啡和浓缩咖啡,因此PremiumCoffeeMachine类应实现CoffeeMachine和EspressoMachine接口。该类已经实现了两个接口定义的方法。您只需要声明它实现了接口。
import java.util.HashMap;import java.util.Map;public class PremiumCoffeeMachine implements CoffeeMachine, EspressoMachine { private Map<CoffeeSelection, Configuration> configMap; private Map<CoffeeSelection, CoffeeBean> beans; private Grinder grinder; private BrewingUnit brewingUnit; public PremiumCoffeeMachine(Map<CoffeeSelection, CoffeeBean> beans) { this.beans = beans; this.grinder = new Grinder(); this.brewingUnit = new BrewingUnit(); this.configMap = new HashMap<>(); this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480)); this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28)); } @Override public Coffee brewEspresso() { Configuration config = configMap.get(CoffeeSelection.ESPRESSO); // grind the coffee beans GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee()); // brew an espresso return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, groundCoffee, config.getQuantityWater()); } @Override public Coffee brewFilterCoffee() { Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE); // grind the coffee beans GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee()); // brew a filter coffee return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee,config.getQuantityWater()); } public void addCoffeeBeans(CoffeeSelection sel, CoffeeBean newBeans) throws CoffeeException { CoffeeBean existingBeans = this.beans.get(sel); if (existingBeans != null) { if (existingBeans.getName().equals(newBeans.getName())) { existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity()); } else { throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection."); } } else { this.beans.put(sel, newBeans); } }}
直接依赖于其中一个实现类的唯一代码是CoffeeAppStarter类,该类实例化CoffeeApp对象并提供CoffeeMachine接口的实现。您可以通过使用依赖项注入框架(例如Spring或CDI)在运行时解析依赖项来完全避免这种编译时依赖项。
public class CoffeeApp { private CoffeeMachine coffeeMachine; public CoffeeApp(CoffeeMachine coffeeMachine) { this.coffeeMachine = coffeeMachine } public Coffee prepareCoffee() throws CoffeeException { Coffee coffee = this.coffeeMachine.brewFilterCoffee(); System.out.println("Coffee is ready!"); return coffee; } }
import java.util.HashMap;import java.util.Map;public class CoffeeAppStarter { public static void main(String[] args) { // create a Map of available coffee beans Map<CoffeeSelection, CoffeeBean> beans = new HashMap<CoffeeSelection, CoffeeBean>(); beans.put(CoffeeSelection.ESPRESSO, new CoffeeBean( "My favorite espresso bean", 1000)); beans.put(CoffeeSelection.FILTER_COFFEE, new CoffeeBean( "My favorite filter coffee bean", 1000)) // get a new CoffeeMachine object PremiumCoffeeMachine machine = new PremiumCoffeeMachine(beans); // Instantiate CoffeeApp CoffeeApp app = new CoffeeApp(machine); // brew a fresh coffee try { app.prepareCoffee(); } catch (CoffeeException e) { e.printStackTrace(); } }}
概要
依赖倒置原则是我们在本系列中讨论的第五个也是最后一个设计原则。它引入了上层和下层软件组件之间的接口抽象,以消除它们之间的依赖关系。
正如在示例项目中看到的那样,您仅需要在代码库中应用“打开/关闭”和“ Liskov替换”原理。完成此操作后,您的类也将遵循“依赖倒置原则”。这使您能够更改上层和下层组件,而不会影响任何其他类,只要您不更改任何接口抽象即可。