Method Dependency Injection in C++: Enhancing Flexibility and Testability

Introduction

In our previous lessons, we explored how to refactor tightly coupled code using abstract classes and pure virtual functions, along with constructor injection. These techniques have helped us make our code more modular, testable, and maintainable. In this lesson, we'll explore method dependency injection, a powerful alternative that offers unique advantages in certain scenarios. This lesson will guide us through understanding, implementing, and leveraging method dependency injection to further enhance our code's flexibility and testability.

Understanding Method Dependency Injection

Method dependency injection is a technique where dependencies are provided to a method at runtime, rather than being set at the time of object creation. This approach allows for greater flexibility, as it enables us to inject different dependencies for different method calls. Unlike constructor injection, which sets dependencies for the lifetime of an object, method injection allows for more granular control over dependencies, making it particularly useful in scenarios where different configurations are needed for different operations.

In C++, method dependency injection can be achieved by passing dependencies as parameters to methods, using abstract classes and pure virtual functions to define the contracts for these dependencies. This allows us to inject different implementations as needed, enhancing both flexibility and testability.

Key Problems Addressed by Method Dependency Injection

Method dependency injection addresses several common issues in tightly coupled code. One of the primary problems it solves is the rigidity of hardcoded dependencies, which can make code difficult to test and adapt. By allowing dependencies to be injected at the method level, we can easily swap out implementations for testing or different runtime environments. This approach enhances testability by enabling the use of mock objects, and it increases flexibility by allowing different configurations for different method calls.

Implementing Method Dependency Injection

Let's explore how to implement method dependency injection using a practical example. Consider the OrderProcessor class, which processes orders by interacting with a database. Instead of relying on fixed dependencies, we can modify the process_order method to accept optional parameters for these dependencies:

#include "OrderProcessor.hpp"
#include "Dependencies.hpp"
#include <iostream>
#include <ctime>

OrderProcessor::OrderProcessor() 
    : db_connection(new DatabaseConnection()), 
      payment_gateway(new PaymentGateway()), 
      owns_db_connection(true), 
      owns_payment_gateway(true) 
{}

OrderProcessor::~OrderProcessor() 
{
    if(owns_db_connection) delete db_connection;
    if(owns_payment_gateway) delete payment_gateway;
}

bool OrderProcessor::process_order(Order& order, IDatabaseConnection* db_connection_override, IPaymentGateway* payment_gateway_override) 
{
    IDatabaseConnection* db_conn = db_connection_override ? db_connection_override : db_connection;
    IPaymentGateway* pay_gateway = payment_gateway_override ? payment_gateway_override : payment_gateway;

    try 
    {
        Customer customer = db_conn->get_customer_by_id(order.customer_id);
        if (customer.id == -1) return false;

        double total_amount = 0;
        for (const auto& item : order.items) 
        {
            Product product = db_conn->get_product_by_id(item.product_id);
            if (product.id == -1) continue;
              
            double item_price = product.price * item.quantity;
              
            total_amount += item_price;
        }

        PaymentResult payment_result = pay_gateway->process_payment(total_amount);
        if (!payment_result.success) return false;

        db_conn->update_order_status(order.id, "Paid");
        order.processed_at = std::time(nullptr);
        order.order_total = total_amount;

        return true;
    } 
    catch (const std::exception& ex) 
    {
        std::cout << "Error processing order: " << ex.what() << std::endl;
        return false;
    }
}

In this example, the process_order method accepts IDatabaseConnection and IPaymentGateway as optional parameters. If no dependencies are provided, it defaults to the class-level fields. This setup allows us to inject a different dependency for each method call, providing flexibility and enhancing testability.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal