接口隔离原理的两个矛盾定义–哪个正确?


14

在阅读有关ISP的文章时,似乎对ISP有两个相互矛盾的定义:

根据第一定义(见123),ISP指出,实现该接口的类不应该被迫实施的功能,他们并不需要。因此,胖界面IFat

interface IFat
{
     void A();
     void B();
     void C();
     void D();
}

class MyClass: IFat
{ ... }

应该分成较小的接口,ISmall_1并且ISmall_2

interface ISmall_1
{
     void A();
     void B();
}

interface ISmall_2
{
     void C();
     void D();
}

class MyClass:ISmall_2
{ ... }

因为这样我MyClass能够实现只需要(方法D()C()),而无需被迫还提供了虚拟实现A()B()并且C()

但根据第二个定义(见12通过纳扎尔Merza答案),ISP指出MyClient调用方法MyService不应该知道的有关方法MyService,它不需要。换句话说,如果MyClient只需要的功能C()D(),然后代替

class MyService 
{
    public void A();
    public void B();
    public void C();
    public void D();
}

/*client code*/      
MyService service = ...;
service.C(); 
service.D();

我们应该将MyService's方法隔离到特定客户端的接口中:

public interface ISmall_1
{
     void A();
     void B();
}

public interface ISmall_2
{
     void C();
     void D();
}

class MyService:ISmall_1, ISmall_2 
{ ... }

/*client code*/
ISmall_2 service = ...;
service.C(); 
service.D();

因此,在前者的定义下,ISP的目标是“ 使实现IFat接口的类的寿命变得更容易 ”,而在后者的定义下,ISP的目标是“ 使客户机调用MyService的方法的寿命变得更容易 ”。

ISP的两个不同定义中的哪一个实际上是正确的?

@MARJAN VENEMA

1。

因此,当您要将IFat拆分为较小的接口时,应根据成员的内聚性决定在哪种方法最终决定ISmallinterface。

虽然将聚合方法放在同一个接口中是有意义的,但我认为使用ISP模式时,客户端的需求要优先于接口的“聚合性”。换句话说,我认为与ISP一起,我们应该将特定客户端所需的那些方法集中在同一个接口中,即使这意味着将那些出于凝聚力也应该放在同一个接口中的方法留在该接口之外?

因此,如果有大量的客户,将永远只能需要通话CutGreens,但不能也GrillMeat,然后坚持ISP模式,我们应该只把CutGreens里面ICook,但也没有GrillMeat,尽管这两种方法都具有高度凝聚力?

2。

我认为您的困惑源于第一个定义中的一个隐含假设:实施类已经遵循单一责任原则。

通过“实现不遵循SRP的类”,您是指实现的那些类IFat还是实现ISmall_1/的类ISmall_2。我假设您是指实现的类IFat?如果是这样,为什么您认为他们还没有遵循SRP?

谢谢


4
为什么不能同时使用相同的原则来定义多个定义?
Bobson

5
这些定义并不矛盾。
Mike Partridge

1
当然,客户的需求不会优先于接口的内聚性。您可以采用这种“规则”方式,并最终在各处使用单一方法接口,这绝对是毫无意义的。停止遵循规则,开始思考创建这些规则的目标。对于“不遵循SRP的类”,我不是在谈论示例中的任何特定类,也不是说它们还没有遵循SRP。再读一次。如果接口不遵循ISP并且类遵循SRP,则第一个定义只会导致拆分接口。
Marjan Venema 2013年

2
第二个定义与实现者无关。它从调用者的角度定义接口,并且不对实现者是否存在进行任何假设。它可能假定当您遵循ISP并实现这些接口时,您当然会在创建它们时遵循SRP。
Marjan Venema 2013年

2
您如何事先知道将存在哪些客户以及他们需要什么方法?你不能 您可以事先了解界面的凝聚力。
TulainsCórdova2013年

Answers:


6

两者都是正确的

以我的理解,ISP(接口隔离原理)的目的是保持接口小而集中:所有接口成员都应具有很高的凝聚力。两种定义都是为了避免“无所不包的大师”的接口。

接口隔离和SRP(单一职责原理)具有相同的目标:确保小型,高度凝聚力的软件组件。他们互相补充。界面隔离可确保界面小巧,集中且高度凝聚。遵循单一责任原则可确保班级规模小,重点突出且凝聚力强。

您提到的第一个定义集中在实现者上,第二个定义在客户端上。与@ user61852相反,我将其视为接口的用户/调用者,而不是实现者。

我认为您的困惑源于第一个定义中的一个隐含假设:实施类已经遵循单一责任原则。

对我来说,以客户端为接口的调用者的第二个定义是达到预期目标的更好方法。

隔离

在您的问题中,您声明:

因为通过这种方式,我的MyClass只能实现所需的方法(D()和C()),而不必强行为A(),B()和C()提供虚拟实现:

但这正在颠覆世界。

  • 实现接口的类不会在实现的接口中规定其需要的内容。
  • 接口指示实现类应提供的方法。
  • 接口的调用者实际上是那些决定它们需要接口提供哪些功能以及实现者应该提供什么功能的调用者。

因此,当您打算拆分IFat为较小的接口时,ISmall应根据成员的凝聚力决定哪种方法最终在哪个接口中确定。

考虑以下接口:

interface IEverythingButTheKitchenSink
{
     void DoDishes();
     void CleanSink();
     void CutGreens();
     void GrillMeat();
}

您要输入哪种方法ICook,为什么?您是否会因为碰巧拥有一个可以做到这一点的类以及一些其他事情而与其他任何方法都不一样而将CleanSink它们放在一起GrillMeat吗?还是将其分为两个更紧密的接口,例如:

interface IClean
{
     void DoDishes();
     void CleanSink();
}

interface ICook
{
     void CutGreens();
     void GrillMeat();
}

接口声明说明

接口定义最好应单独放在一个单独的单元中,但是如果绝对需要与调用者或实现者一起使用,则它实际上应该与调用者一起存在。否则,调用者将立即依赖于实现者,这完全破坏了接口的目的。另请参见:在与基类相同的文件中声明接口,这是一种好习惯吗?关于程序员,为什么我们应该将接口与使用它们的类而不是实现它们的类放在一起?在StackOverflow上。


1
您能看到我所做的更新吗?
EdvRusj 2013年

仅当您违反DIP(依赖性反转原则),并且根据DIP的要求,如果调用者的内部变量,参数,返回值等是类型ICook而不是type时SomeCookImplementor“调用者就立即依赖于实现者 ”。不必依赖SomeCookImplementor
TulainsCórdova13年

@ user61852:如果接口声明和实现者在同一单元中,我会立即获得对该实现者的依赖。不一定是在运行时,而是最肯定是在项目级别,仅因为事实在那里。没有它或使用它的项目将无法再编译。同样,依赖注入与依赖倒置原理也不相同。你可能有兴趣在DIP在野外
马里安Venema

我重新使用您的代码示例在这个问题上programmers.stackexchange.com/a/271142/61852,改进之后它已经被接受。我给你的例子功劳。
图兰斯·科尔多瓦

酷@ user61852:)(感谢您的功劳)
Marjan Venema 2015年

14

您将“四人一组”文档中使用的“客户端”一词与服务使用者中的“客户端”相混淆。

正如“四个帮派”定义所预期的,“客户端”是实现接口的类。如果类A实现了接口B,则他们说A是B的客户。否则,“不应强迫客户实现他们不使用的接口”这句话没有意义的,因为“客户”(如消费者)不需要什么都没实现 仅当您将“客户端”视为“执行者”时,该短语才有意义。

如果“客户端”的意思是一个类“消费”(调用)实现大接口的另一个类的方法,则通过调用您关心的两个方法并忽略其余的方法,足以使您与其他方法分离您不使用的方法。

该原理的精神是避免“客户端”(实现接口的类)仅在关心一组相关方法时才必须实现伪方法,以便遵守整个接口。

它还旨在尽可能减少耦合量,以便在一处进行的更改造成的影响较小。通过隔离接口,可以减少耦合。

当接口执行过多操作并具有应分为几个接口而不是一个接口的方法时,就会出现该问题。

您的两个代码示例都可以。仅在第二个示例中,您假定“客户端”的意思是“使用/调用另一个类提供的服务/方法的类”。

我发现在您给出的三个链接中所解释的概念没有矛盾。

只要在SOLID对话中明确指出“客户”是实施者即可


但是,根据@pdr,尽管所有链接中的代码示例均遵循ISP,但ISP的定义更多是“使客户端(一个调用另一类方法的类)不了解服务”,而不是“防止客户端(实施者)被迫实施他们不使用的接口。”
EdvRusj 2013年

1
@EdvRusj我的答案是基于对象导师(鲍勃·马丁企业)网站上的文档,该文档由马丁本人在著名的“四人帮”中撰写。如您所知,四人制是由包括Martin在内的一组软件工程师创造的SOLID首字母缩写词,确定并记录了这些原理。docs.google.com/a/cleancoder.com/file/d/...
Tulains科尔多瓦

因此,您不同意@pdr,因此发现ISP的第一个定义(请参阅我的原始帖子)更令人满意?
EdvRusj 2013年

@EdvRusj我认为两者都是正确的。但是第二个使用客户端/服务器隐喻增加了不必要的混乱。如果必须选择一个,我会选择官方的“四人帮”,这是第一个。但是,重要的是减少耦合和不必要的依赖性,这毕竟是SOLID原则的精神。哪一个是正确的都没关系。重要的是您应根据行为来隔离接口。就这样。但是,如果有疑问,请转到原始来源。
TulainsCórdova13年

3
我非常不同意您的主张,即“客户”是SOLID Talk中的实施者。对于一个语言,将提供者(实现者)称为提供者(实现者)是一种语言上的废话。我也没有看到关于SOLID的任何文章试图传达这一点,但我可能只是错过了。最重要的是,尽管它设置了接口的实现者,以决定接口中应该包含什么。这对我来说毫无意义。接口的调用者/用户定义了接口中需要的内容,并且该接口的实现者(多个)必须提供该接口。
Marjan Venema 2013年

5

ISP的全部目的是隔离客户端,使其对服务的了解超出其所需要了解的范围(例如,保护它免受无关的更改)。您的第二个定义是正确的。就我的阅读而言,这三篇文章中只有一篇提出了其他建议(第一篇),这完全是错误的。(编辑:不,没有错,只是误导。)

第一个定义与LSP紧密相关。


3
在ISP中,不应强迫客户端使用他们不使用的接口组件。在LSP中,不应强迫SERVICES实现方法D,因为调用代码需要方法A。它们并不矛盾,是互补的。
pdr

2
@EdvRusj,实现ClientA调用的InterfaceA的对象实际上可能与实现Client B所需的InterfaceB的对象完全相同。在极少数情况下,同一客户端需要将相同的对象视为不同的类,因此代码不会通常是“触摸”。您将把它视为一个目的是A,而另一个目的是B。
艾米·布兰肯希

1
@EdvRusj:如果您在这里重新考虑接口的定义,可能会有所帮助。用C#/ Java术语并不总是一个接口。您可能有一个包含许多简单类的复杂服务,因此客户端A使用包装类AX与服务X“接口”。因此,当您以影响A和AX的方式更改X时,您不会被迫影响BX和
B。– pdr

1
@EdvRusj:准确地说,A和B不在乎他们是否都在呼叫X或一个在呼叫Y而另一个在呼叫Z。这就是ISP的基本要点。因此,您可以选择要使用的实现,并在以后轻松地改变主意。ISP并不偏爱一条路由或另一条路由,但LSP和SRP可能如此。
pdr

1
@EdvRusj否,客户端A可以将Service X替换为Service y,两者都将实现接口AX。X和/或Y可以实现其他接口,但是当客户端将它们称为AX时,它并不在乎那些其他接口。
艾米·布兰肯希
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.