I'm looking for feedback/recommendations/best-practices to accomplish the following architecture in the most elegant, expressive, and extensible fashion. I've tried a few approaches, including multiple inheritance and composition OOP as well as a "flat" classless C-like approach, but all require boilerplate and repetitive code which seems "smelly" to me. I'm convinced there should be a better way. What other design patterns should I consider? Ways to use templates and static typing? Other ways to achieve the necessary polymorphism?
Problem Statement:
"Device" objects can have different "Capabilities" which are supported depending on the concrete type of the device. Each type of concrete device's supported capabilities is known at compile time, so there is no need to assign different capabilities at run-time. Capabilities don't need to interact with other capabilities that any given device supports.
ConcreteDevices
(A,B, & C in example below) inherit the BaseDevice
class. BaseDevice
contains functionality common to all devices. BaseDevice
is also the interface class on which a client will invoke a DoCapability_N_()
method, possibly without knowing the exact type of ConcreteDevice
with which they are interacting (e.g. via a collection of devices stored in a std::list<BaseDevice*>
). Run-time polymorphism required here.
If the ConcreteDevice
has been "assigned" the N capability, the DoCapability_N_()
method should be delegated to the associated ImplCapability_N_
class, otherwise the fallback DoCapability_N_()
method from BaseDevice
should be called, indicating that capability hasn't been assigned to this device.
The Capability implementations, ImplCapability
(1,2, & 3 in the example below), inherit common functionality from the BaseCapability
class. These implementations also need access to the BaseDevice
's public members in order to perform their work.
Here is some basic "starter" code which isn't fully functional but expresses the basic class design. Comments indicate the missing pieces and possible approaches:
class BaseCapability {
public:
void* mCapabilityHandle{ nullptr };
int mBaseCapabilityData{ 0 };
};
class ImplCapability1 : public BaseCapability {
public:
// QUESTION: how to make sure this method gets called if invoked via a device which has been assigned this capability?
// QUESTION: how to make sure this method can access public members of BaseDevice? (dependency injection messy; not shown)
void DoCapability1() { cout << "Doing Capability1!\n"; }
ImplCapability1() { cout << "Capability1 assigned.\n"; }
};
class ImplCapability2 : public BaseCapability {
public:
void DoCapability2() { cout << "Doing Capability2!\n"; }
ImplCapability2() { cout << "Capability2 assigned.\n"; }
};
class ImplCapability3 : public BaseCapability {
public:
void DoCapability3() { cout << "Doing Capability3!\n"; }
ImplCapability3() { cout << "Capability3 assigned.\n"; }
};
class BaseDevice {
public:
std::unique_ptr<ImplCapability1> Cap1Ptr{ nullptr }; // use for composition, construct in ConcreteDevice as necessary
std::unique_ptr<ImplCapability2> Cap2Ptr{ nullptr }; // use for composition
std::unique_ptr<ImplCapability3> Cap3Ptr{ nullptr }; // use for composition
void* mDeviceHandle;
int mBaseDeviceData{ 42 };
// default functions to invoke if no capabilities have been assigned to the ConcreteDevice
void DoCapability1() { cout << "Capability not assigned.\n"; }
void DoCapability2() { cout << "Capability not assigned.\n"; }
void DoCapability3() { cout << "Capability not assigned.\n"; }
};
// QUESTION: how to "assign" supported capabilities here to each type of concrete devices?...
class ConcreteDeviceA : public BaseDevice, public ImplCapability1, public ImplCapability3 { // MULTIPLE INHERITANCE?
public:
// needs Capability1, Capability3
ConcreteDeviceA() { cout << "DeviceA constructed.\n\n"; };
};
class ConcreteDeviceB : public BaseDevice {
public:
// needs Capability2
ConcreteDeviceB() {
Cap2Ptr = std::make_unique<ImplCapability2>(); // COMPOSITION?
cout << "DeviceB constructed.\n\n"; };
};
class ConcreteDeviceC : public BaseDevice {
public:
// needs Capability1, Capability2, Capability3
// .. some other method?
ConcreteDeviceC() { cout << "DeviceC constructed.\n\n"; };
};
And desired use:
int main()
{
// create devices
std::unique_ptr<BaseDevice> DeviceA = std::make_unique<ConcreteDeviceA>(); // has capabilities: 1, 3
std::unique_ptr<BaseDevice> DeviceB = std::make_unique<ConcreteDeviceB>(); // has capabilities: 1
std::unique_ptr<BaseDevice> DeviceC = std::make_unique<ConcreteDeviceC>(); // has capabilities: 1, 2, 3
std::cout << '\n';
// test capability assignments by dereferencing pointer to base
DeviceA->DoCapability1(); // should do Capability 1
DeviceA->DoCapability2(); // should indicate not assigned
DeviceA->DoCapability3(); // should do Capability 3
std::cout << '\n';
DeviceB->DoCapability1(); // should indicate not assigned
DeviceB->DoCapability2(); // should do Capability 2
DeviceB->DoCapability3(); // should indicate not assigned
std::cout << '\n';
DeviceC->DoCapability1(); // should do Capability 1
DeviceC->DoCapability2(); // should do Capability 2
DeviceC->DoCapability3(); // should do Capability 3
}
As for why I have a feeling that neither Multiple Inheritance nor Composition are the most elegant approach:
Composition:
- having to write pass-thru functions which "wrap" the
ImplCapability
'sDoCapability_N_()
method within theBaseDevice
'sDoCapability_N_()
method - having to explicitly check if a Capability has been assigned via testing
if(CapNPtr != nullptr)
... - passing of injected dependencies in constructors and correspondingly longer initialization lists
- uses run-time polymorphism when compile-time should suffice for the
ConcreteDevice
toImplCapability
relationships
Multiple inheritance:
- injected dependencies with lots of parent class constructors and correspondingly longer initialization lists
- less extensible: what if in the future we wanted to give DeviceX two different Capability1's (with slightly different parameterization)? We would have to create a new "
ImplCapability4
" and copy a bunch of code. - uses run-time polymorphism when compile-time should suffice for the
ConcreteDevice
toImplCapability
relationships
To summarize:
What is the recommended strategy or design pattern to achieve this "assignment" of capabilities to devices? -> Multiple inheritance? Composition? Template-based approaches (mixins, CRTP), Other?
What is the recommended strategy to achieve the polymorphism that automatically selects the ImplCapability
's DoCapability_N_()
method if that capability has been assigned to the device?
-> Virtual functions & overriding? Templates? Overloading? Other?
What is the recommended strategy to insure the ImplCapability
classes can access the BaseDevice
members? -> Inheritance? Dependency injection? Other?
Aucun commentaire:
Enregistrer un commentaire