为什么大多数系统架构师坚持要先对接口进行编程?

时间:2020-03-05 18:49:01  来源:igfitidea点击:

我阅读的几乎所有Java书籍都谈到使用接口作为对象之间共享状态和行为的一种方式,这些对象在最初"构造"时似乎并没有共享关系。

但是,每当我看到架构师设计应用程序时,他们要做的第一件事就是开始对接口进行编程。怎么会?我们如何知道该接口内将发生的对象之间的所有关系?如果我们已经知道这些关系,那么为什么不扩展抽象类呢?

解决方案

回答

对接口进行编程意味着遵守使用该接口创建的"合同"。因此,如果IPoweredByMotor接口具有start()方法,则实现该接口的将来的类(例如,MotorizedWheelChair,Automobile或者SmoothieMaker)将在实现该接口方法时增加灵活性系统,因为一段代码可以启动许多不同类型的事物的马达,因为一段代码需要知道的只是它们对start()的响应。无论如何启动,都必须启动。

回答

这是促进松散耦合的一种方法。

With low coupling, a change in one module will not require a change in the implementation of another module.

这个概念的一个很好的用途是抽象工厂模式。在Wikipedia示例中,GUIFactory接口生成Button接口。具体工厂可以是WinFactory(产生WinButton)或者OSXFactory(产生OSXButton)。想象一下,如果我们正在编写一个GUI应用程序,并且必须四处浏览OldButton类的所有实例,然后将它们更改为WinButton。然后明年,我们需要添加OSXButton版本。

回答

我认为,抽象类在很大程度上已被开发人员所放弃的原因之一可能是一种误解。

当四人帮写道:

Program to an interface not an implementation.

没有像Java或者Cinterface这样的东西。他们在谈论每个类都具有的面向对象的接口概念。埃里希·伽玛(Erich Gamma)在这次采访中提到了这一点。

我认为,不加思考就机械地遵循所有规则和原则会导致难以阅读,导航,理解和维护代码库。切记:最简单的方法可能会起作用。

回答

我会假设(使用@ eed3s9n)是为了促进松散耦合。而且,没有接口,单元测试将变得更加困难,因为我们无法模拟对象。

回答

从某种意义上说,我认为问题可以简单地归结为:"为什么要使用接口而不是抽象类?"从技术上讲,我们可以同时实现两者的松散耦合-基础实现仍不暴露给调用代码,并且我们可以使用Abstract Factory模式返回基础实现(接口实现与抽象类扩展),以提高设计的灵活性。实际上,我们可能会争辩说,抽象类为我们提供了更多,因为它们既使我们既需要实现来满足代码("我们必须实现start()")又提供默认的实现("我有一个标准的paint()我们可以可以重写,如果我们想使用接口,则必须提供实现,随着时间的推移,实现可能会因接口更改而导致脆弱的继承问题。

不过,从根本上讲,我使用接口的主要原因是Java的单一继承限制。如果我的实现必须从调用代码使用的抽象类继承,那意味着我失去了从其他东西继承的灵活性,即使这样做可能更有意义(例如,对于代码重用或者对象层次结构)。

回答

对接口进行编程可带来以下好处:

  • GoF类型模式(例如访客模式)必需
  • 允许替代实现。例如,对于抽象使用中的数据库引擎的单个接口,可能存在多个数据访问对象实现(AccountDaoMySQL和AccountDaoOracle都可以实现AccountDao)
  • 一个类可以实现多个接口。 Java不允许具体类的多重继承。
  • 摘要实现细节。接口可能仅包含公共API方法,隐藏了实现细节。好处包括记录明确的公共API和记录明确的合同。
  • 现代依赖项注入框架(例如http://www.springframework.org/)大量使用。
  • 在Java中,接口可用于创建动态代理-http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html。可以与Spring之类的框架一起非常有效地使用它来执行面向方面的编程。方面可以为类添加非常有用的功能,而无需直接向这些类添加Java代码。此功能的示例包括日志记录,审计,性能监视,事务划分等。http://static.springframework.org/spring/docs/2.5.x/reference/aop.html。
  • 模拟实现,单元测试-当依赖类是接口的实现时,可以编写模拟类来实现那些接口。模拟类可用于促进单元测试。

回答

好问题。我将向我们推荐《有效Java》中的Josh Bloch,他写了(项目16)为什么比抽象类更喜欢使用接口。顺便说一句,如果我们还没有这本书,我强烈推荐!这是他说的总结:

  • 可以很容易地对现有的类进行改造,以实现新的接口。我们需要做的就是实现接口并添加所需的方法。现有的类不能轻易地进行改造以扩展新的抽象类。
  • 接口是定义混入的理想选择。混合接口允许类声明其他可选行为(例如Comparable)。它允许将可选功能与主要功能混在一起。抽象类不能定义混入-一个类不能扩展多个父类。
  • 接口允许使用非分层框架。如果类具有许多接口的功能,则可以全部实现它们。如果没有接口,则必须为每个属性组合使用一个类来创建一个庞大的类层次结构,从而导致组合爆炸。
  • 接口可增强安全功能。我们可以使用Decorator模式(稳健而灵活的设计)来创建包装器类。包装器类实现并包含相同的接口,将某些功能转发到现有方法,同时向其他方法添加专门的行为。我们不能使用抽象方法来执行此操作-必须改为使用继承,因为继承更加脆弱。

抽象类提供基本实现的优势又如何呢?我们可以为每个接口提供一个抽象的骨架实现类。这结合了接口和抽象类的优点。框架实现提供了实现帮助,而没有施加抽象类用作类型定义时所施加的严格约束。例如,Collections Framework使用接口定义类型,并为每个接口提供一个基本实现。

回答

在我看来,我们经常看到这种情况,因为这是一种非常好的做法,经常会在错误的情况下应用。

相对于抽象类,接口有很多优点:

  • 我们可以切换实现而无需重新构建依赖于接口的代码。这对以下有用:代理类,依赖项注入,AOP等。
  • 我们可以在代码中将API与实现分开。这样做很好,因为在更改将影响其他模块的代码时,它会很明显。
  • 它使开发人员可以编写依赖于代码的代码,从而轻松地模拟API以进行测试。

在处理代码模块时,我们将从接口中获得最大的优势。但是,没有容易的规则来确定模块边界应该在哪里。因此,这种最佳做法很容易过度使用,尤其是在首次设计某些软件时。

回答

怎么会?

因为那是所有书所说的。像GoF模式一样,许多人认为它是普遍良好的,并且从未考虑过它是否真的是正确的设计。

我们如何知道该接口内将发生的对象之间的所有关系?

你没有,那是一个问题。

如果
你已经知道那些关系,
那为什么不只是扩展一个摘要
班级?

不扩展抽象类的原因:

  • 我们会有截然不同的实现,并且要建立一个像样的基类太难了。
  • 我们需要将自己的唯一基类用于其他用途。

如果都不适用,请继续使用抽象类。这将为我们节省很多时间。

我们没有问的问题:

使用界面的不利之处是什么?

我们无法更改它们。与抽象类不同,接口是固定的。一旦使用了它,对其进行扩展将破坏代码,句点。

我真的需要吗?

大多数时候,不。在构建任何对象层次结构之前,请认真思考。 Java之类的语言中的一个大问题是,它使创建大量复杂对象层次结构变得太容易了。

考虑LameDuck从Duck继承的经典示例。听起来很简单,不是吗?

好吧,直到我们需要指出鸭子已经受伤并且现在已经la脚为止。或者表明la脚鸭已经he愈,可以再次行走。 Java不允许我们更改对象类型,因此使用子类型表示la行实际上是行不通的。

回答

原因之一是接口允许增长和扩展。举例来说,假设我们有一个将对象作为参数的方法,

公众无效饮料(一些咖啡)
{

}

现在,假设我们要使用完全相同的方法,但是要传递hotTea对象。好吧,你不能。我们只是将上述方法硬编码为仅使用咖啡对象。也许那是好事,也许那是坏事。上面的缺点是,当我们想要传递各种相关对象时,它严格地将我们锁定在一种类型的对象中。

使用IHotDrink这样的界面,

接口IHotDrink {}

并重写上述方法以使用接口而不是对象,

公共无效饮料(IHotDrink someDrink)
{

}

现在,我们可以传递实现IHotDrink接口的所有对象。当然,我们可以编写与使用不同的对象参数执行完全相同的操作的完全相同的方法,但是为什么呢?我们突然维护了肿的代码。

回答

为什么扩展是邪恶的。本文几乎是对所提问题的直接答案。我几乎不会想到实际上需要抽象类的情况,以及在很多情况下都不是一个好主意的情况。这并不意味着使用抽象类的实现是不好的,但是我们必须注意,不要使接口协定依赖于某些特定实现的构件(例如:Java中的Stack类)。

还有一件事:到处都没有接口,也没有必要,也是一种好习惯。通常,我们应该确定何时需要接口以及何时不需要接口。在理想情况下,大多数情况下,第二种情况应作为最终课程实施。

回答

Programming to an interface means respecting the "contract" created by 
  using that interface

这是关于接口的最容易误解的事情。

无法通过接口强制执行任何此类合同。根据定义,接口根本无法指定任何行为。类是行为发生的地方。

这种错误的信念非常普遍,以至于被许多人认为是传统观念。但是,这是错误的。

因此,OP中的此语句

Almost every Java book I read talks about using the interface as a way 
  to share state and behavior between objects

只是不可能。接口既没有状态也没有行为。他们可以定义实现类必须提供的属性,但这要尽可能地接近。我们不能使用接口共享行为。

我们可以假设人们将实现一个接口以提供其方法名称所隐含的某种行为,但这并不是同一件事。而且,何时调用此类方法也没有任何限制(例如,应在停止之前调用Start)。

这个说法

Required for GoF type patterns, such as the visitor pattern

也不正确。 GoF书完全使用零接口,因为它们不是当时使用的语言的功能。尽管有些模式可以使用它们,但是这些模式都不需要接口。 IMO,观察者模式是一种接口可以在其中扮演更优雅角色的模式(尽管该模式现在通常使用事件来实现)。在Visitor模式中,几乎总是需要一种基本的Visitor类,该类为每种类型的受访节点(IME)实现默认行为。

我个人认为问题的答案有三点:

  • 许多人将界面视为灵丹妙药(这些人通常在"合同"误解下工作,或者认为界面神奇地解耦了他们的代码)
  • Java人员非常关注使用框架,其中许多(正确地)需要类来实现其接口
  • 在引入泛型和注释(C#中的属性)之前,接口是执行某些操作的最佳方法。

接口是一种非常有用的语言功能,但被滥用。症状包括:

  • 一个接口只能由一个类实现
  • 一个类实现多个接口。通常被吹捧为接口的优点,通常意味着所讨论的类违反了关注点分离的原则。
  • 接口有一个继承层次结构(通常由类的层次结构反映)。我们首先要通过使用接口来避免这种情况。对于类和接口,太多的继承都是一件坏事。

所有这些都是代码气味,IMO。

回答

一切都与编码前的设计有关。

如果我们在指定接口后不了解两个对象之间的所有关系,那么定义接口的工作就很糟糕-相对容易修复。

如果我们直接研究编码并中途完成,则会丢失一些很难修复的东西。

回答

这里有一些很好的答案,但是如果我们正在寻找一个具体的原因,除了单元测试之外别无所求。

考虑我们要在业务逻辑中测试一种方法,该方法检索发生事务的区域的当前税率。为此,业务逻辑类必须通过存储库与数据库对话:

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

在整个代码中,请使用IRepository类型而不是TaxRateRepository。

该存储库具有一个非公共的构造函数,以鼓励用户(开发人员)使用工厂实例化该存储库:

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

工厂是唯一直接引用TaxRateRepository类的地方。

因此,在此示例中,我们需要一些支持类:

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

还有一个其他的IRepository实现样机:

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

因为实时代码(业务类)使用工厂来获取存储库,所以在单元测试中,我们要为TaxRateRepository插入MockRepository。进行替换后,我们可以对返回值进行硬编码,并使数据库不必要。

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

请记住,我们只想测试业务逻辑方法,而不要测试存储库,数据库,连接字符串等。每种测试都有不同的测试。通过这种方式,我们可以完全隔离正在测试的代码。

附带的好处是,我们也可以在没有数据库连接的情况下运行单元测试,这使其速度更快,更可移植(请考虑在远程位置的多开发人员团队)。

另一个附带的好处是,我们可以将测试驱动开发(TDD)流程用于开发的实施阶段。我不严格使用TDD,而是将TDD和老式编码结合使用。

回答

我们可以从perl / python / ruby​​的角度看到这一点:

  • 当我们将对象作为参数传递给方法时,如果我们不传递其类型,则只知道它必须响应某些方法

我认为以Java接口作为类比可以最好地解释这一点。我们实际上并没有传递类型,而只是传递了对方法做出响应的东西(如果可以的话,则是特征)。

回答

我认为在Java中使用接口的主要原因是对单一继承的限制。在许多情况下,这会导致不必要的复杂化和代码重复。看一下Scala中的Traits:http://www.scala-lang.org/node/126 Traits是一种特殊的抽象类,但是一个类可以扩展其中的许多类。