接口到处都是? - 最佳实践

关于在OOP中正确使用接口的工作正在讨论中。我受过教育,并且总是在前提下工作,界面先于具体结果,所有方法都应该处理合同。这与我脱钩101。

我发现应用这种模式普遍教导初级开发人员绳索,让我在未来取得成功(“哦!所有这些都紧密耦合!我不能使用任何这个!”),并且花费很少的时间。理解我们总是一直在处理合同是很简单(也是值得的)。

另一位研究员表示,只有在有多个实现时才应用抽象,并且一直这样做会让团队感到困惑。对我来说,我并不在意; D一开始只会让人感到困惑,很快就会成为第二天性,对我而言,这只是正确的建构方式。

但我想伸出手,看看是否有人可以提供一些参考/专家文本来解释为什么或为什么不。

2
我认为“抽象”在软件工程中有几个不同的含义。对我来说,接口与抽象无关。抽象与实现细节无关。它与设计有关。概念化。您可以使用或不使用接口实现的功能。
额外 作者 glacier,
从单个类创建接口的主要危险在于,你会发现它非常错误。想象一下,如果古代澳大利亚人定义了IMammal的界面。他们都有一个必需的 sizeOfPouch()方法......
额外 作者 MrG,
解释为什么Y完全受益于抽象类。抽象是完全相同的吗?没有代码更改。在数百万个地方有构造函数调用,请记住类不必提供构造函数吗?依赖注入远远超过C#和Java。
额外 作者 Frank Hileman,
我没有看到你的逻辑。如果API相同,则无论使用接口,具体类还是抽象类,交换都是无缝的。唯一的区别是构造,可以通过许多不同的方式隔离,而不仅仅是通过DI。
额外 作者 Frank Hileman,
只有在同一进程中同时使用两个服务(多重继承)时,InterfaceB才会提供值。但是你也可以通过使用基类获得这种好处。
额外 作者 Frank Hileman,
如果将一种类型替换为另一种类型,则可以使用基类,也可以更改原始类型。任何OO语言都可以。您描述的情况是您要么同时需要两种类型,要么有不同的组织创作组件(插件)。
额外 作者 Frank Hileman,
您没有就此声明提供任何解释。我从用户的角度告诉你,接口(专用类)和类之间的唯一区别是构造函数。你必须举一个例子来修改这个花费数百万美元。
额外 作者 Frank Hileman,
通常,昂贵的更改不是对类型的名称更改,而是对API的侵入式更改。 DI和接口无济于事。您的方案与保存工作无关。
额外 作者 Frank Hileman,
我花了很多时间去除多余的接口,这就是为什么我将它标记为货物崇拜设计。唯一的好处是增加开发软件的成本 - 也许对承包商和员工有利,但对于为开发付费的人却不是。
额外 作者 Frank Hileman,
@froodley正确,但这种语言中的类既是抽象(提供的操作),也是一个文本文档中的实现。即而在其他语言中,它们可能是分开的,这种语言允许您将它们组合成一个单元。他们仍然可以分开......一个班级可以让成员无法实现;如果一切都是这样,它相当于一个接口,没有多重继承的优点。
额外 作者 Frank Hileman,
在您所暗示的语言类型中,类是抽象的抽象和实现。类提供“接口”(API)。 interface关键字使您可以指定没有实现的类,以解决语言限制。从用户的角度来看,类和接口都没有比另一个更好或更抽象。接口可能使构造更加困难。
额外 作者 Frank Hileman,
看起来你通常不会使用计算机科学中使用的相同术语。类是抽象,甚至是整数,浮点数也是抽象。接口不会增加“抽象”:更抽象的数据类型是更通用的类型,并且在类型层次结构中更高;从抽象的角度来看,实现位置是无关紧要的。
额外 作者 Frank Hileman,
@Meo我同意,这些语言的接口仅用于多重继承。但我不喜欢在问题中滥用“抽象”。
额外 作者 Frank Hileman,
关于原始帖子中的“解耦101”,与1:1类的公共成员匹配的接口不会解耦任何东西,因为只有一个实现(意思是,仍然耦合到单个类),并且因为修改公共签名时必须同时更新两者。同时,解耦应该耦合的东西没有任何好处 - 解耦是针对耦合是一个问题的情况。多余的界面是货物崇拜设计的一种形式。
额外 作者 Frank Hileman,
@froodley构造函数只能由类提供,但类也不必提供一个。是没有公共构造函数的类“具体”?用户不关心实现的位置;它们只适用于抽象。
额外 作者 Frank Hileman,
@froodley“具体”是什么意思?计算机科学,抽象数据类型和抽象级别中的术语与Java和C#中的关键字“abstract”无关。该关键字最好称为“未实现”,或者在类“不完整”的情况下。你的意思是提供构造函数的数据类型?抽象更正式地是一组操作以及这些操作的语义;在我们所说的语言中,我们只能在文档中指定语义。所有数据类型都是抽象 - 用户不会考虑实施。
额外 作者 Frank Hileman,
你是否在软件设计术语中混淆了“接口”和“具体化”与Java/C#中的“接口”和“类”?您可以编程到接口而无需实际编写单独的Java 接口
额外 作者 immibis,
我不同意具体的类是抽象的。你的意思是它是一块计算机代码而不是真正的汽车吗?我指的是抽象,即我正在处理纯粹抽象的东西,它应该具有一组字段和操作,一种商定的方式来讨论不依赖于接收任何特定实现的对象。除了一个纯粹的抽象类之外的任何东西都不再是抽象,除非它实际上不是一个汽车。
额外 作者 Dave Lau,
根据定义,抽象不能是实现。
额外 作者 Dave Lau,
替代方案:服务X直接在任何地方注入。它根本不再有效。每个下游消费者都必须改变,一些在其他公司,一些是由离开公司的开发人员开发的软件。
额外 作者 Dave Lau,
再说一遍,如果你曾经花了多年时间和数百万人解决某人的YAGNI slop编程问题,你可能会明白为什么我不同意。
额外 作者 Dave Lau,
至于货物崇拜,我真的不同意它没有任何意义。我相信你正确处理抽象而不是特定的实现,而你所说的就是说使URL可配置而不是硬编码是因为它们可能不会改变。
额外 作者 Dave Lau,
如果你曾经错误地认为你的软件被认为是紧密耦合的,并且它将花费数百万美元来解耦,你就会明白我拒绝开发紧密耦合的软件。即使只有一个实现,处理接口肯定会使您的软件解耦。任何使用者都可以将您的库替换为另一个提供任何实现或外观的库,而该实现或外观提供相同的接口。我真的不知道你来自哪里;这对我来说似乎是懒惰和邋。
额外 作者 Dave Lau,
计算机中的所有东西都是抽象的。
额外 作者 Dave Lau,
根据定义,接口比具体类更抽象。
额外 作者 Dave Lau,
它是对象的概念表示,而不是该概念表示的特定实现。
额外 作者 Dave Lau,
如果我说你必须给我一个ServiceX对象,我不能给你一个ServiceY对象。编译器会阻止它。如果我说你必须给我一个InterfaceB对象,你可以给我一个。
额外 作者 Dave Lau,
在更换现在拥有数十名消费者的图书馆时,它最初会在最初节省数百万美元的同时增加成本。
额外 作者 Dave Lau,
如果将一种类型替换为另一种类型,除了在配置中提供注入器的工厂外,我什么都不做。它会旋转另一个实现interfaceB的类,根本不会更改任何代码。
额外 作者 Dave Lau,
服务X已被弃用,因为后端系统已被替换。创建服务Y是为了达到同样的目的。我们为依赖注入器提供服务Z,并且期望只有满足接口B的服务的多个库和应用程序中的子项完全不受影响。
额外 作者 Dave Lau,
你用什么语言工作?您不能将ServiceY注入到期望使用任何强类型语言的ServiceX的类中。
额外 作者 Dave Lau,
我们提供服务Y *
额外 作者 Dave Lau,
@FrankHileman确切地说,OP基本上为一个实现制作2个接口,而它应该是相反的 - 1个接口用于2个以上的实现。
额外 作者 Michael,
语言@froodley你工作吗?
额外 作者 Michael,

5 答案

我甚至不想知道我是否使用了接口(关键字kind)。我的驾驶功能需要一辆车。具体与否是不是它的任何业务。

这就是为什么我对C#的 ICar 约定感到恼火。得到那些毫无意义的的声音。 Java并没有好多少。哦,按照惯例,我可以在源代码中命名接口,抽象类或具体类 Car ,但如果我从一个改为另一个,我必须重新编译使用它的所有内容!

我想要的只是表达类型系统中 drive()的需求。我不希望 drive()知道它在说什么,除了知道它是什么,它知道如何倾听。

顺便说一句,如果你有很好的测试,一个带有鸭子打字的语言可以免费提供所有这些。

但由于我必须使用这些语言,因此我倾向于使用角色接口。这意味着我不写驱动器(Car car)我编写驱动器(DriverControls driverControls)以及任何想通过接受转向,加速和中断消息的内容DriverControls 协议是免费的。

所以,如果这就是你所说的我和你在一起。如果你是那些坚持认为像 Car 这样的每个类都有 ICar 对象的狂热分子之一,你就可以去湖中跳。

6
额外
在Java中,您的DriverControls接口通常被命名为Drivable。
额外 作者 Alexei,
@froodley同意。这意味着真正的争论应该是当您首先使用具体类并稍后将它们重构为接口时可能导致的问题。这很难做到并尊重开放的封闭原则。更糟糕的是,了解 Car 的具体状态是多么容易传播。我探讨了在保持具体的这里的同时试图解决这个问题。它不漂亮。
额外 作者 candied_orange,
基本上是因为你给我的原因,我。我不想处理一些特定的东西,称为汽车,它以某种特定的方式做事。对我来说这只是一次性的垃圾代码。我想处理与合同的互动。我知道我对.drive()和.steer()的了解,并给我一个字符串,我不需要知道它是汽车还是船。
额外 作者 Dave Lau,

你错过了一大块难题:

在任何地方使用界面都会使您的代码更难以导航

假设您有一个包含数百个类的大型代码库,这些类已经发展了多年。

通常,当您想知道方法中发生了什么时,只需单击它,IDE就会跳转到它的代码......也就是说,如果它是一个类。如果它是一个接口,你将跳转到界面,然后你必须找出实例化的类,这可能需要一段时间,然后去那里,然后进入另一个方法并再次登陆界面,所以你必须再次弄清楚实际上是什么,这可能需要一段时间,等等。

基本上,由于超级接口,导航代码变得越来越令人沮丧。

我的经验法则是:当你确实有多个不同的实现时,使用一个接口,而不仅仅是为了它


编辑:

我的“更难”可能是夸大其词。我应该写“更难”或“不方便”。

为了说明这一点,在日食中。使用类中的方法:

  • Ctrl +单击方法内的跳转(单击1次)。

使用接口的方法,它将是:

  • Ctrl +单击
  • 继续前进
  • 右键单击interace name
  • open type explorer
  • 选择课程
  • 开放大纲
  • 转到方法

...所以,是的,你可以通过良好的IDE支持来实现它,但它仍然非常烦人。

3
额外
嗨!好起来吧! ;)我的“更难”可能是夸大了。我应该写“更难”或“不方便”。为了说明这一点,使用eclipse中的类方法,在方法内按Ctrl +单击跳转(单击1次)。使用界面,它将是Ctrl +单击,移动到顶部,右键单击interace名称,打开类型资源管理器,选择类,打开轮廓,转到方法。所以,是的,你可以通过IDE支持来实现它,但它仍然是7次点击左右,这仍然非常烦人。
额外 作者 TLS,
@froodley:恕我直言,系统地添加界面只是添加无用的噪音。它不会改善解耦,也不会确保底层实现尊重合同。它只是增加了不必要的间接,噪音和惹恼了经验丰富的开发人员。有很多情况下接口是有用的,但系统地应用恕我直言愚蠢。
额外 作者 TLS,
@froodley无论lib是使用接口还是类,我都将自己与自己联系起来。我没关系。如果我调用 myLib.doThat(someArg),那么 myLib 是一个接口还是一个类并不重要。如果我真的要保持某种程度上独立于它,我将不得不围绕它构建一个包装器。但是,在大多数情况下,添加这样的抽象更具障碍而不是有用。切换依赖项是非常罕见的,如果你这样做,你通常会注意到你的包装器不能很好地适应其他lib,导致两个地方而不是一个地方的变化。
额外 作者 TLS,
@froodley取代依赖:每隔几年一次。导航/理解/重构代码:每一天。如果您喜欢它,请将界面放在任何地方,我不在乎,但从长远来看,请保持您的东西。
额外 作者 TLS,
@Meo我们在这里不是在谈论 Runnable ,这是一个错误的组成示例,类似于“如果你想找到 Object 的特定子类,该怎么办”。找到 MyInterface 的具体实现没有问题。当然,除非您决定让所有自定义类无缘无故地实现 MyInterface ,否则会涉及更大的问题。
额外 作者 Kayaman,
@Meo只有在规则是“每个类必须实现一个接口”的情况下才是现实的,没有关于哪个接口的任何条件。在专业环境中我不太可能看到这种情况。我的意思是,如果我们在恶劣的环境中工作,接口将不会有任何好处,因为有人可能有意或无意地编写不正确的实现。这是否意味着接口是无用的,因为它们不能保证实现是正确的?
额外 作者 Kayaman,
任何值得盐的IDE都知道如何跳转到实现。除非您使用的是差工具,否则这绝不是一个有效的观点。
额外 作者 Kayaman,
@Meo我绝不提倡每个类都需要实现一个接口。对于任何初级开发人员,应该明确哪种类保证接口(例如服务)和不接受什么类(例如实体,值对象)。如果没有架构或指导方针,那么无论如何代码库都会处于糟糕的状态并不重要。
额外 作者 Kayaman,
@Meo我认为这篇文章是在我的评论之后编辑的。我不提倡每个类的接口,我不提倡一个如此糟糕的架构,以至于你无法跟踪正在发生的事情。
额外 作者 Kayaman,
@Meo不要在我嘴里说话然后说“错”。我说没有理由避免使用单个实现的接口。我说没有所有类都实现了一个接口,因为那只是愚蠢的(特别是如果它导致所有类由于懒惰而实现 Runnable )。我说在做出设计决策时会使用你的经验(或者让经验更丰富的人)。我不确定你所谈论的成本是开发成本还是运行时成本,但无论如何都可以忽略不计。这就像避免接口的微优化一样。
额外 作者 Kayaman,
这绝对重要。如果您可以使用实现相同签名的someLib替换myLib而不更改代码,更不用说花费200万美元来替换您对myLib所做的数千个硬引用,那么我给你的是一种语言而不是一台机器你是与永远联系在一起有更高,更难和更昂贵的解耦区域来实现更松散的关联,但如果你只处理抽象,你的软件默认是非常松散耦合的,并且可以在不对消费代码进行任何更改的情况下进行替换。
额外 作者 Dave Lau,
这更难。更难以将密码硬编码到代码库中或将业务逻辑放入模型中。这很严格。这对我来说是“你想要一份工作,还是一份工作”。
额外 作者 Dave Lau,
它根本没用。如果我交给你一个只处理具体问题的课程或图书馆,你就会把自己与我的工作紧密结合起来,而且我们都开箱即用。根据我的经验,它让不耐烦的初级开发人员感到厌烦,而经验丰富的开发人员想要处理合同并意识到自己不会自杀。
额外 作者 Dave Lau,
@Kayaman我们会看到它是否被编辑过,但事实并非如此。
额外 作者 Michael,
@Kayaman我指的是你的第一条评论。事实是“由于超级接口,导航代码变得越来越令人沮丧。”没有工具可以改变这一点。你写道,这不是一个有效的观点,这是错误的。
额外 作者 Michael,
@Kayaman不,你说的是从“超级接口”跳到实现或返回是没有成本的。错误。
额外 作者 Michael,
@Kayaman如果每个类都有自己的接口,或者需要最紧密的接口,那么我同意你的看法IDE应该处理得很好。你知道什么是最紧密的接口?班级本身。期望过度使用的接口没有导航成本只是天真。
额外 作者 Michael,
@Kayaman如果你过度概括一切,那么你将不可避免地面临同样的问题。如果有一个接口是常态,有人会迟早节省时间并使用现有的不应该的时间,然后IDE将提供不合逻辑的实现。
额外 作者 Michael,
@Kayaman如果您使用糟糕的抽象,请尝试通过其Runnable接口找到一个特定的实现。
额外 作者 Michael,

我同意你的同事。

创建良好的抽象很难。就像,真的,真的很难!

抽象为软件增添了复杂性。它们使得导航代码和推理它变得更加困难。它们使事情变得更难以改变,因为抽象的变化通常需要在多个地方进行更改。

只复制类所具有的方法的接口不是一个好的抽象。只有你可以告诉你具有良好抽象的时间是你可以想象(或实际上)有多种不同的实现。

每当你创建一个抽象时,你需要问自己一个问题“它是否真的值得使代码复杂化以便我可以将这些部分分离出去?”

3
额外
创建良好的抽象很难,因为它们消除了复杂性。糟糕的抽象反而增加了它。
额外 作者 Shizam,
@froodley如果你的接口和类具有相同的签名,除了构造之外,你没有解耦任何东西。
额外 作者 Frank Hileman,
创建出色的抽象很难,创建用于解耦目的的抽象并不难。如果我的消费者可以用另一个可以提供相同签名的模块或类替换我的模块或类,我觉得我已经完成了正确的工作,并且他们与我所做的并不紧密相关。
额外 作者 Dave Lau,

解耦本身并不是一件好事。你需要它只是一件好事,而其他地方都是坏事。如果你在一个应该具有高内聚力的地方使用界面,那么你做错了。

每个界面都有成本和价值,如果你不考虑它并盲目地将它们制作到各处,那么你就是这么做错了。

1
额外
@froodley契约是一组操作和语义。从用户的角度来看,接口只是一组操作,一组签名,看起来与一个类完全相同。据我所知,你称之为“具体”的是类中构造函数的存在。解耦意味着您可以独立于实现更改API。类可以做到这一点,接口可以做到这一点,但真正的解耦意味着使用多个接口或动态语言。
额外 作者 Frank Hileman,
@froodley从用户的角度来看,你所谓的“具体”类和“接口”之间唯一的依赖关系是构造函数依赖。您可以构造一个提供构造函数的类。您不能构造不提供构造函数的专用类,例如接口。从用户的角度来看,接口和类是相同的,否则。
额外 作者 Frank Hileman,
这是你的看法,我的相反。当您处理密切相关的合同时,仍然可以实现模块内部的高内聚。我认为基于合同的构建始终消除任何模糊性并强制执行正确的思维,同时导致高度可交换的代码库“便宜”,而后者试图解耦时,许多消费者已经依赖于您结核。但同样,我正在寻找一些关于这个主题的阅读材料的参考。
额外 作者 Dave Lau,
接口是合同。类是生​​成该合同的一些实现的机器。这些非常非常不同。我完全不相信混凝土机器的交易与合同交易是一样的。
额外 作者 Dave Lau,

我编写数据业务应用程序

这些通常不适用于经典的“真实”OOP,其中对象实现其自己的行为,因为所有业务规则都依赖于当前连接的用户等等。相反,我有着名的贫血模型+服务,因为除了一些独立的技术组件外,我没有找到更适合我需要的东西。

我的服务总是有接口,因为即使我有一个实现,也有一段时间我想要嘲笑它们。我抽象了一点,但不是太多,因为在我这样做的时候,我不知道我需要多少以及如何正确地做到这一点,而且我的时间更好,因为浪费时间(YAGNI/KISS) )。

当然,因为我只从一个实现开始,我的界面几乎是我的实现需要公开的副本,但这是故意的。如果我需要另一个实现,它本质上会迫使我将我的接口重构为更通用的接口,并且我会在需要的时候执行它。

注意:我使用常规名称作为我使用的服务的接口(没有“我”或其他),因为这些服务的消费者真的不关心他们是@candied_orange所说的接口

0
额外
在Java中你可以在没有接口的情况下模拟它们(使用Mockito等),除非你想要手动实现类。
额外 作者 Michael,