it-swarm.cn

简单地调用另一个函数的函数,糟糕的设计选择?

我有一个代表建筑物的类的设置。该建筑物的平面图有边界。

我设置的方式是这样的:

public struct Bounds {} // AABB bounding box stuff

//Floor contains bounds and mesh data to update textures etc
//internal since only building should have direct access to it no one else
internal class Floor {  
    private Bounds bounds; // private only floor has access to
}

//a building that has a floor (among other stats)
public class Building{ // the object that has a floor
    Floor floor;
}

这些对象在做不同的事情时有其存在的独特原因。但是,有一种情况,我想在建筑物的本地获取一个提示。

在这种情况下,我实际上是在做:

Building.GetLocalPoint(worldPoint);

然后有:

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

这导致我的Floor对象中的此功能:

internal Vector3 GetLocalPoint(Vector3 worldPoint){
    return bounds.GetLocalPoint(worldPoint);
}

然后,当然,bounds对象实际上会执行所需的数学运算。

如您所见,这些功能是多余的,因为它们只是传递给下层的另一个功能。这对我来说并不聪明-闻起来像是糟糕的代码,会在代码混乱的情况下咬住我。

另外,我也可以像下面这样编写我的代码,但我必须公开一些我不想做的事情:

building.floor.bounds.GetLocalPoint(worldPoint);

当您转到许多嵌套对象时,这也开始变得很愚蠢,并导致获得较大兔子孔来获得给定的功能,最终您可能会忘记它在哪里-这也闻起来像是糟糕的代码设计。

设计所有这些的正确方法是什么?

53
WDUK

永远不要忘记 Demeter法则

得墨meter耳定律(LoD)或最低知识原则是用于开发软件(尤其是面向对象的程序)的设计指南。在一般形式下,LoD是松耦合的一种特殊情况。该指南由东北大学的伊恩·荷兰(Ian Holland)于1987年底提出,可以通过以下每种方式进行简要总结:[1]

  • 每个单元对其他单元的知识应该有限:只有与当前单元“紧密”相关的单元。
  • 每个单位只能与朋友交谈;不要和陌生人说话。
  • 仅与您的直属朋友交谈

基本概念是,根据“信息隐藏”的原则,给定的对象应尽可能少地考虑其他任何事物(包括其子组件)的结构或属性。
它可能被视为最小特权原则的必然结果,该原则规定一个模块仅拥有为其合法目的所必需的信息和资源。


building.floor.bounds.GetLocalPoint(worldPoint);

此代码违反了LOD。您当前的消费者需要以某种方式知道:

  • 该建筑物具有floor
  • 地板有bounds
  • 边界具有GetLocalPoint方法

但实际上,您的消费者应该只处理building,而不要处理建筑物内部的任何内容(不应直接处理子组件)。

如果这些基础类的any在结构上发生了变化,那么您也突然需要更改此使用者,即使他可能比您实际更改的类高出几个级别。
这开始侵犯您拥有的图层的分离,因为更改会影响多个图层(不仅仅是其直接邻居)。

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

假设您引入了第二种类型的建筑物,即没有地板的建筑物。我想不出一个真实的例子,但是我试图显示一个通用的用例,所以让我们假设EtherealBuilding是这种情况。

因为您有building.GetLocalPoint方法,您可以在不改变建筑物使用者的情况下更改其运作方式,例如:

public class EtherealBuilding : Building {
    public Vector3 GetLocalPoint(Vector3 worldPoint){    
        return universe.CenterPoint; // Just a random example
    }
}

更难理解的是,没有地板的建筑物没有明确的用例。我不知道您的域名,也无法判断是否/如何发生。

但是,开发准则是放弃特定上下文应用程序的通用方法。如果我们更改上下文,该示例将变得更加清晰:

// Violating LOD

bool isAlive = player.heart.IsBeating();

// But what if the player is a robot?

public class HumanPlayer : Player {
    public bool IsAlive() {
        return this.heart.IsBeating();
    }
}

public class RobotPlayer : Player {
    public bool IsAlive() {
        return this.IsSwitchedOn();
    }
}

// This code works for both human and robot players, and thus wouldn't need to be changed when new (sub)types of players are developed.

bool isAlive = player.IsAlive();

这就证明了Player类(或其任何派生类)上的方法为何有目的,即使其当前实现很简单


旁注
为了举例说明,我略过了切线讨论,例如如何处理继承。这些不是答案的重点。

110
Flater

如果您偶尔在某些地方有这种方法,那么这可能只是一致性设计的副作用(或者,您可能要付出的代价)。

如果您有很多,那么我认为这是该设计本身有问题的迹象。

在您的示例中,也许不应该一种从建筑物外部“在建筑物上本地获取点”的方法,取而代之的是,建筑物的方法应处于较高的抽象水平,并可以使用此类方法仅在内部指向。

21
Michael Borgwardt

著名的“得墨meter耳法律”是一条法律,规定了要编写哪种代码,但没有解释任何有用的代码。弗拉特的回答很好,因为它给出了例子,但我不会将它们称为“违反/遵守Demeter律”。如果您所在的地方实施了《德米特尔法律》,请与当地的德米特警察局取得联系,他们将很乐意为您解决问题。

请记住,您始终是编写代码的高手,因此,在创建“委托函数”与不编写它们之间,这是您自己判断的问题。没有清晰的线条,因此无法定义任何清晰的规则。相反,我们可以找到一些像Flater一样的情况,其中创建此类函数毫无用处,而创建此类函数很有用。 (Spoiler:在前一种情况下,修复方法是内联函数;在后一种情况中,修复方法是创建函数。

唯一没有原因的示例包括:

  • 当成员不是应该封装的实现详细信息时,访问成员返回的对象的成员。
  • 您的接口成员已通过.NET的 准实现 正确实现
  • 符合Demeter

创建委托函数很有用的示例包括:

  • 排除反复重复的呼叫链
  • 语言迫使您进行例如通过委派给另一个成员或简单地调用另一个函数来实现接口成员
  • 当您调用的函数与其他相同级别的调用不在同一个概念级别(例如,与插件自省相同级别的LoadAssembly调用)
1
Laurent LA RIZZA

忘了您暂时了解Building的实现。有人写了它。也许是只给您编译代码的供应商。或实际上是下周开始编写的承包商。

您所知道的只是Building的接口和对该接口的调用。它们看起来都很合理,所以您还好。

现在,您穿上另一套外套,您突然成为了Building的实现者。您不知道Floor的实现,只知道接口。您使用Floor接口来实现Building类。您知道Floor的接口以及对该接口的调用以实现Building类,它们看起来都非常合理,因此您再也可以。

总而言之,没问题。一切安好。

1
gnasher729

building.floor.bounds.GetLocalPoint(worldPoint);

不好。

对象只能与它们的直接邻居打交道,因为否则您的系统将很难更改。

0
kiwicomb123

可以只调用函数。当然,有许多设计模式正在使用该技术,例如适配器和外观,但也有一些扩展模式,例如装饰器,代理等等。

这都是关于抽象级别的。您不应混合使用来自不同抽象级别的概念。为此,您有时需要调用内部对象,以使您的客户端不会被迫自己执行。

例如(汽车示例会更简单):

您有“驾驶员”,“汽车”和“车轮”对象。在现实世界中,为了驾驶汽车,您是否让驾驶员直接用轮子做某事,或者他只是与汽车整体互动?

如何知道某事不好:

  • 封装被破坏,内部对象在公共API中可用。 (例如,类似于car.Wheel.Move()的代码)。
  • SRP原则被打破,对象正在做许多不同的事情(例如,准备电子邮件文本并实际在同一对象中发送)。
  • 难以对特定的类进行单元测试(例如,有很多依赖项)。
  • 有不同的领域专家(或公司部门)来处理您在同一个班级中处理的事情(例如销售和包裹递送)。

违反得墨Law耳定律的潜在问题:

  • 硬单元测试。
  • 依赖于其他对象的内部结构。
  • 对象之间的高耦合。
  • 公开内部数据。
0
0lukasz0