Search code examples
c++open-closed-principleregistry-pattern

How to apply registry pattern to make "select class depend on input" obey open closed principle?


for example, I have some Fruits:

Fruit.h

#ifndef __Fruit__
#define __Fruit__
#include <string>
class Fruit{
public:
    virtual void hi(std::string username)=0;
};
#endif

Apple.h

#include "Fruit.h"
#include <stdio.h>
class Apple : public Fruit{
public:
    virtual void hi(std::string message){
        printf("Hi %s,I am apple\n",message.c_str());
    }
};

Orange.h

#include "Fruit.h"
#include <stdio.h>
class Orange : public Fruit{
public:
    virtual void hi(std::string message){
        printf("Hi %s,I am orange\n",message.c_str());
    }
};

I need to decide which to use according to input string:

#include "Apple.h"
#include "Orange.h"
int main(){
    std::string username="abc";
    std::string input="Orange";
    if(input=="Apple"){
        Apple().hi(username);
    }else if(input=="Orange"){
        Orange().hi(username);
    }
    return 0;
}

I know it doesn't obey open closed principle because adding a new Fruit needs to add a new if-else condition to map to the correct function. I heard registry pattern can make this case obey open closed principle, is it true? if so, how to implement registry pattern here?


Solution

  • This can become quite verbose codewise so I use words instead. A definition of the registry pattern :

    A registry is a global association from keys to objects, allowing the objects to be reached from anywhere. It involves two methods: one that takes a key and an object and add objects to the registry and one that takes a key and returns the object for the key

    The thing here is that the registry doesn't know how to build the object, just how to retrieve it. This is a significant difference to the creational patterns.

    Basically your fruit should can change to

    #include <string>
    class Fruit{
    public:
        virtual std::string key() const = 0;
        virtual void hi(std::string username) = 0;
    };
    

    And now you introduce a registry

    class FruitRegistry final {
    public:
        bool register(Fruit* fruit);
        Fruit* locate(std::string key);
    
    private: 
       std::map<string, Fruit*> registry;
    };
    

    The means of registering/retrieving fruits should be the same whatever the fruit. Here it can be done using a map string to fruit. You can also design the class fruit so it uses an accept method, which is quite useful when the input is complex (think initializing a fruit depending on its description).

    When it is useful ? When it is used behind an interface to access a type of resources, like img.load("cats.jpg"). The format jpg, bmp, png are quite different and might need a separate engine for each, or one for jpg, the other for both bmp and png. But the user doesn't care about those details. Note that more and more images type can be added in the future without touching the image loading.

    The issue is that to provide the nice img.load("cats.jpg") the registry mechanism underneath can be complex to design. You need to ask yourself :

    • Do the user care about apple and oranges (if no stop here, you don't need a registry)? Or does he just want to eat a fruit ?
    • How easy will it be to eat fruits the registry is implemented? (should be a single line)
    • How often or how long new types of fruits can be added? (should be forever)
    • How different are the fruits ? (should be completely, just that they can be eaten)