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.
-
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)
-
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)
-
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