C ++中的长代理链

时间:2012-07-24 13:05:39

标签: c++ design-patterns delegation chaining method-chaining

  

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

在我最近的几个项目中,我曾经实现过长代理链是常见的架构。

经常会遇到双重委托链:

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 ++中的长委托链?
  3. 以下是我在做出选择时考虑的一些优缺点:

    优点:

    • 它非常具有描述性:从1行代码中可以看出对象来自
    • 长代表团看起来不错

    缺点:

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

    我想知道长代表团的其他利弊。请根据有争议的观点提出你的推理和投票,而不是你对它的认同程度如何。

8 个答案:

答案 0 :(得分:14)

我不会到目前为止称为反模式。但是,第一个缺点是即使变量C具有逻辑相关性(过于无偿的范围),您的变量if (clCanvas* C = Env->Renderer->GetCanvas()) { C->TextStr( ... ); /* some more things with C */ } 也是可见的。

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

C

这在C ++中是允许的(虽然它不在C中)并且允许你保持适当的范围(template <typename T> T notNULL(T value) { assert(value); return value; } // e.g. notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr(); 的作用域就像它在条件块中一样)并检查NULL。

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


此外,如果您觉得特别花哨,可以将支票封装在一个额外的免费功能中:

{{1}}

答案 1 :(得分:6)

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

答案 2 :(得分:6)

如果你遵循Law of Demeter,就不会发生如此长的授权链。我经常与其中的一些支持者争论说,他们过于认真地坚持自己,但如果你想到如何最好地处理长授权链,你应该更加符合其建议

答案 3 :(得分:4)

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

我的两分钱

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

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

所以,再次,你的链接问题并不一定表明设计好坏 - 它与模式的功能目标或反模式的缺点无关。有些设计即使设计得很好,也会有很多嵌套对象。


该怎么做:

长时间的委托链肯定会在一段时间内很痛苦,只要你的设计规定那些链中的指针不会被重新分配,我想保存一个指向链中的点的临时指针你对此感兴趣是完全正常的(功能范围或更不优选)。

就个人而言,我反对将一个指向链的一部分的永久指针保存为类成员,因为我已经看到最终会有30个指向永久存储的子对象的人,并且你失去了所有的概念对象以您正在使用的模式或体系结构布局。

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

<强>性能

我的最后一点是,这种包装函数方法以及您的委托链方法都存在性能缺陷。如果在循环中使用这些对象,则保存临时指针可以避免多次出现两次解除引用。同样地,从函数调用中存储指针将避免在每个循环周期中额外函数调用的过头。

答案 4 :(得分: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. 速度:如果您需要进行大量调用以获取相同的链元素,从链中“拉出”局部变量而不是调用“正确”的getter可能会更快它的功能。我不知道你的代码是否是生产代码,但它似乎错过了“正确的”getter函数,而似乎只使用了属性。

答案 5 :(得分:3)

长授权链给我带来一点点设计气味。

代表团链告诉我的是,一段代码可以深入访问不相关的代码段,这让我想到了high coupling,这违反了SOLID设计原则。

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

更好的方法是创建一个遵循SOLID原则的架构,并使用Dependency InjectionInversion Of Control等技术来保证您的对象始终可以访问执行其职责所需的内容。这种方法也适用于自动化和单元测试。

只需2美分。

答案 6 :(得分:2)

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

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

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

if ( Env.HasRenderer() ) clCanvas* C = Env.Renderer().GetCanvas();

答案 7 :(得分:1)

如果你能保证所有的物品都存在,我真的没有看到你在做什么的问题。正如其他人所提到的,即使你认为NULL永远不会发生,它也可能就此发生。

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

这就是说,它不会阻止 - &gt;操作员偶尔投掷一次。

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

请注意,使用C ++ 11,你有std :: smart_ptr&lt;&gt;,所以请使用它!