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