Dependency Injection In C++ with Hypodermic
/ 4 min read
Table of Contents
C++ doesn’t have built-in dependency injection like some other languages, but Hypodermic fills that gap nicely. It’s a lightweight IoC container that makes managing dependencies much cleaner.
Dependency injection is particularly valuable in robotics systems where you have many interconnected components - sensors, actuators, path planners, safety systems. Without DI, these components become tightly coupled and hard to test individually. With DI, you can easily swap implementations (real sensors vs simulators) and test each component in isolation.
Why Dependency Injection Matters
Without DI, your classes often create their own dependencies, making them hard to test and tightly coupled:
class NavigationService {public: void plan_path(const Point& target) { // Creates its own laser scanner - hard to test! LaserScanner laser("/dev/lidar0"); // Hard-coded hardware dependency auto scan_data = laser.scan();
// Creates its own path planner AStarPlanner planner; // Can't swap algorithms auto path = planner.calculate(scan_data, target);
// Direct motor control MotorController motors("/dev/motor"); // Another hard dependency motors.execute_path(path); }};With DI, dependencies are injected from the outside:
class NavigationService { std::shared_ptr<ISensor> sensor_; std::shared_ptr<IPathPlanner> planner_; std::shared_ptr<IMotorController> motors_;
public: NavigationService(std::shared_ptr<ISensor> sensor, std::shared_ptr<IPathPlanner> planner, std::shared_ptr<IMotorController> motors) : sensor_(sensor), planner_(planner), motors_(motors) {}
void plan_path(const Point& target) { auto scan_data = sensor_->scan(); // Can inject mock sensor for testing auto path = planner_->calculate(scan_data, target); // Can swap algorithms motors_->execute_path(path); // Can test without real hardware }};Basic Registration Patterns
Here are the most common registration patterns I use:
1. Concrete Types
class IMotorController {public: virtual ~IMotorController() = default; virtual void execute_path(const Path& path) = 0;};
class DifferentialDriveController : public IMotorController {public: void execute_path(const Path& path) override { // Motor control logic for differential drive }};
// Register as transient (new instance each time)builder.registerType<DifferentialDriveController>().as<IMotorController>();
// Register as singletonbuilder.registerType<DifferentialDriveController>().as<IMotorController>().singleInstance();2. Interface to Implementation
class ISensor {public: virtual ~ISensor() = default; virtual std::vector<float> scan() = 0;};
class LidarSensor : public ISensor {public: std::vector<float> scan() override { // Read from LIDAR hardware return {}; }};
// Register implementation for interfacebuilder.registerType<LidarSensor>().as<ISensor>().singleInstance();3. Factory Registration
builder.registerInstance(std::make_shared<RobotConfig>(config_file));4. Additional Interfaces
class IPathPlanner {public: virtual ~IPathPlanner() = default; virtual Path calculate(const std::vector<float>& sensor_data, const Point& target) = 0;};
class AStarPlanner : public IPathPlanner {public: Path calculate(const std::vector<float>& sensor_data, const Point& target) override { // A* path planning algorithm return {}; }};
builder.registerType<AStarPlanner>().as<IPathPlanner>();Constructor Injection
Hypodermic automatically resolves constructor dependencies:
class AutonomousDriver { std::shared_ptr<IPathPlanner> path_planner_; std::shared_ptr<ISensor> sensor_; std::shared_ptr<IMotorController> motors_;
public: AutonomousDriver(std::shared_ptr<IPathPlanner> path_planner, std::shared_ptr<ISensor> sensor, std::shared_ptr<IMotorController> motors) : path_planner_(path_planner), sensor_(sensor), motors_(motors) {}
void navigate_to(const Point& target) { auto scan_data = sensor_->scan(); auto path = path_planner_->calculate(scan_data, target);
if (!path.empty()) { motors_->execute_path(path); } }};
// Register everythingbuilder.registerType<AStarPlanner>().as<IPathPlanner>();builder.registerType<LidarSensor>().as<ISensor>().singleInstance();builder.registerType<DifferentialDriveController>().as<IMotorController>().singleInstance();builder.registerType<AutonomousDriver>();
// Hypodermic automatically wires up all dependenciesauto container = builder.build();auto driver = container->resolve<AutonomousDriver>();Real-World Example
Here’s how I set up DI in a robot fleet management system:
// Interfacesclass IRobotRepository {public: virtual ~IRobotRepository() = default; virtual std::optional<Robot> find_by_id(const std::string& robot_id) = 0; virtual void update_status(const Robot& robot) = 0;};
class ITaskScheduler {public: virtual ~ITaskScheduler() = default; virtual void assign_task(const std::string& robot_id, const Task& task) = 0;};
// Implementationsclass DatabaseRobotRepository : public IRobotRepository { std::shared_ptr<MotorController> db_;
public: DatabaseRobotRepository(std::shared_ptr<MotorController> db) : db_(db) {}
std::optional<Robot> find_by_id(const std::string& robot_id) override { // Database lookup return std::nullopt; // Placeholder }
void update_status(const Robot& robot) override { // Save to database }};
class PriorityTaskScheduler : public ITaskScheduler { std::string scheduling_algorithm_;
public: PriorityTaskScheduler(const std::string& algorithm) : scheduling_algorithm_(algorithm) {}
void assign_task(const std::string& robot_id, const Task& task) override { // Task scheduling logic }};
// Application serviceclass FleetManager { std::shared_ptr<IRobotRepository> robot_repo_; std::shared_ptr<ITaskScheduler> scheduler_;
public: FleetManager(std::shared_ptr<IRobotRepository> robot_repo, std::shared_ptr<ITaskScheduler> scheduler) : robot_repo_(robot_repo), scheduler_(scheduler) {}
bool deploy_robot(const std::string& robot_id, const Task& task) { auto robot = robot_repo_->find_by_id(robot_id); if (!robot.has_value() || robot->status != RobotStatus::Idle) { return false; // Robot not available }
robot->status = RobotStatus::Active; robot_repo_->update_status(*robot); scheduler_->assign_task(robot_id, task);
return true; }};
// Container setupauto setup_container() { auto builder = Hypodermic::ContainerBuilder();
// Infrastructure builder.registerType<MotorController>().singleInstance();
// Repositories builder.registerType<DatabaseRobotRepository>().as<IRobotRepository>();
// Services builder.registerInstance(std::make_shared<PriorityTaskScheduler>("priority")) .as<ITaskScheduler>();
// Application services builder.registerType<FleetManager>();
return builder.build();}Testing Benefits
The real payoff comes when testing. You can easily mock dependencies:
class MockTaskScheduler : public ITaskScheduler {public: mutable std::vector<std::pair<std::string, Task>> assigned_tasks;
void assign_task(const std::string& robot_id, const Task& task) override { assigned_tasks.push_back({robot_id, task}); }};
// In your testauto builder = Hypodermic::ContainerBuilder();builder.registerType<MockRobotRepository>().as<IRobotRepository>();builder.registerType<MockTaskScheduler>().as<ITaskScheduler>();builder.registerType<FleetManager>();
auto container = builder.build();auto fleet_manager = container->resolve<FleetManager>();
Task patrol_task{"patrol", "warehouse_zone_A"};fleet_manager->deploy_robot("robot_001", patrol_task);
auto mock_scheduler = container->resolve<MockTaskScheduler>();assert(mock_scheduler->assigned_tasks.size() == 1);The Pattern I Follow
I typically set up the container once at application startup and use it to resolve top-level services:
int main() { auto container = setup_container();
// Resolve main application service auto robot_app = container->resolve<RobotApplication>(); robot_app->start_operations();
return 0;}Hypodermic handles the complex dependency graph automatically. Once you get used to constructor injection, it makes C++ applications much more modular and testable.