最少知识原理


32

我了解最少知识原理的动机,但是如果尝试将其应用到我的设计中,则会发现一些缺点。

我在《Head First Design Patterns》一书中发现了该原理的一个示例(实际上是如何不使用它)指定,根据该原理,调用从其他方法返回的对象上的方法是错误的。

但是似乎有时非常需要使用这种功能。

例如:我有几个类:video-capture类,编码器类,streamer类,它们都使用一些基本的其他类VideoFrame,并且由于它们彼此交互,因此它们可以执行以下操作:

streamer 班级代码

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

如您所见,此原理在这里不适用。可以在这里应用该原理,还是不能总是在这样的设计中应用该原理?




4
这可能是更接近的重复。
罗伯特·哈维

1
相关:这样的代码是不是“火车残骸”(违反Demeter定律)?(完整披露:这是我自己的问题)
CVn

Answers:


21

您正在谈论的函数原理(更好地称为Demeter律)可以通过在流媒体类中添加另一个辅助方法来应用,例如

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

现在,每个功能仅“与朋友交谈”,而不是与“朋友的朋友”。

恕我直言,这是一个粗略的准则,可以帮助创建更严格遵循单一责任原则的方法。在上面的简单情况下,如果这样做确实值得麻烦,并且所生成的代码确实“干净”,或者是否只是形式上扩展代码而没有任何明显收获,则可能会非常怀疑。


这种方法确实具有使对单元代码进行单元测试,调试和推理更加容易的优点。
gntskn 2015年

+1。但是,进行代码更改的一个很好的理由是,类接口可能会因方法的工作而变得肿,这些方法的工作仅仅是对多个其他方法进行一次或几次调用(而没有其他逻辑)。在某些类型的编程环境中,例如C ++ COM(尤其是WIC和DirectX),与其他语言相比,将每种方法添加到COM接口的成本很高。
rwong

1
在C ++ COM接口设计中,将大类分解为小类(这意味着您有更多机会与多个对象进行对话)并最大程度地减少接口方法的数量是基于深刻理解的两个具有实际好处(降低成本)的设计目标内部机制(虚拟表,代码可重用性,许多其他方面)的概念。因此,C ++ COM程序员通常必须忽略LoD。
rwong 2015年

10
+1:得墨meter耳定律的确应命名为得墨Suggest耳的建议:YMMV
Binary Worrier,2015年

得墨meter耳定律是做出建筑选择的建议,而您建议进行外观上的更改,以便您似乎遵守了该“定律”。这将是一个误解,因为语法上的变化并不意味着突然之间一切都很好。据我所知,得墨meter耳定律的基本含义是:如果要进行OOP,则不要在各处使用getter函数编写过程代码。
user2180613

39

最少知识原理得墨meter耳定律是警告,不要让您的课程与遍历每一层的其他课程的细节纠缠在一起。它告诉您最好只与您的“朋友”交谈而不是与“朋友的朋友”交谈。

想象您被要求将盾牌焊接在穿着闪亮板甲的骑士雕像上。您将防护罩小心地放在左臂上,使其看起来自然。您会注意到前臂,肘部和上臂上有三个小地方,盾牌碰到了装甲。焊接所有三个位置是因为要确保连接牢固。现在想象您的老板生气了,因为他无法移动盔甲的肘部。您以为装甲永远不会移动,因此在前臂和上臂之间建立了固定的连接。屏蔽只能连接到它的朋友,前臂。不要前臂的朋友。即使您必须添加一些金属以使其接触。

隐喻是很好的,但朋友实际上是什么意思?对象知道如何创建或查找的任何东西都是朋友。或者,一个对象可以只要求交出其他对象,而该对象只知道其接口。这些不算作朋友,因为没有对如何获得他们的期望。如果该对象不知道它们来自何处,是因为有其他事物通过/注入了它,则它不是朋友的朋友,甚至不是朋友。这是对象仅知道如何使用的东西。这是好事。

当尝试应用这样的原则时,重要的是要了解它们永远不会禁止您完成某些事情。这些警告是您可能会忽略做更多的工作来获得更好的设计来完成相同的事情。

没有人会无缘无故地工作,因此了解您从此中学到的东西很重要。在这种情况下,它可使您的代码保持灵活。您可以进行更改,而受更改影响的其他类更少。听起来不错,但除非您将其视为某种宗教教义,否则它不会帮助您决定要做什么。

不要盲目地遵循这个原理,而是简单地解决这个问题。写一个不遵循原理的解决方案,而遵循该原理的解决方案。现在,您已经有了两种解决方案,您可以通过尝试将两种更改都接受来比较它们对更改的接受程度。

如果您在遵循此原则时仍无法解决问题,则可能是您缺少另一项技能。

解决您特定问题的一种方法是,将注入frame到知道如何与框架交谈的东西(一个类或方法)中,这样您就不必在您的类中散布所有这些框架聊天细节,现在,该类仅知道如何以及何时进行交谈。得到一个框架。

实际上,这遵循另一个原则:将使用与构造分开。

frame = encoder->WaitEncoderFrame()

通过使用此代码,您已承担了以某种方式获得的责任Frame。您尚未承担与交谈的任何责任Frame

frame->DoOrGetSomething(); 

现在,您必须知道如何与进行对话Frame,但应将其替换为:

new FrameHandler(frame)->DoOrGetSomething();

现在,您只需要知道如何与您的朋友FrameHandler交谈。

有很多方法可以做到这一点,这也许不是最好的方法,但是它说明了遵循该原理不会使问题解决。只是需要您做更多的工作。

每个好的规则都有一个例外。我知道的最好的例子是内部特定领域语言。一条DSL方法链似乎一直在滥用Demeter法则,因为您一直在调用返回不同类型并直接使用它们的方法。为什么这样可以?因为在DSL中返回的所有内容都是经过精心设计的朋友,您应该直接与之交谈。根据设计,您有权要求DSL的方法链不变。如果您只是随机钻研代码库,将发现的内容链接在一起,那么您就没有权利。最好的DSL是非常薄的表示形式或与其他对象的接口,您可能都不应该去研究它们。我之所以只提一下,是因为当我了解了DSL是一个好的设计之后,我发现对demeter的定律有了更好的了解。有人甚至说DSL甚至没有违反demeter的真实法则。

另一个解决方案是让其他东西注入frame您的体内。如果frame来自设置员或最好是构造器,则您不承担构建或获取框架的任何责任。这意味着您在这里扮演的角色更像FrameHandlers是要扮演的角色。相反,现在您是一个健谈的人,Frame并提出了一些其他Frame 解决方案,以弄清楚如何获得许可。某种程度上,这是相同的解决方案,只是改变了视角。

SOLID原则是大的我尽量遵循。这里要尊重的两个是单一责任和依赖倒置原则。确实很难尊重这两个人,但最终仍然违反了Demeter法则。

违反Demeter的心态就像在自助餐厅吃东西一样,在这里您可以随心所欲。通过一些前期工作,您可以为自己提供菜单和服务器,以带给您您喜欢的任何东西。坐下来,放松,并给小费。


2
“它告诉您最好只与您的“朋友”聊天而不是与“朋友的朋友”聊天” ++
RubberDuck

1
第二段,那是从某个地方偷来的,还是您编成的?那使我完全理解了这个概念。它使什么是“朋友的朋友”变得公然,并且纠结了太多的类(关节)也有什么弊端。A +报价。
死于maus 2016年

2
@diemaus如果我从某个地方偷走了那个屏蔽段落,那么它的来源就从我的脑海中泄漏出来了。当时我的大脑只是以为它很聪明。我记得的是,在大多数投票之后我都添加了它,因此很高兴看到它得到验证。很高兴它有所帮助。
candied_orange

19

功能设计是否比面向对象设计好?这取决于。

MVVM是否比MVC更好?这取决于。

是Amos&Andy还是Martin和Lewis?这取决于。

它取决于什么?您做出的选择取决于每种技术在满足设计,性能和可维护性目标的同时,如何满足软件的功能和非功能要求。

[某些书]说[某件事]是错的。

当您在书本或博客中阅读此内容时,请根据其优缺点评估您的主张;也就是说,问为什么。在软件开发中,没有对与错的技术,只有“该技术能达到我的目标的程度如何?它是有效的还是无效的?它解决了一个问题却创造了一个新问题吗?整个过程能否被人们很好地理解?开发团队,还是太晦涩?

在这种特殊情况下-在另一种方法返回的对象上调用方法的动作-由于存在一种将这种做法(工厂)编码的实际设计模式,因此很难想象有人可以断言它是绝对的错误。

之所以称为“最低知识原则”,是因为“低耦合”是系统的理想质量。彼此之间没有紧密绑定的对象可以更独立地工作,因此更易于单独维护和修改。但正如您的示例所示,有时更需要高耦合,以便对象可以更有效地协调其工作。


2

布朗博士的答案显示了经典的《德米特律法》教科书的实现-以及添加数十种方法的烦恼/杂乱无章的代码膨胀,这可能就是为什么程序员(包括我自己)即使这样做应该也不愿意这么做的原因。

还有一种分离对象层次结构的替代方法:

通过您的方法和属性公开interface类型,而不是class类型。

在原始海报(OP)的情况下,encoder->WaitEncoderFrame()将返回IEncoderFrame而不是Frame,并定义允许的操作。


解决方案1

在最简单的情况下,FrameEncoder类都是你的控制之下,IEncoderFrame是方法框架的一个子集已经公开暴露,而Encoder类并不真正关心你做的是什么样的对象。然后,实现很简单(c#中的代码):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

解决方案2

在中间情况下,如果Frame定义不受您的控制,或者将IEncoderFrame的方法添加到不合适Frame,则Adapter是一个很好的解决方案。这就是CandiedOrange的答案所讨论的new FrameHandler( frame )。重要信息:如果执行此操作,则将其公开为接口而不是,则更加灵活。Encoder我必须了解class FrameHandler,但是客户只需要了解interface IFrameHandler。或以我的名字命名,interface IEncoderFrame-表示从编码器的POV看,它具体是Frame

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

成本:每次encoder.TheFrame调用新对象EncoderFrameWrapper的分配和GC 。(您可以缓存该包装程序,但会添加更多代码。只有在无法用新框架替换编码器的框架字段时,才能轻松可靠地进行编码。)


解决方案3

在更困难的情况下,新包装器需要同时了解EncoderFrame。该对象本身会违反LoD-它正在操纵Encoder和Frame之间的关系,这应该是Encoder的责任-可能很难解决。如果您沿着这条路走,可能会发生以下情况:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

那太丑了。当包装器需要触摸其创建者/所有者(编码器)的详细信息时,有一个复杂的实现:

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

当然,如果我知道自己会在这里结束,我可能不会这样做。只需编写LoD方法即可完成。无需定义接口。另一方面,我喜欢接口将相关方法包装在一起。我喜欢对“框架”进行“类似框架的操作”的感觉。


最后评论

考虑一下:如果实施者Encoder觉得公开Frame frame适合他们的整体体系结构,或者“比实现LoD容易得多”,那么如果他们改为执行我所展示的第一个代码片段,则安全得多-公开有限的框架,作为接口。 以我的经验,这通常是一个完全可行的解决方案。只需根据需要向接口添加方法。(我说的是一个场景,我们“知道” Frame已经有了所需的方法,或者添加起来很容易且没有争议。每种方法的“实现”工作是在接口定义中添加一行。)知道即使在最坏的未来情况下,也可以保持该API正常工作-在这里,IEncoderFrameFrameEncoder

另请注意,如果您没有权限添加IEncoderFrameFrame,或者所需的方法不太适合一般Frame类,并且解决方案2不适合您,可能是由于额外的对象创建和销毁,解决方案3可以看作是一种组织EncoderLoD 方法的简单方法。不要只是通过许多方法。将它们包装在Interface中,并使用“显式接口实现”(如果您使用的是c#),以便当通过该接口查看对象时才可以访问它们。

我要强调的另一点是,决定将功能公开为接口,从而解决了上述所有3种情况。首先,IEncoderFrame只是Frame的功能的子集。第二个IEncoderFrame是适配器。第三部分IEncoderFrameEncoders功能的一个分区。在这三种情况之间您的需求是否变化都没关系:API保持不变。


将类与其协作者之一返回的接口(而不是具体的类)进行耦合,尽管有所改进,但仍然是耦合的来源。如果需要更改界面,则意味着您的班级也需要更改。要去除的重要一点是,只要确保避免不必要地耦合不稳定的对象或内部结构耦合就不会固有地不好。这就是为什么需要用少量的盐来遵守《得墨meter耳定律》的原因。它会指示您根据情况始终避免出现可能会或可能不会出现问题的事情。
Periata Breatta

@PeriataBreatta-我当然不能也不会不同意。但我想指出一件事:接口,顾名思义,代表了需要在两个类之间的边界处知道的内容。如果它“需要改变”,那是基本的 -任何替代方法都无法以某种方式神奇地避免了所需的编码。与我描述的三种情况中的任何一种(而不是使用接口)相反,而是返回一个具体的类。在1,Frame,2,EncoderFrameWrapper,3,Encoder中。您将自己锁定在方法中。该界面可以适应所有这些。
ToolmakerSteve

@PeriataBreatta ...展示了将其显式定义为接口的好处。我希望最终能够增强IDE,以使其变得如此方便,以至于接口将被大量使用。大多数多级访问将通过某个界面进行,因此更容易管理更改。(如果在某些情况下这是“过度杀伤力”,则代码分析再加上关于我们愿意在何处承担没有接口风险的注释,以换取较小的性能提升,可以“编译出来”-替换为3个“解决方案”中的3个具体类之一。)
ToolmakerSteve

@PeriataBreatta-当我说“界面可以适应所有人”时,我并不是说“啊,这一种语言功能可以涵盖这些不同的情况真是太好了”。我的意思是,定义接口可以最大程度地减少您可能需要进行的更改。最好的情况是,设计可以从最简单的情况(解决方案1)到最困难的情况(解决方案3),从根本上改变,而无需改变界面-只有生产者的内部需求变得更加复杂。恕我直言,即使需要进行更改,它们的普及程度也较低。
ToolmakerSteve
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.