csisy
csisy

Reputation: 510

c++ Dependency Injection + Law of Demeter + logger/assert

I've seen two great videos (this and this) about dependency injection, law of demeter and global states (Singletons are considered as global).

I think I got the basic idea but I already have some singleton classes in my library. However if I want a testable and "well-designed" or "less coupled" code, I should use the DI and the LoD. This of course means that Singletons (as a design pattern) are evil because the caller does not now the implementation and any dependency on a global thing is bad, at least from a testing perspective.

More specifically I'm building a simple game engine without using any bigger 3rd party libs. This means I have to work with platform-specific and low level code as well.

Let's be more concrete. I have a Math section in my library where I have a class Vector2. It should be able to "throw assert" when invalid data is entered for one of its function. Or should be able to log it as an error. Or both. Until this time I've simply used a Singleton<Logger> so I was able to access it everywhere.

But I agree, these things should not be used and DI solves these issues. Eg. what if the logger is not initialized yet? What if I want a dummy logger for tests? And so on... What do you recommend for these cases (like the Logger and Assert classes)?

Also the LoD says that I should not use accessors for objects (like getObjectA()->getObjectB()->doSomething()). Instead, pass them as a parameter to the function/constructor. It's okay that it makes everything easier to test (and debug), but it can be painful to skip these functions.

Consider an example from the Unity engine. A GameObject has a method for getting a component from that object. Eg. if I want to manually transform my object I have no choice to call the "object getter", something like this:

this.GetComponent<Transform>().SetPosition(...);

This is against the LoD, isn't it?

Upvotes: 4

Views: 983

Answers (1)

utnapistim
utnapistim

Reputation: 27385

This means I have to work with platform-specific and low level code as well.

Use dependency inversion (not only injection).

What do you recommend for these cases (like the Logger and Assert classes)?

DI requires that you alter your APIs to allow you to inject things, where they are used. To avoid a situation where you have to add lots of extra parameters (one for logger, one for assert implementation, or global configuration settings and so on), group them together:

  • create a runtime configuration class
  • add services to it (logger service, validation service, configuration service, and so on)
  • pass the runtime configuration around;

Also the LoD says that I should not use accessors for objects (like getObjectA()->getObjectB()->doSomething()). Instead, pass them as a parameter to the function/constructor.

There are a few issues with this type of call chaining:

  • it encourages repetition (if you have getObjectA()->getObjectB()-> many times in your code, that is already a maintenance problem)

  • it is a poor substitute for incomplete design. LoD says that if you need to doSomething() starting from an instance of ObjectA, then ObjectA should have a method doSomething:

    void ObjectA::doSomething(ObjectA& a)
    {
        getObjectB()->doSomething();
    }
    

    (or similar).

    This adds a natural point for extending how "doSomething is done starting from an instance of ObjectA" that is good for maintenance.

  • It imposes on all client code that needs to doSomething, the fact that it need to know about ObjectB's interface. This sounds small, but the problem is pervasive, and when applied as a design policy, it compounds badly (if your ObjectA has not only an ObjectB, but also an ObjectC and an ObjectD, this may be enough to force you to spend lots of time just maintaining the dependency relationships).

this.GetComponent<Transform>().SetPosition(...);

This is against the LoD, isn't it?

Yes. The code can be split as follows:

void SetPosition(Transform& t) { t.SetPosition(); }

client code:

SetPosition(this.GetComponent<Transform>());

This way, to set positions in client code, you no longer care about the interface of Transform. You also do not care in the implementation of void SetPosition(Transform& t), that there is an API called GetComponent.

Alternative LoD implementation for your example above:

void YourObject::SetTransformPositions()
{
    GetComponent<Transformation>.SetPosition();
}

... where the type of this is YourObject*.

Upvotes: 3

Related Questions