C++中的长委托链

Long delegation chains in C++

本文关键字:C++      更新时间:2023-10-16

这肯定是主观的,但我想尽量避免变得爱争论。如果人们对待它是恰当的。

在我最近的几个项目中,我曾经实现长委托链是一种常见现象的体系结构。

双重委托链经常会遇到:

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++中的长委托链

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

优点:

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

缺点:

  • 由于很难检查委派链中的多个临时对象,因此交互调试很困难

我想知道长期代表团链条的其他利弊。请陈述你的推理,并根据你的观点进行投票。

我不会称之为反模式。然而,第一种方法的缺点是,即使变量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();

根据我的经验,像这样的链通常包含不太重要的getter,导致效率低下。我认为(1)是一个合理的方法。使用代理对象似乎有些过头了。我宁愿看到NULL指针崩溃,也不愿使用代理对象。

如果你遵循德米特定律,就不应该发生这样长的委托链。我经常和它的一些支持者争论,他们过于认真地坚持它,但如果你想知道如何最好地处理长的授权链,你可能应该更遵守它的建议。

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

我的两美分

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

同样,反模式是(在我看来)与设计模式的目的背道而驰的方法。例如,设计模式都与代码的结构和适应性有关。人们认为单例是一种反模式,因为它(通常,并不总是)会导致类似蜘蛛网的代码,因为它本质上创建了一个全局,当你有很多时,你的设计会很快恶化。

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


该怎么办:

一段时间后,长的委托链肯定会让人头疼,只要你的设计规定这些链中的指针不会被重新分配,我认为保存一个指向链中你感兴趣的点的临时指针是完全可以的(函数范围或不太好)。

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

另一个想法是——我不确定我是否喜欢这样,但我看到有些人创建了一个私有的(为了你的理智)函数来导航链,这样你就可以回忆起这一点,而不必处理你的指针是否在封面下更改,或者你是否有null的问题。将所有逻辑打包一次,在函数顶部放一个漂亮的注释,说明指针来自链的哪个部分,然后直接在代码中使用函数结果,而不是每次都使用委托链,这可能很好。

性能

我最后要指出的是,这种封装函数方法和您的委托链方法都存在性能缺陷。如果在循环中使用这些对象,保存临时指针可以多次避免额外的两个取消引用。同样,存储函数调用中的指针可以避免在每个循环周期都会出现额外的函数调用。

对于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函数,相反,它似乎只使用了属性

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

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

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

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

只有我的2美分。

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

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

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

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

如果你能保证所有的对象都存在,我真的认为你做的事情没有问题。正如其他人所提到的,即使你认为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<>,所以用它吧!

相关文章:
  • 没有找到相关文章