skip to content
Mehdi Mehdikhani
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 singleton
builder.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 interface
builder.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 everything
builder.registerType<AStarPlanner>().as<IPathPlanner>();
builder.registerType<LidarSensor>().as<ISensor>().singleInstance();
builder.registerType<DifferentialDriveController>().as<IMotorController>().singleInstance();
builder.registerType<AutonomousDriver>();
// Hypodermic automatically wires up all dependencies
auto container = builder.build();
auto driver = container->resolve<AutonomousDriver>();

Real-World Example

Here’s how I set up DI in a robot fleet management system:

// Interfaces
class 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;
};
// Implementations
class 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 service
class 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 setup
auto 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 test
auto 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.