Search code examples
c++design-patternsdelegationchainingmethod-chaining

Long delegation chains in C++


This is definitely subjective, but I'd like to try to avoid it becoming argumentative. I think it could be an interesting question if people treat it appropriately.

In my several recent projects I used to implement architectures where long delegation chains are a common thing.

Dual delegation chains can be encountered very often:

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

And triple delegation is not rare at all:

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

Delegation chains of higher order exist but are really scarce.

In above mentioned examples no NULL run-time checks are performed since the objects used are always there and are vital to the functioning of the program and explicitly constructed when execution starts. Basically I used to split a delegation chain in these cases:

1) I reuse the object obtained through a delegation chain:

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

2) An intermediate object somewhere in the middle of the delegation chain should be checked for NULL before usage. Eg.

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

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

I used to fight the case (2) by providing proxy objects so that a method can be invoked on non-NULL object leading to an empty result.

My questions are:

  1. Is either of cases (1) or (2) a pattern or an antipattern?
  2. Is there a better way to deal with long delegation chains in C++?

Here are some pros and cons I considered while making my choice:

Pros:

  • it is very descriptive: it is clear out of 1 line of code where did the object came from
  • long delegation chains look nice

Cons:

  • interactive debugging is labored since it is hard to inspect more than one temporary object in the delegation chain

I would like to know other pros and cons of the long delegation chains. Please, present your reasoning and vote based on how well-argued opinion is and not how well you agree with it.


Solution

  • I wouldn't go so far to call either an anti-pattern. However, the first has the disadvantage that your variable C is visible even after it's logically relevant (too gratuitous scoping).

    You can get around this by using this syntax:

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

    This is allowed in C++ (while it's not in C) and allows you to keep proper scope (C is scoped as if it were inside the conditional's block) and check for NULL.

    Asserting that something is not NULL is by all means better than getting killed by a SegFault. So I wouldn't recommend simply skipping these checks, unless you're a 100% sure that that pointer can never ever be NULL.


    Additionally, you could encapsulate your checks in an extra free function, if you feel particularly dandy:

    template <typename T>
    T notNULL(T value) {
      assert(value);
      return value;
    }
    
    // e.g.
    notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr();