23

这绝对是主观的,但我想尽量避免它变得有争议。我认为如果人们适当地对待它,这可能是一个有趣的问题。

在我最近的几个项目中,我曾经实现过长委托链很常见的架构。

双委托链经常会遇到:

bool Exists = Env->FileSystem->FileExists( "foo.txt" );

三重委托并不罕见:

Env->Renderer->GetCanvas()->TextStr( ... );

存在更高阶的委托链,但非常稀缺。

在上面提到的示例中,没有执行 NULL 运行时检查,因为使用的对象始终存在并且对程序的运行至关重要,并且在执行开始时显式构建。基本上我曾经在这些情况下拆分委托链:

1)我重用通过委托链获得的对象:

{ // make C invisible to the parent scope
   clCanvas* C = Env->Renderer->GetCanvas();
   C->TextStr( ... );
   C->TextStr( ... );
   C->TextStr( ... );
}

2)委托链中间某处的中间对象在使用前应检查是否为 NULL。例如。

clCanvas* C = Env->Renderer->GetCanvas();

if ( C ) C->TextStr( ... );

我曾经通过提供代理对象来对抗情况 (2),以便可以在导致结果的非 NULL 对象上调用方法empty

我的问题是:

  1. 案例 (1) 或 (2) 是模式还是反模式?
  2. 有没有更好的方法来处理 C++ 中的长委托链?

以下是我在做出选择时考虑的一些利弊:

优点:

  • 它非常具有描述性:从 1 行代码中可以清楚地看出对象来自哪里
  • 长委托链看起来不错

缺点:

  • 交互式调试很费力,因为很难在委托链中检查多个临时对象

我想知道长委托链的其他优点和缺点。请提出您的推理并根据有充分理由的意见进行投票,而不是您是否同意。

4

8 回答 8

14

我不会说这两种模式都是反模式。但是,第一个缺点C是即使在逻辑上相关之后,您的变量也是可见的(范围太无意义)。

您可以使用以下语法解决此问题:

if (clCanvas* C = Env->Renderer->GetCanvas()) {
  C->TextStr( ... );
  /* some more things with C */
}

这在 C++ 中是允许的(虽然它不在 C 中),并且允许您保持适当的范围(C范围就像在条件块内一样)并检查 NULL。

断言某些东西不是 NULL 绝对比被 SegFault 杀死要好。所以我不建议简单地跳过这些检查,除非你 100% 确定该指针永远不会为 NULL。


此外,如果您觉得特别花哨,您可以将您的检查封装在一个额外的免费函数中:

template <typename T>
T notNULL(T value) {
  assert(value);
  return value;
}

// e.g.
notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr();
于 2012-07-24T13:17:40.217 回答
6

以我的经验,这样的链通常包含不那么微不足道的吸气剂,导致效率低下。我认为(1)是一种合理的方法。使用代理对象似乎有点矫枉过正。我宁愿看到 NULL 指针崩溃,也不愿使用代理对象。

于 2012-07-24T13:11:26.227 回答
6

如果您遵循得墨忒耳法则,就不应该发生如此长的授权链。我经常与它的一些支持者争辩说,他们过于认真地坚持它,但如果你想知道如何最好地处理长委托链,你可能应该更符合它的建议。

于 2012-08-25T17:39:22.060 回答
4

有趣的问题,我认为这是可以解释的,但是:

我的两分钱

设计模式只是常见问题的可重用解决方案,这些解决方案足够通用,可以广泛应用于面向对象(通常)编程中。许多常见模式将从接口、继承链和/或包含关系开始,这将导致您在某种程度上使用链接来调用事物。这些模式并没有试图解决这样的编程问题——链接只是它们解决手头功能问题的副作用。所以,我不会真的认为它是一种模式。

同样,反模式是(在我看来)与设计模式的目的相反的方法。例如,设计模式都与代码的结构和适应性有关。人们认为单例是一种反模式,因为它(通常,并非总是)会产生类似蜘蛛网的代码,因为它固有地创建了一个全局,并且当你有很多时,你的设计会迅速恶化。

因此,同样,您的链接问题并不一定表明设计的好坏 - 它与模式的功能目标或反模式的缺点无关。即使设计得很好,有些设计也只有很多嵌套对象。


该怎么办:

一段时间后,长的委托链肯定会让人头疼,只要您的设计规定这些链中的指针不会被重新分配,我认为将临时指针保存到链中您感兴趣的点in 完全没问题(功能范围或更小)。

不过,就我个人而言,我反对将指向链的一部分的永久指针保存为类成员,因为我已经看到最终导致人们拥有 30 个指向永久存储的子对象的指针,并且你失去了对对象如何的所有概念在您正在使用的模式或架构中进行布局。

另一个想法 - 我不确定我是否喜欢这个,但我看到有些人创建了一个私人(为了你的理智)功能来导航链,这样你就可以回忆起它而不处理关于是否您的指针在幕后发生变化,或者您是否有空值。将所有逻辑包装一次会很好,在函数顶部放置一个很好的注释,说明它从链的哪一部分获取指针,然后直接在代码中使用函数结果而不是使用委托每次上链。

表现

我的最后一点是,这种功能包装方法以及您的委托链方法都存在性能缺陷。如果您在循环中使用这些对象,保存临时指针可以让您避免额外的两次取消引用。同样,存储来自函数调用的指针将避免每个循环周期的额外函数调用的开销。

于 2012-07-24T13:24:58.740 回答
3

因为bool Exists = Env->FileSystem->FileExists( "foo.txt" );我宁愿对你的链进行更详细的细分,所以在我的理想世界中,有以下代码行:

Environment* env = GetEnv();
FileSystem* fs = env->FileSystem;
bool exists = fs->FileExists( "foo.txt" );

为什么?一些原因:

  1. 可读性:我的注意力会丢失,直到我不得不读到行尾,以防万一bool Exists = Env->FileSystem->FileExists( "foo.txt" );这对我来说太长了。
  2. 有效性:不管您提到的对象是,如果您的公司明天雇用一个新程序员并且他开始编写代码,那么后天对象可能不存在。这些长行非常不友好,新人可能会害怕它们并会做一些有趣的事情,例如优化它们......这将需要更有经验的程序员额外的时间来修复。
  3. 调试:如果有任何机会(并且在您雇用了新程序员之后)应用程序在长链列表中抛出了分段错误,那么很难找出哪个对象是有罪的。细分越详细,就越容易找到错误的位置。
  4. speed:如果您需要进行大量调用以获取相同的链元素,则从链中“拉出”局部变量而不是为其调用“适当的”getter 函数可能会更快。我不知道您的代码是否是生产代码,但它似乎错过了“正确的”getter 函数,而是似乎只使用了属性。
于 2012-08-27T11:44:19.000 回答
3

长的委托链对我来说有点设计的味道。

委托链告诉我的是,一段代码可以深度访问一段不相关的代码,这让我想到了高耦合,这违背了SOLID设计原则。

我遇到的主要问题是可维护性。如果你达到了两个层次,那就是两段独立的代码,它们可以自行进化并在你的控制下崩溃。当您在链中拥有函数时,这会很快复合,因为它们可以包含自己的链 - 例如,Renderer->GetCanvas()可能会根据来自另一个对象层次结构的信息选择画布,并且很难强制执行不会结束的代码路径在代码库的整个生命周期内深入到对象。

更好的方法是创建一个遵循 SOLID 原则并使用依赖注入控制反转等技术的架构,以确保您的对象始终可以访问他们执行职责所需的内容。这种方法也很适合自动化和单元测试。

只是我的2美分。

于 2012-08-31T00:07:41.583 回答
2

如果可能的话,我会使用引用而不是指针。所以委托保证返回有效对象或抛出异常。

clCanvas & C = Env.Renderer().GetCanvas();

对于不存在的对象,我将提供额外的方法,例如 has、is 等。

if ( Env.HasRenderer() ) clCanvas* C = Env.Renderer().GetCanvas();
于 2012-07-24T17:33:20.950 回答
1

如果您可以保证所有对象都存在,那么我真的不认为您正在做的事情有问题。正如其他人所提到的,即使您认为 NULL 永远不会发生,它也可能会发生。

话虽如此,我看到您到处都使用裸指针。我建议你开始使用智能指针。当您使用 -> 运算符时,如果指针为 NULL,则通常会抛出智能指针。所以你避免了SegFault。不仅如此,如果您使用智能指针,您可以保留副本,并且对象不会消失在您的脚下。您必须在指针变为 NULL 之前显式重置每个智能指针。

话虽如此,它不会阻止 -> 运算符偶尔抛出一次。

否则我宁愿使用 AProgrammer 提出的方法。如果对象 A 需要一个指向对象 B 指向的对象 C 的指针,那么对象 A 正在做的工作很可能是对象 B 实际上应该做的事情。所以 A 可以保证它始终有一个指向 B 的指针(因为它拥有一个指向 B 的共享指针,因此它不能为 NULL),因此它总是可以调用 B 上的函数来对对象 C 执行操作 Z。 Z, B 知道它是否总是有一个指向 C 的指针。这是其 B 实现的一部分。

请注意,对于 C++11,您有 std::smart_ptr<>,所以使用它!

于 2012-08-30T23:56:55.117 回答