2

I have been practicing this hybrid approach for dependency injection in the last couple of days and I am wondering if it should also apply to components which are within the same package?

For example:

I have a GPIO module that uses the device chip and that required to be mocked for unit testing. I also exposed its Pins as interfaces, so their consumers would not have any coupling.

I have a Motor component that lives in another package and consumes the GPIO Pins' interfaces.

Then a ControlAgent component that lives within the same package as the Motor component, and consumes it.

One benefit of using an interface for the Motor and a template class for its implementation seems to be making its construction a little more generic (as long as I provide what's needed at compile time), and also makes the unit testing easier.

But I also have three more components (PID/Encoder/Odometry) and potentially more that are consumed by the ControlAgent.

Seems like a big effort in development time and complexity to setup each of those as interfaces when they are part of the same package.

What is the long term benefit (if any) in the ControlAgent consuming all of its neighboring components as std::unique_ptrs (or any pointers) to interfaces rather than friends/members?

Especially since the implementations use templates, so the types must be known at compile time.

Does the over-engineering make the implementation less readable but also more maintainable?

  • 1
    The main advantage I could think of would be ease of mocking and unit testing. It's generally much less effort to test small, isolated, decoupled components compared with large monoliths, and you're also more likely to get closer to 100% code coverage with those tests. – Ben Cottrell Apr 12 at 16:06
  • @BenCottrell makes sense! so if the system demands high test coverage, it's probably worth the effort. Like an autonomous vehicle for example – Sam Hammamy Apr 12 at 16:10
1

I have been practicing this hybrid approach for dependency injection in the last couple of days and I am wondering if it should also apply to components which are within the same package?

The question is hard to respond. I could respond to you a general pattern that would apply to generic problems, but I don't believe in generic problems.

Dependency injection is great, but I think it's a common misconception to think of type polymorphism to be mandatory when doing so (static with templates or dynamic with vtables).

Here's an example:

struct Logger {
    void log(int value) {
        switch(output) {
        case Output::File:
            // output to file
            break;
        case Output::Console:
            // output to console
            break;
        default:
            // noop
            break;
        }
    }

    enum struct Output { File, Console, NoOp } output;
};

struct Task {
    Logger logger;

    void process() {
        logger.log(42);
    }
};

int main() {
    // isn't that dependency injection?
    auto task = Task{Logger{Logger::Output::Console}};
}

So...

What is the long term benefit (if any) in the ControlAgent consuming all of its neighboring components as std::unique_ptrs (or any pointers) to interfaces rather than friends/members?

Especially since the implementations use templates, so the types must be known at compile time.

Does the over-engineering make the implementation less readable but also more maintainable?

I think adding type polymorphism where there is no benefit will hurt the maintainability of the code. Calling concrete implementation can upgrade readability greatly in some cases. Since you know by reading the code what happens, which functions are truely called it's easier to reason about it without dragging much of the stuff around in your head or having to run the program.

Polymorphism applied carefully only at the places where you know it will be a good extension point or where dynamic polymorphism is actually needed will help maintainability.

Also, note that polymorphism can also be introduce by values. For example, a class that has a std::function member has a polymorphic entry there, even though it's a concrete type.


What about unit tests? you need type polymorphism everywhere to inject mocks!

I think changing the whole design, introduce a maintainability overhead and introduce type polymorphism where it's unneeded will hurt more than help.

Mock classes are a natural solution in a language where all types are dynamic or polymorphic, but in other cases I think we should not forcefeed that solution.

There are way to mock concrete implementation with libraries such as elfspy so I think it's more of a tooling problem than a design problem.

  • That's a good point! Template injection or even constructor injection amounts to the same thing (at least for my cases) – Sam Hammamy Apr 12 at 16:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.