SOLID设计原则之接口隔离

SOLID设计原则之接口隔离

“接口隔离原则”的目标是通过将软件分为多个独立的部分来减少所需更改的副作用和频率。

SOLID设计原则之接口隔离

接口隔离原则是Robert C. Martin的SOLID设计原则之一。尽管这些原则已有多年历史,但它们仍然与他首次出版时一样重要。您甚至可能会争辩说,微服务体系结构样式增加了它们的重要性,因为您也可以将这些原理应用于微服务。

在前面的文章中,我已经解释了单一责任原则,开放/封闭原则和Liskov替代原则。因此,让我们集中讨论接口隔离原则。

提示:使用Stackify Retrace立即发现应用程序错误和性能问题

借助集成的错误,日志和代码级性能见解,可以轻松地对代码进行故障排除和优化。

接口隔离原则的定义

接口隔离原则是由Robert C. Martin在为Xerox咨询时定义的,以帮助他们为新的打印机系统构建软件。他将其定义为:

“不应强迫客户端依赖于不使用的接口。”

听起来很明显,不是吗?好吧,正如我将在本文中向您展示的那样,很容易违反此接口,尤其是在您的软件不断发展并且您必须添加越来越多的功能的情况下。但是稍后会更多。

与“单一职责原则”相似,“接口隔离原则”的目标是通过将软件分为多个独立的部分来减少所需更改的副作用和频率。

正如我将在以下示例中向您展示的那样,只有在定义接口以使其适合特定客户端或任务的情况下,这才可以实现。

违反接口隔离原则

我们谁也不会无视通用的设计原则来编写不良软件。但是,经常会发生这样的情况:一个应用程序被使用了很多年,并且它的用户经常要求新功能。

从业务角度来看,这是一个很好的情况。但是从技术角度来看,每次更改的实施都存在风险。尝试将新方法添加到现有接口很诱人,即使该方法实现了不同的职责,并且最好在新接口中进行分离。这通常是接口污染的开始,迟早会导致接口过大,其中包含实现多种职责的方法。

让我们看一个发生此情况的简单示例。

最初,该项目使用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;}

用新方法污染接口

但是后来有人认为该应用程序还需要支持浓缩咖啡机。开发团队将其建模为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.");
    }}

开发人员认为浓缩咖啡机只是另一种咖啡机。因此,它必须实现CoffeeMachine接口。

唯一的区别是brewEspresso方法,该方法由EspressoMachine类而不是brewFilterCoffee方法实现。现在让我们忽略接口隔离原理,并执行以下三个更改:

该EspressoMachine类实现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类上实现brewEspresso方法,因为它是由CoffeeMachine接口定义的。您还可以在CoffeeMachine接口上提供与默认方法相同的实现。

    @Overridepublic CoffeeDrink brewEspresso() throws CoffeeException {
    throw new CoffeeException("This machine only brews filter coffee.");}

完成这些更改后,类图应如下所示:

image.png

特别是第二次和第三次更改应该向您显示CoffeeMachine界面不适用于这两种咖啡机。该brewEspresso的方法BasicCoffeeMachine类和brewFilterCoffee的方法EspressoMachine类抛出CoffeeException,因为这些操作不会受到这些类型的机器支持。您仅需实现它们,因为CoffeeMachine接口需要它们。

但是,这两种方法的实现不是真正的问题。问题是,如果BasicCoffeeMachine方法的brewFilterCoffee方法的签名更改,则CoffeeMachine接口将更改。这也将需要在一个变化EspressoMachine类和使用的所有其他类EspressoMachine,即便如此,brewFilterCoffee方法不提供任何功能,他们不调用它。

遵循接口隔离原则

好的,那么您如何解决CoffeMachine接口及其实现BasicCoffeeMachine和EspressoMachine?

您需要将CoffeeMachine接口拆分为用于不同类型咖啡机的多个接口。接口的所有已知实现都实现addGroundCoffee方法。因此,没有理由将其删除。

public interface CoffeeMachine {

    void addGroundCoffee(GroundCoffee newCoffee) throws CoffeeException;}

brewFilterCoffee和brewEspresso方法不是这种情况。您应该创建两个新接口以将它们彼此隔离。并且在此示例中,这两个接口还应该扩展CoffeeMachine接口。但是,如果您重构自己的应用程序,则不必如此。请仔细检查接口层次结构是正确的方法,还是应该定义一组接口。

完成此操作后,FilterCoffeeMachine接口将扩展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类不再需要提供空方法实现,并且彼此独立。

image.png

现在,BasicCoffeeMachine类实现了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类实现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;
        }
    }}

扩展应用程序

  • 分离了接口以便可以彼此独立地发展两个咖啡机实现之后,您可能想知道如何在应用程序中添加不同种类的咖啡机。通常,有四个选项:

  • 新的咖啡机是FilterCoffeeMachine或EspressoCoffeeMachine。在这种情况下,您只需要实现相应的接口即可。

  • 新的咖啡机冲泡过滤咖啡和浓缩咖啡。这种情况类似于第一种情况。唯一的区别是您的类现在同时实现了两个接口。在FilterCoffeeMachine和EspressoCoffeeMachine。

  • 新的咖啡机与其他两个完全不同。也许这是这些便签机之一,您也可以用来冲茶或其他热饮。在这种情况下,您需要创建一个新接口并决定是否要扩展CoffeeMachine接口。在便签机的示例中,您不应该这样做,因为您无法将咖啡粉添加到便签机中。因此,您的PadMachine类不需要实现addGroundCoffee方法。

  • 新的咖啡机提供了新的功能,但您也可以使用它来冲泡过滤咖啡或浓缩咖啡。在这种情况下,您应该为新功能定义一个新接口。然后,您的实现类可以实现此新接口以及一个或多个现有接口。但是请确保将新接口与现有接口分开,就像对FilterCoffeeMachine和EspressoCoffeeMachine接口所做的那样。

概要

SOLID设计原则可帮助您实施健壮且可维护的应用程序。在本文中,我们详细研究了接口隔离原则,Robert C. Martin将其定义为:

“不应强迫客户端依赖于不使用的接口。”

通过遵循此原则,可以防止为多个职责定义方法的接口过大。如“单一职责原则”中所述,您应该避免具有多种职责的类和接口,因为它们经常更改并且使您的软件难以维护。