I'm doing the analysis for a software which I've had in mind for a long time. Its purpose is to turn sound into an image, apply graphical transformations on it, and turn it back into sound to hear the result. I plan to write it in C++ and I want to make it as modular as possible. As I am relatively inexperienced in modular design, my approach to the problem may be naive.
The app would be an assembly of several Modules
which have DataInputs
and DataOutputs
that could be connected to each other. Basically, when a module has some data to send, it can tell one of its outputs to send a DataBlock
to all of the inputs it is connected to, and these inputs would store the data block until their own module tells them to read it for further processing. This yields the following class diagram (excerpt):
class diagram summing up everything
The part I'm really not sure about is which type the inner data (DataBlock::data
) should take. Up to now I've put void*
as a type because it would allow me to statically cast it back into anything I want it to be without much trouble, but for many reasons which I'm not going to enumerate here, this is obviously not a solution I want to go with.
A solution I've also come up with is to make an abstract BaseData
type which any concrete data type would inherit from, like so:
I think this would be an okay solution, but I'm concerned about using inheritance "just" to make it so concrete data types have something in common while they represent completely different stuff. With this solution, modules would have to handle data blocks differently based on their type (given by the DataBlock::dataType
field), which I think is fine if done correctly.
What is this design worth? Am I headed in the right direction or am I going to suffer the pain of a thousand fell gods because of some flaw I overlooked? What are potential alternatives and/or well-known design patterns for that purpose?
1 Answer 1
A base class makes sense, if and only if, you can easily imagine performing operations on things of the type of the base class. In your example above (to the degree to which its fleshed out) - it appears this is not the case.
If I were designing something like the above, I would have each module define whatever API made sense for it, and feel no need to make them have inputs and outputs which could be manipulated generically. I think this is the crux of the problem you've artificially created for yourself.
If - however - you do have good reason to want to treat these three kinds of objects generically, there are two families of good ways to go about that in C++.
- templates
- variant union (tagged union)
TEMPLATES
You can write algorithms (procedures) which operate on an arbitrary 'typename T', and produce whatever types make sense for output.
VARIANT UNION To use a variant union (see https://en.wikipedia.org/wiki/Tagged_union) there are a number of libraries or language features you can use. However, problably the least weak approach would be
- https://en.cppreference.com/w/cpp/utility/variant
I'm not too fond of this approach, as its syntactically quite ugly.
-
I'll look into templates more precisely, thanks for the hint. I did think about them but couldn't see how to use them, @leftaroundabout clarified it in the comments above. :)qreon– qreon2018年07月18日 16:57:38 +00:00Commented Jul 18, 2018 at 16:57
-
I'm not sure templates are a good fit for what you've described, but they maybe at some point. Templates work well for an algorithm IF AND ONLY IF you can imagine writing a procedure DoSomething(T element) { ... do soemthing to each element which might be from different models; } This CAN happen (like sort() is a good example of something like that).Lewis Pringle– Lewis Pringle2018年07月18日 17:28:40 +00:00Commented Jul 18, 2018 at 17:28
-
If you are not used to using templates, a good way to start is to WAIT until you see you are writing duplicative code. When you see the same patterns over and over again, templates are a good way to capture that and reduce the duplication. But I think they maybe just a source of confusion in the early object oriented design phase.Lewis Pringle– Lewis Pringle2018年07月18日 17:29:49 +00:00Commented Jul 18, 2018 at 17:29
DataBlock
? Why is it necessary anyway to store different types that will only be selected at runtime (so it can't be expresses with a simple template)? I have a vague feeling of YAGNI here.DataBlocks
will store audio samples as well as pixel data, or even commands in some cases. I failed to mention that previously, sorry. IMHO, this could be useful for the audio playback module for example: it needs to receive audio samples to play as well as commands to know when to start and to stop. That's why I think I need different types of data blocks. However, they need to be all passed through the same kind of data pipelines, hence the inheritance from a common abstract type. Can you elaborate on how templates could be used in this case?DataInput<Audio>
, which in turn hasDataBlock<Audio>
which is by the type system guaranteed to only store audio, not something else. — Commands are another thing, and I daresay you should probably handle them in a separate fashion entirely.