Search code examples
c++polymorphismcovariancecontravariance

C++ Covariant Parameters - Design Pattern


Take the following simplified C++ class hierarchy as an example. What I want to accomplish is that Service provides a virtual method for saving arbitrary Model objects. But each subclass of Service, e.g. BoxService should and can only save Box objects.

Due to the fact that C++ does not support covariance in method parameters I cannot simply declare the save method in BoxService.h like:

void save(Box box);

My question is, is there any preferred design pattern or best practice for that problem? Or should I check in the implementation of the save function in BoxService.cpp if the arriving Model object is of type Box and throw an exception otherwise?

Model.h

class Model {
private:
    int id;
};

Box.h

class Box : public Model {
private:
    int size;
};

Service.h

class Service {
public:
    virtual void save(Model model);
};

BoxService.h

class BoxService : public Service {
public:
    void save(Model box);
};

BoxService.cpp

void BoxService::save(Model box) {
    // TODO: save box and make sure that box is of type 'Box' and not any other subtype of 'Model'
}

Solution

  • So you sound like you want to group operation implementations by model type. I'll explain a more OOP approach.

    Separate Service from the implementations, but we're going to get rid of the pesky parameter:

    class Service { ... };
    class ServiceImpl {
        virtual ~ServiceImpl() {}
        void save() const = 0;
        void remove() const = 0;
    };
    

    Each implementation will be lightweight and support the operations, but will take the parameter in the constructor:

    class BoxService : public ServiceImpl {
        Box _b;
    
    public:
        BoxService(Box b) : _b(b) {}
    
        void save() const { ... }
        void remove() const { ... }
    };
    

    Now we have an abstract factory to create implementations as we need them:

    class ServiceImplFactory {
    public:
        std::unique_ptr<ServiceImpl> create(Model m) const {
            if (auto b = dynamic_cast<Box*>(m)) {
                return std::make_unique<BoxService>(*b);
            } else if (auto x = dynamic_cast<X*>(m)) {
                return std::make_unique<XService>(*x);
            } else {
                assert(!"Not all Models covered by Service implementations");
            }
        }
    };
    

    Now Service:

    class Service {
        ServiceImplFactory _implFactory;
    
    public:
        void save(Model m) const {
            return _implFactory.create(m)->save();
        }
    
        void remove(Model m) const {
            return _implFactory.create(m)->remove();
        }
    };
    

    Further steps:

    • Engineer a solution that gives a compile-time error instead of asserting.
    • Allow both more model types and more operations to be added without changing (much) existing code. This should be equivalent to the Expression Problem. Notice that this approach of grouping operations by model type requires a much more widespread change to add a new operation than it does to add a new model type. The reverse would be true for using a visitor and grouping model types by operation. There are solutions to the Expression Problem, such as object algebras, but they can be a bit more obscure.