Saturday, May 9, 2020

C++ Multiple Implementation Design Pattern

It is common to have multiple implementations to a given problem. In STL, for example, you can build a queue from a vector or a list, i.e,

std::queue<int, std::vector<int>> queue_using_vector;
std::queue<int, std::list<int>> queue_using_list;

Here, the container is provided as a template and therefore must be declared during compilation time. What if, here comes the question, I want to be able to choose its implementation during runtime and not compilation time?

In this post, I want to show the closest solution to the problem above. Consider the following scenario. I want to build a queue that uses some container APIs. The container can be either implemented through an array or through a list. I want to be able to decide this during runtime.

The code below shows a sketch of this design pattern. First, you must create its container APIs through a purely abstract class, i.e., define container interface. Then, you implement the APIs using two implementations: using an array and using a list. Next, you define a concrete container class which takes in either one of the implementation pointer. Finally, you can now use this concrete container class to implement a queue.


#include <iostream>
#include <memory>
/*
* This code provides an example design pattern to achieve the following:
*
* We want to implement Container class using two different implementations
* We further want to select its implementation during runtime
* We also want to be able to do the same for any classes derived from Container
* We even want to be able to templatize on Container class
*/
/**
* interface that all implementations must bind to
*/
class ContainerInterface {
public:
virtual ~ContainerInterface() = default;
virtual void identify() const = 0;
virtual void push_back(int) = 0;
virtual void pop_back() = 0;
virtual size_t size() const = 0;
virtual int &front() = 0;
virtual int &back() = 0;
};
/**
* one type of implementation
*/
class Array : public ContainerInterface {
public:
Array() = default;
void identify() const override { std::cout << "array" << std::endl; }
void push_back(int x) override { /* ... */ }
void pop_back() override { /* ... */ }
size_t size() const override { /* ... */ }
int &front() override { /* ... */ }
int &back() override { /* ... */ }
private:
/* ... */
};
/**
* another type of implementation
*/
class List : public ContainerInterface {
public:
List() = default;
void identify() const override { std::cout << "list" << std::endl; }
void push_back(int x) override { /* ... */ }
void pop_back() override { /* ... */ }
size_t size() const override { /* ... */ }
int &front() override { /* ... */ }
int &back() override { /* ... */ }
private:
/* ... */
};
/**
* a concrete container taking one implementation
*/
class Container : public ContainerInterface {
public:
explicit Container(std::unique_ptr<ContainerInterface> impl)
: impl_{std::move(impl)} {}
void identify() const override { impl_->identify(); }
void push_back(int x) override { impl_->push_back(x); }
void pop_back() override { impl_->pop_back(); }
size_t size() const override { return impl_->size(); }
int &front() override { return impl_->front(); }
int &back() override { return impl_->back(); }
private:
std::unique_ptr<ContainerInterface> impl_;
};
/**
* Demonstration of templated private inheritance
* Queue is implemented through Container APIs
*/
template<typename C = Container>
class Queue : private C {
public:
explicit Queue(std::unique_ptr<ContainerInterface> impl) :
Container{std::move(impl)} {
C::identify();
}
void push(int x) { /* ... */ }
void pop() { /* ... */ }
const int& top() const { /* ... */ }
size_t size() const { return C::size(); }
bool empty() const { return C::empty(); }
};
/**
* Demonstration of public inheritance
* AdvancedContainer extends Container APIs
* but its implementations should only rely on Container APIs
* and should not depend on implementation specific of array or list
*/
class AdvancedContainer : public Container {
public:
explicit AdvancedContainer(std::unique_ptr<ContainerInterface> impl) :
Container{std::move(impl)} {}
bool empty() const { return size() == 0; }
};
int main(int argc, const char** argv) {
// two copies of implementations depending on user input, determined during run-time
std::unique_ptr<ContainerInterface> impl1, impl2;
if (std::strcmp(argv[1], "array") == 0) {
impl1 = std::unique_ptr<ContainerInterface>(new Array);
impl2 = std::unique_ptr<ContainerInterface>(new Array);
}
else {
impl1 = std::unique_ptr<ContainerInterface>(new List);
impl2 = std::unique_ptr<ContainerInterface>(new List);
}
// both Queue and AdvancedContainer implementations are determined during run-time
Queue<Container> queue{std::move(impl1)};
AdvancedContainer advancedContainer{std::move(impl2)};
advancedContainer.identify();
return 0;
}
/*
* run example
* $ ./a.out array
* array
* array
*
* $ ./a.out list
* list
* list
*/
Cool, right? Happy hacking!

1 comment:

  1. Excellent, This is an amazing superb article Keep Sharing this...
    Thanks a lot!!!!

    Germany VPS Hosting

    ReplyDelete