This is a question for the Object Design Pattern specialists.
Let's assume I have a Parser
class that is in charge of reading/parsing a stream of data (that carry information packets of different types). Each of these packets carry a different type of information, so ideally I would have a class for each type of packet (PacketTypeA
, PacketTypeB
, ... each one with its own interface).
class Parser {
public:
/* ctor */
/* dtor */
void read_packet(/* arguments */);
// methods...
private:
// more methods...
}
The method Parser::read_packet
would then go through the stream and return a class (or pointer or reference to a class) to the appropriate packet type.
Would you use void pointers for this? How about a generic class (PacketBasicInterface
) that would provide a common (partial) interface to query about the type of packet (so that any decision could then be made at runtime)?
// Pure virtual (abstract) class to provide a common (and partial) interface
class PacketBasicInterface {
public:
std::string whoAmI() const = 0;
bool amIofType(const std::string& type) const = 0;
}
// Class to access data of type A packet
class PacketTypeA : public PacketBasicInterface {
public:
// methodA_1()
// methodA_2(), ...
}
// Class to access data of type A packet
class PacketTypeB : public PacketBasicInterface {
public:
// methodB_1()
// methodB_2(), ...
}
Any thought or feedback would be very much appreciated!
Many thanks!
You do not have to use one pattern for every piece of functionality. In fact, many pieces of functionality don't use patterns at all, and some pieces will use multiple patterns.
No you can have more than one method on your strategy interface. However, in order for your strategy object to actually use the Strategy pattern, at least one of the method implementations should differ between the different strategies.
This is what std::variant is for.
I would define an enumeration class, that enumerates all possible packet types:
enum class packet_type {initialization_packet, confirmation_type, ... };
And have read_packet
return a tuple of packet_type and a variant:
typedef std::variant< ... > packet_info;
std::tuple<packet_type, packet_info> read_packet();
Don't really need a formal enumeration, but it makes it easier to figure out what to do with the variant.
A few variations on this general approach include:
Using an opaque std::string
, rather than a fixed enumeration, to specify the packet type.
Using std::any
instead of a formal std::variant
.
Instead of using a simple enumeration, or an opaque token like a std::string
, use a slightly non-trivial class to define the packet type, with the class's methods taking the variant metadata as parameters, and encapsulating the operations that can be done on the packet.
Of course, as noted in the cited link, std::variant
requires C++17. Which would be a good argument for you to update your compiler: you get a simple way to implement a completely type-safe approach.
Double dispatching can be the way to go if you are looking for a design pattern from the realm of object oriented programming.
It follows a minimal, working example:
#include<iostream>
struct Visitor;
struct PacketBasicInterface {
virtual void accept(Visitor &) = 0;
};
struct PacketTypeA: PacketBasicInterface {
void accept(Visitor &) override;
};
struct PacketTypeB: PacketBasicInterface {
void accept(Visitor &) override;
};
struct Visitor {
void visit(PacketTypeA) {
std::cout << "PacketTypeA" << std::endl;
}
void visit(PacketTypeB) {
std::cout << "PacketTypeB" << std::endl;
}
};
void PacketTypeA::accept(Visitor &visitor) {
visitor.visit(*this);
}
void PacketTypeB::accept(Visitor &visitor) {
visitor.visit(*this);
}
struct Parser {
PacketBasicInterface * read_packet() {
return new PacketTypeB{};
}
};
int main() {
Visitor visitor;
auto *packet = Parser{}.read_packet();
packet->accept(visitor);
delete packet;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With