关于语言不可知:为什么大多数系统架构师坚持首先对接口进行编程?

关于语言不可知:为什么大多数系统架构师坚持首先对接口进行编程?

Why do most system architects insist on first programming to an interface?

几乎所有我读过的Java书籍都谈到使用接口作为一种共享对象和行为的方式,当第一个"构建"的对象似乎没有共享关系时。

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


编程到接口意味着要尊重使用该接口创建的"契约"。因此,如果您的IPoweredByMotor接口有一个start()方法,未来实现该接口的类(无论是MotorizedWheelChairAutomobileSmoothieMaker在实现该接口的方法时,增加了系统的灵活性,因为一段代码可以启动许多不同类型的东西的马达,因为所有这些东西都是需要知道的一段代码是它们对start()的响应。不管他们怎么开始,只要他们必须开始。


好问题。我将用有效Java来引用Josh Bloch,他编写(第16条)为什么更喜欢使用抽象类上的接口。顺便说一句,如果你没有这本书,我强烈推荐它!以下是他所说的总结:

  • 现有的类可以很容易地进行改装以实现新的接口。您所需要做的就是实现接口并添加所需的方法。现有类不能很容易地进行改装以扩展新的抽象类。
  • 接口是定义混合的理想选择。混合接口允许类声明附加的可选行为(例如,可比较的行为)。它允许可选功能与主要功能混合在一起。抽象类不能定义混合类型——类不能扩展多个父类。
  • 接口允许非层次框架。如果有一个类具有许多接口的功能,那么它可以实现所有接口。如果没有接口,就必须为每个属性组合创建一个膨胀的类层次结构和一个类,从而导致组合爆炸。
  • 接口支持安全功能增强。您可以使用decorator模式(一种健壮且灵活的设计)创建包装类。包装类实现并包含相同的接口,将一些功能转发给现有方法,同时向其他方法添加专门的行为。不能用抽象方法来实现这一点——必须使用继承,而继承更脆弱。
  • 抽象类提供基本实现的优势是什么?您可以为每个接口提供一个抽象骨架实现类。这结合了接口和抽象类的优点。骨架实现提供实现帮助,而不施加抽象类作为类型定义时强制实施的严格约束。例如,collections框架使用接口定义类型,并为每个接口提供框架实现。


    接口编程提供了几个好处:

  • GOF类型模式(如访客模式)需要

  • 允许替代实现。例如,一个抽象正在使用的数据库引擎的接口可能存在多个数据访问对象实现(accountdaomysql和accountdaomoracle都可以实现accountdao)

  • 一个类可以实现多个接口。Java不允许对具体类进行多重继承。

  • 抽象实现细节。接口可能只包括公共API方法,隐藏实现细节。好处包括一个清晰记录的公共API和有良好记录的合同。

  • 被现代依赖注入框架大量使用,如http://www.springframework.org/。

  • 在爪哇中,接口可以用来创建动态代理——HTTP://JavaSun.COM/J2SE/1.5.0/DOCS/API/Java/Lang/Realth/PROXY.HTML。这可以非常有效地用于Spring等框架,以执行面向方面的编程。方面可以在不直接向这些类添加Java代码的情况下向类添加非常有用的功能。此功能的示例包括日志记录、审核、性能监视、事务划分等。http://static.springframework.org/spring/docs/2.5.x/reference/aop.html。

  • 模拟实现、单元测试——当依赖类是接口的实现时,可以编写模拟类来实现这些接口。模拟类可以用来促进单元测试。


  • 我认为抽象类被开发人员抛弃的原因之一可能是一个误解。

    当"四人帮"写下:

    Program to an interface not an implementation.

    没有Java或C接口这样的东西。他们讨论的是面向对象的接口概念,每个类都有。埃里希·伽玛在这次采访中提到过。

    我认为机械地遵循所有的规则和原则而不思考会导致难以阅读、导航、理解和维护代码库。记住:最简单的事情可能是可行的。


    怎么会?

    因为所有的书都这么说。像GOF模式一样,许多人认为它是一个普遍的好模式,从不考虑它是否真的是正确的设计。

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

    你没有,这是个问题。

    如果你已经知道这些关系了,那为什么不把摘要班级?

    不扩展抽象类的原因:

  • 您有完全不同的实现,创建一个合适的基类太难了。
  • 你需要烧掉你的一个并且只为别的东西烧掉基类。
  • 如果两者都不适用,请继续使用抽象类。这会节省你很多时间。

    你没有问的问题:

    使用接口有哪些缺点?

    你不能改变它们。与抽象类不同,接口是用石头设置的。一旦您使用了一个,扩展它将破坏代码,句点。

    我真的需要吗?

    大多数情况下,不。在构建任何对象层次结构之前,请仔细考虑。像Java这样的语言的一个大问题是,它太容易创建大规模复杂的对象层次结构。

    考虑一下跛脚鸭继承自鸭的经典例子。听起来很简单,不是吗?

    好吧,直到你需要指出鸭子受伤了,现在跛脚了。或者表示跛脚鸭已经痊愈,可以再次行走。Java不允许您更改对象类型,因此使用子类型表示跛行实际上不起作用。


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

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

    没有办法用接口强制执行任何这样的契约。根据定义,接口根本不能指定任何行为。类是行为发生的地方。

    这种错误的信仰如此普遍,以至于许多人都认为是传统的智慧。然而,这是错误的。

    所以这句话

    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中,观察者模式是一种接口可以发挥更优雅作用的模式(尽管现在通常使用事件实现该模式)。在访问者模式中,几乎总是需要为每种被访问节点类型实现默认行为的基本访问者类IME。

    我个人认为这个问题的答案是三倍:

  • 许多人认为接口是银弹(这些人通常是在"契约"的误解下工作,或者认为接口可以神奇地将代码解耦)

  • Java用户非常关注使用框架,其中许多(正确地)需要类来实现它们的接口。

  • 在引入泛型和注释(C中的属性)之前,接口是最好的方法。

  • 接口是一种非常有用的语言特性,但是被滥用了很多。症状包括:

  • 接口只能由一个类实现

  • 类实现多个接口。通常被吹捧为接口的优势,这通常意味着所讨论的类违反了关注点分离的原则。

  • 存在接口的继承层次结构(通常由类的层次结构镜像)。这是您试图通过首先使用接口来避免的情况。对于类和接口来说,太多继承是一件坏事。

  • 在我看来,所有这些都是代码气味。


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

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

    这个概念的一个很好的应用是抽象工厂模式。在维基百科的例子中,guifactory接口生成按钮接口。混凝土工厂可以是winfactory(生产winbutton)或osxfactory(生产osxbutton)。想象一下,如果您正在编写一个GUI应用程序,您必须查看OldButton类的所有实例,并将它们更改为WinButton。接下来的一年,您需要添加OSXButton版本。


    在我看来,你经常看到这一点,因为这是一个非常好的实践,经常应用在错误的情况下。

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

    • 您可以在不重新构建依赖于接口的代码的情况下切换实现。这对于:代理类、依赖注入、AOP等很有用。
    • 您可以在代码中分离API和实现。这是很好的,因为当您更改将影响其他模块的代码时,这会使它变得很明显。
    • 它允许开发人员编写依赖于您的代码的代码,以便轻松模拟您的API进行测试。

    在处理代码模块时,您可以从接口中获得最大的优势。然而,确定模块边界的位置并不是一个简单的规则。所以这个最佳实践很容易被过度使用,特别是在最初设计一些软件时。


    我假设(使用@eed3s9n)它是为了促进松耦合。此外,没有接口,单元测试变得更加困难,因为您不能模拟对象。


    为什么延伸是邪恶的。这篇文章几乎是对所问问题的直接回答。我几乎想不出在什么情况下你实际上需要一个抽象类,以及很多情况下它是一个坏主意。这并不意味着使用抽象类的实现是不好的,但是您必须要小心,这样就不会使接口契约依赖于一些特定实现的工件(例如:Java中的堆栈类)。

    还有一件事:在任何地方都没有接口是不必要的,也没有良好的实践。通常,您应该确定何时需要接口,何时不需要。在一个理想的世界中,第二个案例大多数时候都应该作为最后一个类来实现。


    这里有一些很好的答案,但是如果您正在寻找一个具体的原因,那么只需看看单元测试。

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

    1
    2
    3
    4
    5
    6
    7
    8
    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。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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类的地方。

    因此,对于这个示例,您需要一些支持类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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的其他实现——实体模型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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。一旦进行了替换,就可以硬编码返回值并使数据库不必要。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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和旧的学校代码混合使用。


    一个原因是接口允许增长和可扩展性。例如,假设有一个方法将对象作为参数,

    公共空饮(咖啡饮料){

    }

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

    通过使用界面,比如说ihotdrink,

    接口IHotdrink

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

    公共禁酒{

    }

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


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


    这一切都是关于编码前的设计。

    如果您在指定接口之后不知道两个对象之间的所有关系,那么您在定义接口方面做得很差——这相对容易修复。

    如果你已经直接潜入了编码领域,并且意识到你已经错过了一些很难解决的事情。


    在某种意义上,我认为你的问题可以简单地归结为,"为什么使用接口而不是抽象类?"从技术上讲,您可以实现与两者的松散耦合——底层实现仍然不暴露于调用代码,并且您可以使用抽象工厂模式返回底层实现(接口实现与抽象类扩展),以提高设计的灵活性。事实上,您可能会争辩说抽象类给了您更多的东西,因为它们允许您同时需要实现来满足您的代码("您必须实现start()")和提供默认实现("我有一个标准的paint(),如果您愿意,您可以重写")——对于接口,必须提供实现,随着时间的推移,实现可以通过接口更改导致脆弱的继承问题。

    但是,从根本上讲,我使用接口主要是由于Java的单继承限制。如果我的实现必须继承一个抽象类来调用代码,这意味着我将失去从其他类继承的灵活性,即使这可能更有意义(例如代码重用或对象层次结构)。


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

    • 当你将一个对象作为参数传递给一个方法时,你不会传递它的类型,你只知道它必须响应一些方法。

    我认为把Java接口作为类比最好解释这一点。你不是真的通过一个类型,你只是传递一些响应方法的东西(一个特性,如果你愿意的话)。


    推荐阅读

      linux无效对象的命令?

      linux无效对象的命令?,软件,系统,单位,网络,管理,术语,检测,电脑,环境,风险,l

      linux命令是什么语言?

      linux命令是什么语言?,系统,环境,代码,传播,管理,语言,操作系统,源码,自由,

      linux命令行图形编程?

      linux命令行图形编程?,系统,不了,情况,密码,工具,地方,百度,管理,图形界面,

      linux改语言命令行?

      linux改语言命令行?,系统,环境,工具,密码,概念,地方,软件,通信,管理,国际,lin

      linux命令行c语言?

      linux命令行c语言?,代码,系统,工具,环境,工作,保险,发行,命令,文件,终端,linu

      c语言在linux命令?

      c语言在linux命令?,系统,工作,管理,命令,保险,基础,环境,信息,文件,语言,linu

      linux终端命令行编程?

      linux终端命令行编程?,系统,工作,命令,终端,概念,时间,第一,代码,发行,地方,L

      linux查看接口命令?

      linux查看接口命令?,地址,系统,标准,命令,管理,工作,文件,端口,目录,路径,lin

      linux编程调用命令?

      linux编程调用命令?,系统,标准,管理,工作,基础知识,情况,环境,设备,基础,首

      linux编写c语言命令?

      linux编写c语言命令?,系统,基础,环境,代码,盘面,保险,百度,情况,数据,工具,在

      linux编程所需的命令?

      linux编程所需的命令?,工作,地址,档案,系统,命令,管理,标准,信息,目录,文件,L

      使用linux命令调接口?

      使用linux命令调接口?,网络,系统,地址,信息,工具,情况,服务,灵活,电脑,名称,

      linux命令行编程乱码?

      linux命令行编程乱码?,环境,统一,乱码,中文,状态,软件,数据,系统,字符集,文

      linux命令添加接口?

      linux命令添加接口?,地址,网络,名称,系统,工具,设备,信息,服务,中心,密码,Lin

      linux编程c命令符?

      linux编程c命令符?,工具,代码,系统,保险,环境,文件,程序,命令,终端,编辑,到底

      linux改变语言命令?

      linux改变语言命令?,系统,管理,网上,官方网站,情况,服务,中文,语言,命令,终

      c语言编译linux命令?

      c语言编译linux命令?,代码,工具,环境,系统,基础,保险,百度,语言,源程序,文件

      linux常用编程命令?

      linux常用编程命令?,基础,基础知识,系统,管理,概念,在线,发展,设备,设计,名

      linux接口模式命令?

      linux接口模式命令?,设备,系统,信息,网络,工具,端口,服务,数字,地址,情况,Lin

      linux命令行界面编程?

      linux命令行界面编程?,系统,环境,代码,密码,命令,终端,首页,软件,工作,电脑,l