jeudi 8 juin 2017

Organizing objects with partially shared interface

I've recently come across a couple of situations that I think could be cleaned up with a different design, but I don't know of any patterns that would fit.

In all of these situations, I have a few classes that partially share an API. For example, a logger class:

struct ILogger { virtual void log(string msg) = 0; };
struct StdOutLogger : public ILogger {
    void log(string msg) override; // Log to stdout
};
struct FileLogger : public ILogger {
    void log(string msg) override; // Log to file
};
struct GuiLogger : public ILogger {
    void log(string msg) override; // Log to GUI
    void draw();
    void clear();
};

or perhaps:

struct Graphic {
    virtual void draw();
    virtual void setPosition();
    // etc.
};
struct AnimatedGraphic : public Graphic {
    void draw() override;
    void start();
    void stop();
    void setLooping(bool loop);
};

Now, depending on who owns these objects, I might have a container of references/pointers to a common interface:

class LogManager {
    std::vector<std::unique_ptr<ILogger>> _loggers;
    // ...
};

Or I might keep the types separated and choose at runtime which one to use:

// This is already starting to get messy
class SomethingWithGraphic {
    std::unique_ptr<Graphic> _graphic;
    std::unique_ptr<AnimatedGraphic> _animatedGraphic;
    // ...
};

The first solution is fine until I need to start using the functionality that is not part of the common interface. The second solution allows me to choose the one I need, but it is error prone and requires ugly branches everywhere.

I've come up with a couple of alternative solutions, but I haven't found one that really feels right.

  1. Keep one owning container, and create additional containers that point to the owned objects, but through a different interface. (Requires that the containers be kept in sync)

  2. Add all functions to interface, but leave implementations empty for objects that don't need the extra functions. (Those functions don't really belong as part of that interface)

  3. Store variants of all potential types. (Feels like a hack, requires visitors everywhere)

Using the logger example:

//// 1 ////
struct IDrawable {
    virtual void draw() = 0;
    virtual void clear() = 0;
};
std::vector<std::unique_ptr<ILogger>> _loggers;
std::vector<IDrawable*>               _drawableLoggers;

//// 2 ////
struct ILogger {
    virtual void log(string msg) = 0;
    virtual void draw() {};
    virtual void clear() {};
};
struct StdOutLogger : public ILogger {
    void log(string msg) override; // Log to stdout
};
struct FileLogger : public ILogger {
    void log(string msg) override; // Log to file
};
struct GuiLogger : public ILogger {
    void log(string msg) override; // Log to GUI
    void draw() override;
    void clear() override;
};

//// 3 ////
std::vector<std::variant<StdOutLogger, FileLogger, GuiLogger>> _loggers;

#1 seems the most correct I think, but still not the greatest.

Does anyone know of any patterns or structures that could clean this up?

Aucun commentaire:

Enregistrer un commentaire