I have a type of "PC" (i.e. personal computer) that I need to model in C++ (can use language standards as new as C++11
, but no newer than that). In short, we have a lot of granularity for components in the PC (in my actual tool, it would be about 10 times the number of components shown here), i.e:
Tower
Motherboard
CPU
CORE1
CORE2
CORE3
CORE4
RAM_BANK
DIMM1
DIMM2
DIMM3
DIMM4
SATA_Controller
Drive1
Drive2
Drive3
PCIE_BUS
VideoCard1
VGA_OUT
HDMI_OUT
DVI_OUT
VideoCard2
VGA_OUT
HDMI_OUT
Power_Button
I would like to setup a common base class for all objects in the tree above so that I can iterate through all objects that use this common base class. Ideally, I'd like to be able to print off the inheritance tree shown above, in as clean a manner as possible. This is because I might have to support thousands of different "PCs" with different types of mothers boards, disk controllers, CPU types; some models may have components others don't have, etc.
In addition to being able to generate the tree shown above, in a generalized manner, I have some component-detection logic that needs to be implemented. For example, the motherboard
class, upon detecting a specific version of a CPU
class, will use a specific interface to communicate with that instantiation of that particular version of the CPU
class, e.g. "Mother board detected an AMD64 CPU, version/model XYZ, use this interface to talk to it". The end goal is a re-usable way to assemble virtual "PCs", and automatically assign the appropriate interface for a parent to communicate bi-directionally with a child object.
Which design pattern should I consider for such an implementation? I was thinking of a simple common base class (i.e. widget
class) which all other classes inherit. Then, the widget class has an abstract data type (list) that contains a reference/pointer to all other members in that class that also inherit from the widget
parent class. However, I can't come up with a method to auto-add all widget
-type child classes to the list. For example:
class widget {
private:
std::list<widget> children;
protected:
char name[256];
public:
void announce()
{ printf("Component name:%s.", this->name);}
};
class MotherBoard: public widget {
private:
CPU cpu1;
CPU cpu2;
RAM_bank rambank1;
public:
MotherBoard() {
// Add all classes that derive from widget to this list.
// i.e. cpu1, cpu2, rambank1.
// Also, construct `cpu1` with a generic interface that
// accepts 2 parameters: this.name (for this particular
// MotherBoard instantiation) and this->cpu1.name;
}
};
class CPU: public widget {
public:
CORE core1;
CORE core2;
CORE core3;
CORE core4;
};
class RAM_BANK: public widget {
public:
DIMM dimm1;
DIMM dimm2;
DIMM dimm3;
DIMM dimm4;
};
Finally, does C++
itself support introspection enough to be able to automatically detect all members that derive from a specific class and add them the the children
list? This is so that if a new component that inherits from widget
is added as a member to the class, people can't forget to have it included in the children
list.
Thank you.
-
3Might be worth at least considering following (a subset of) the Desktop Management Task Force Common Information Model DMTF CIM.Jerry Coffin– Jerry Coffin12/06/2017 04:29:17Commented Dec 6, 2017 at 4:29
1 Answer 1
Structure
The good
Your Widget
is a tree like structure, thanks to the list of sub-widgets. The advantage is that you can always decompose a Widget
in smaller parts. This is perfect to represent the structure of your PC and to navigate between the parts.
A variant could have been the composite design pattern, but this wouldn't add much benefits here, except that you would differentiate components that have sub-components and final components which haven't.
The bad
What is missing, in your model is the bidirectional interface. The parent knows about its children, but the children don't know about their parents. SO the child needs a reference or a pointer back to its parent. You have to adapt the "assembly" process to achieve this
Another problem is that you don't use the Widget
potential to its full extent. For example, the memory bank shouldn't just have 4 sub-objects; it should use the component list as well (i.e. adding 4 DIMM to the list).
The ugly
automatically assign the appropriate interface for a parent to communicate bi-directionally with a child object
If "interface" is meant in its OO sense, you're ok. Except that the interface is only the Widget
interface. Is it appropriate enough ? (By the way, you have no virtual function which would adapt their behavior depending on the object class.)
If not, or if "interface" if it's meant in the domain world meaning (i.e. SATA, PCI, etc...), then you don't comply:
- If CPU sends something to the memory bank, the memory bank should dialogue with its children in a bus fashion (i.e. all the messages to the bank are sent to all its children).
- the SATA controller of your model doesn't use SATA interface to dialogue with its children (and by the way, the S of SATA means Serial, so that one child is connected to the next and not to the SATA controller).
So after restructuring for the bidirectional and further componentization, you could further add some methods to emulate the specific PC-interfaces. But this puts the Widget
approach in question, because each sub class of widget would have a different interface, and you would never be sure if you use the right one ! For this you would need to use polymorphism and dynamic_cast
, which will not be a piece of cake...
Alternative structure
An alternative design is to remove the general Widget
list out of the Widget
class, and add a list only to the classes that need it. For example the memory bank would have a DIMM list and the SATA controller a DISK list. THen you'd allways use the right interface.
Navigation across your structure would then be less easy: it's still possible to foresee methods at the top that recursively dispatch to the children until the bottom. But you'd certainly need to implement the visitor pattern if you want to have a generalized exploration algorithm for your structure, once for printing it out, another time to search for a component, etc... (each with a specific "visitor")
Building the structure
The end goal is a re-usable way to assemble virtual "PCs",
For the moment, you rely on the constructors to assemble the PC. If tomorrow you want to have not 4 DIMM but 8 ? What it you want to add a co-processor ? How do you know how many disks to add when you create a controller ? This approach is not realistic and not reusable.
You need to look at the builder pattern to be able to construct the parts separately and assemble them. Perhap's you'll even need a separate builder for each configurable component.
To introspect or not to introspect ?
With the two approaches (improvement of your model or alternative), and the builder, it's not needed to do any introspection.
If you need it at runtime, you need to make your class polymorphic by having at least one virtual function (e.g. a virtual destructor in the base class). You can look for dynamic_cast
, which is a try-error approach to introspection. Or at type_info
, but which has only limited use as type names are not standardized.
At compile time, there is is_same
and is_base_of
, but this won't help you at all in your case.
Explore related questions
See similar questions with these tags.