A Functional Alternative to Dependency Injection in C++

A Functional Alternative to Dependency Injection in C++

By Satprem Pamudurthy

Overload, 25(140):22-24, August 2017


Dependency injection allows flexibility. Satprem Pamudurthy showcases a functional alternative in C++.

Functional programming languages have certain core principles: functions as first-class citizens, pure functions (immutable state, no side-effects) and composable generic functions. C++ is not a pure functional language – we cannot impose immutability constraints on a function, for instance – but that is alright. Most real-world applications have side effects such as writing to databases and I/O, and thus cannot be written exclusively using pure functional constructs. With the addition of variadic templates, generic lambdas, perfect forwarding and the ability to return lambdas from functions, C++’s functional credentials are the strongest they have ever been. While OOP is the most popular paradigm in C++, by introducing elements of functional programming into our designs, we can create highly modular, extensible and loosely coupled components. In this article, I propose an alternative to dependency injection that uses functions to allow object behaviors to be configured at runtime.

Dependency injection

The basic building block of OOP in C++ is a class. A class encapsulates data and methods operating on that data. The behavior of an object is defined by its methods and how they manipulate the object’s state. Some objects require the use an external service (a dependency) to implement some of their behavior. Dependency injection is a technique for decoupling the client of a service from the service’s implementation [ Wikipedia-a ]. If the client object were to directly create an instance of the service, it would introduce a hard-coded dependency (strong coupling) between the client and the service implementation. The client object would have to know the exact type of the service, making it impossible to substitute a different implementation of the service at runtime. In dependency injection, we define an interface for the service and the client accesses the service’s methods through the interface. Code external to the client is responsible for creating an instance of the service and injecting it into the client. The injection of the service can be done at construction, or post-construction through setter methods. We can now configure the behavior of the client by substituting different implementations of the service interface. Consider the example in Listing 1.

class ICustomerDatabaseService {
public:
  virtual ~ICustomerDatabaseService() { }
  virtual void 
    deleteCustomer(const CustomerId&) = 0;
  virtual CustomerProfile 
    getProfile(const CustomerId&) const = 0;
  virtual void updateProfile(const CustomerId&,
    const CustomerProfile&) = 0;
};
class IOrderDatabaseService {
public:
  virtual ~IOrderDatabaseService() { }
  virtual Orders
    getPastOrders(const CustomerId&) const = 0;
  virtual void enterNewOrder(const CustomerId&,
    const Order&) = 0;
};

class Customer {
public:
  Customer(const CustomerId& id,
    std::shared_ptr<ICustomerDatabaseService>
      pCustomerDb,
    std::shared_ptr<IOrderDatabaseService>
      pOrderDb)
  : id_(id)
  , pCustomerDb_(pCustomerDb)
  , pOrderDb_(pOrderDb)
  {
  }
  CustomerProfile getProfile() const
  {
    return pCustomerDb_->getProfile(id_);
  }
  void 
    updateProfile(const CustomerProfile& profile)
  {
    pCustomerDb_->updateProfile(id_, profile);
  }
  Orders getPastOrders() const
  {
    return pOrderDb_->getPastOrders(id_);
  }
private:
  CustomerId id_;
  std::shared_ptr<ICustomerDatabaseService>
    pCustomerDb_;
  std::shared_ptr<IOrderDatabaseService>
    pOrderDb_;
};
			
Listing 1

The Customer class has two dependencies:

  • ICustomerDatabaseService
  • IOrderDatabaseService

It uses the ICustomerDatabaseService to get or update the customer’s profile, and the IOrderDatabaseService to load information about past orders. The Customer class should not and does not concern itself with where this information is actually stored or even whether it is even stored anywhere – we might have constructed mock implementations of the services. We can also use the Decorator pattern to extend the behavior of a service. The Decorator pattern is an object-oriented design pattern that allows us to add behavior to an object at runtime [ Wikipedia-b ]. A decorator is a special implementation of the service interface that forwards calls to an inner service implementation while executing code around the forwarded calls. Consider the class in Listing 2, which traces all calls to an order database service.

class TracingOrderDatabaseService 
  : public IOrderDatabaseService {

public:
  explicit 
    TracingOrderDatabaseService
    (std::shared_ptr<IOrderDatabaseService>
    pInner) : pInner_(pInner)
  {
  }

  virtual ~TracingOrderDatabaseService()
  {
  }

  Orders getPastOrders(const CustomerId& id)
    const override
  {
    std::cout << "Getting past orders";
    return pInner_->getPastOrders(id);
  }

  void enterNewOrder(const CustomerId& id, 
    const Order& order) override
  {
    std::cout << "Entering new order";
    pInner_->enterNewOrder(id, order);
  }

private:
  std::shared_ptr<IOrderDatabaseService> pInner_;
};
			
Listing 2

Tight coupling in inheritance

In our example, what does creating a new service implementation entail? For starters, you need to define a new class, and each concrete service class must implement every service method. When extending an existing implementation, you need to define a new class even if you only need to extend one of the service methods. Put another way, the unit of abstraction and extension in object-oriented programming is a class. Implementation inheritance also creates strong coupling between base and derived classes, because the derived class has access to all of the base class’s public and protected data and methods. For an in-depth discussion of the various types of inheritance and their implications, please refer to John Lakos’s presentation on inheritance [ Lakos16a ]. The video of his presentation is available on the ACCU YouTube channel [ Lakos16b ].

OK, so can we solve this problem by using the Interface Segregation Principle (ISP) [ Wikipedia-c ], whereby we define finer role interfaces instead of a fat interface (we can still have a single class implement multiple role interfaces)? Yes, but only for the time being. Interfaces tend to accumulate methods over time, and each new method requires changes down the inheritance tree, which brings us back to square one. Dependency injection can also create unintended dependencies between the Customer and the services. All public service methods are visible to every method of the Customer class, and there is nothing preventing the Customer class from using any of them. In the example above, the Customer class has access to the enterNewOrder() method, and even though it does not use it now, we cannot guarantee that it will not do so in the future. It is good practice to assume that every available method will be used. To quote David L. Parnas’s influential paper on design methodology, a good programmer makes use of the available information given him or her [ Parnas71 ]. Unintentional and hidden dependencies increase complexity and drastically affect maintainability of the code. We need a solution that allows us to better manage dependencies amongst code components.

A functional approach to configurable objects

Let us introduce functional programming into the mix and re-think the design of the Customer class. The illustration below uses a utility class I put together called RuntimeBoundMethod . It is a callable template class that stores a function object (as an std::function ) whose first argument is a reference to the type containing the RuntimeBoundMethod (similar to the implicit ‘this’ in member functions). It takes a reference to the containing object in its constructor and passes it along to the stored function. This allows us to call a RuntimeBoundMethod as we would a member function. We can also specify the const-ness of the bound method with respect to the object containing the RuntimeBoundMethod . The code for this class (Listing 3) is available on github [ Pamudurthy ].

class Customer {
public:
  explicit Customer(const CustomerId& id)
  : id_(id)
  {
  }

  const CustomerId& id() const
  {
    return id_;
  }

  RuntimeBoundMethod<const Customer,
    CustomerProfile> getProfile { this }; 
    // 'const' method
  RuntimeBoundMethod<Customer, void, 
    const CustomerProfile&> updateProfile 
    { this };
  RuntimeBoundMethod<const Customer, Orders>
    getPastOrders { this }; // 'const' method

private:
  CustomerId id_;
};

int main()
{
  CustomerId id{ 1 };
  Customer customer{ id };

  // bind service methods to the customer
  customer.getProfile = [](const Customer& self)
  {
    auto id = self.id();
    CustomerProfile profile;
    // populate the profile for id
    return profile;
  };

  customer.updateProfile = [](Customer& self,
    const CustomerProfile& profile) {
    // commit the new profile to storage
  };

  customer.getPastOrders = 
    [](const Customer& self) {
    auto id = self.id();
    Orders orders;
    // load order details from storage for id
    return orders;
  };

  auto orders = customer.getPastOrders();

  CustomerProfile profile;
    customer.updateProfile(profile);

  return 0;
}
			
Listing 3

We still have the Customer class but instead of service interfaces, the Customer now depends on service methods. The only thing we require of the service methods is that they are callable. We do not require the use of inheritance or any other technique that entails strong coupling amongst service methods. Each service method can be bound (i.e. injected) independently of the other methods. Just as with role interfaces, it could very well be that a single class implements multiple service methods but that is entirely transparent to the Customer class. The entity that wires the Customer and the service methods together gets to decide exactly which service methods the Customer is able to use. Thus there are no unintended dependencies between the Customer and the services. But it is not all roses. If we forget to bind any of the service methods, we will get a nasty surprise at runtime. This is also true of interface-based dependency injection when using setter methods to inject dependencies post-construction. We can avoid creating incomplete objects by requiring that all dependencies be provided at construction.

Extending function behaviors

The unit of abstraction, extension and composition in functional programming is a function. Just as we use decorator classes to extend the behaviors of an object, we can use decorator functions to extend the behavior of a function. C++ provides a powerful and concise syntax for writing generic functions that can forward arguments to another function. The decorator in Listing 4 adds a trace message before calling an inner function, while perfectly forwarding its arguments to that function.

template<typename Method >
void addTraceMessage(Method& method,
  const std::string& traceMessage)
{
  method = [=](auto& self, auto&&... xs) {
    std::cout << traceMessage << std::endl;
    return
      method(std::forward<decltype(xs)>(xs)...);
  };
}
			
Listing 4

We would add tracing to a service method as follows:

  addTraceMessage(customer.getPastOrders,
    "Getting past orders");
  auto orders = customer.getPastOrders();
    // prints a message before calling
    // the inner function

Again, because we are dealing with functions and not interfaces, we are able to add tracing only to the service methods we are interested in.

A caveat about runtime behavior configuration

When using techniques that allow us to configure the behavior of an object at runtime, the intended behavior of an object cannot simply be deduced from its type. Instead, you will need to understand how the object has been wired at and after construction. This requires some adjustment on part of the programmer when it comes to code analysis and debugging, and this remains true even when using a functional approach.

Final thoughts

C++ is not a pure functional language, but ultimately programming paradigms are not so much about language features as they are ways of thinking about component and system design. Thinking functionally will allow us to build highly modular designs that are easy to compose and extend. Object-oriented and functional programming can coexist and C++ allows us get the best of both worlds – we can use classes to encapsulate entities, and function objects to define and extend their behaviors.

References

[Lakos16a] Proper Inheritance, John Lakos at https://raw.githubusercontent.com/boostcon/cppnow_presentations_2016/master/00_tuesday/proper_inheritance.pdf

[Lakos16b] Proper Inheritance, John Lakos, ACCU 2016 at https://www.youtube.com/watch?v=w1yPw0Wd6jA

[Parnas71] Information distribution aspects of design methodology , David L. Parnas, 1971

[Pamudurthy] RuntimeBoundMethod.hpp at https://github.com/spamudurthy1520/FunctionalCPP/tree/master/source

[Wikipedia-a] Dependency Injection at https://en.wikipedia.org/wiki/Dependency_injection

[Wikipedia-b] Decorator Pattern at https://en.wikipedia.org/wiki/Decorator_pattern

[Wikipedia-c] Interface Segregation Principle at https://en.wikipedia.org/wiki/Interface_segregation_principle






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.