Reputation: 101
I have three types of devices (USB, COM and wireless) and one PC software to connect with them. Every device have functions like connect, read and write data from RS485 network. On PC software I must implement application layer classes to work with devices. I am looking for some design pattern to write connection between application layer and transport layer. The first idea is to write abstract class DataLink and every device will inherit from the abstract class interface (pure OOP):
class DataLink {
public:
virtual bool read() = 0;
virtual bool write() = 0;
};
class USBDevice : public DataLink {
public:
bool read() { /* some code */ }
bool write() { /* some code */ }
bool specificUSBFunction() { /* some code */ }
};
class COMDevice : public DataLink {
public:
bool read() { /* some code */ }
bool write() { /* some code */ }
bool specificCOMFunction(){ /* some code */ }
};
DataLink *dl = new COMDevice();
dl->read();
dl->write();
Now if I want to use specific USB or COM function I must use ugly cast. The other problem is that this class must be singleton because we have only one device available so we cannot create multiple objects. I am looking for a good way to do this using C++ (can be v11 or v14).
Upvotes: 2
Views: 793
Reputation: 2070
You have a couple options, but my own personal experience is from the middle-ware approach. So that approach is what I will recommend (even if you aren't writing 'middleware' the 'ideas' can still be useful)
We had several different 'physical connections': Military Radio 1, Military Radio 2, Wifi, USB to Ethernet, etc. Each of these can be thought of as similar to your different connection types.
Make use of the Bridge Pattern which...
is meant to "decouple an abstraction from its implementation so that the two can vary independently". The bridge uses encapsulation, aggregation, and can use inheritance to separate responsibilities into different classes.
1) Define an interface that all of your connections will use.
2) Encapsulate the base atomic actions of each connection type into a 'helper' class. (open(), close(), read(), write(Byte[] data), etc.)
3) Write a bridge class that converts the universal interface to the 'helper class' implementation for each connection type.
4) Have some logic that determines which 'connection' should be 'active' at a given time, and associate the 'connection interface' with the bridge impl. of the connection type being used. (or list of connections if this is multi-cast sending, etc.)
That should do it. You have a single Interface that the 'rest' of your application can write/read from. and the "impl. details" are hidden inside your atomic action 'helper' class and/or bridge class.
Example Interface: // obviously extremely simple examle
interface IConnection{
byte[] read(int size);
void write(byte[] data);
bool open();
bool close();
}
And an implementation class:
class usb_wrapper{
// this is completely made up, but made up methods to show pattern as an example
// these methods are extreme exaggerations and not 'real' at all
int open(String connectionName, int id){
// returns connection_id of new connection
}
int close(int connection_id){...} // returns a flag if connection was closed
bool write128byte(byte[] data) {...} // you can only write 128 byte chunks
byte[] read128byte(){...} // you can only read 128 byte chunks
}
As you can see the snippets above the have 'similarities' but the actual methods have different parameters, different requirements, etc.
bridge class:
class usbConnectionBridge implements IConnection{
usb_connection conn = new usb_connection();
// Here is where you have the IConnection methods, inside these methods you
// have the logic to 'adapt' from these methods ... to the 'conn' object
byte[] read(int size){...}
void write(byte[] data){...}
bool open(){...}
bool close(){...}
// possibly additional helper methods below, etc.
}
So the 'bridge' class would wrap(encapsulate) the usb_wrapper and make it able to interact with the interface. Thereby allowing the decoupling of the interface(abstraction) from its implementation(usb_wrapper) so that the two can vary independently" which is the bridge pattern by definition.
Upvotes: 2
Reputation: 73446
First, as you have an abstract class, I'd suggest you strongly to consider defining an abstract constructor.
class DataLink {
public:
virtual bool read() = 0;
virtual bool write() = 0;
virtual ~DataLink() {}
};
Now creating the devices raises some questions. Your polymorphic design would rather speak for a parameterized factory method, where a parameter (configuration data ?) would tell if a COM, USB or WIFI device is to be created:
DataLink *dl = CreateDevice("COM"); // For example. COuld use an enum as well
But you add another constraint:
This class must be singleton because we have only one device available so we cannot create multiple objects.
In fact, the intent of a singleton is not only to ensure a single instance, but also to ensure a global point of access to it. If you don't need such a global access, I'd strongly recommend not to use a singleton here.
By the way, your constraint raises other questions: Do you have one device of each type ? Or do you have one device whatever its type is ? And most of all, won't it be possible one day, that you have to support several devices ?
So, conceptually speaking, even if you have only one device currently, the unicity is not a property of your generic device class nor its concrete implementations. It's only your current use case for creating the DataLink. I'd therefore recommend you implement a factory and derive an application specific factory to implement your creational constraints;
class DeviceFactory { // application independent
public:
enum DeviceType { COMDevice, USBDevice, ... };
DataLink *CreateDevice(std::string devicename, DeviceType t);
};
class MySpecificFactory : public DeviceFactory { // application specific constraints
std::map<std::string,DataLink*> objects;
public:
DataLink *CreateDevice(std::string devicename, DeviceType t) {
if (objects.count(devicename)!=0) {
// device already exists, either report an error, or
// return the previously created object with the same name (provided it has the same type)
...
}
else {
DataLink* dl = DeviceFactory::CreateDevice(devicename,t);
if (dl)
objects[devicename]=dl;
return dl;
}
}
};
The handling of link specific functions is orthogonal to the creation issue. The easiest and safest way is certainly the dynamic cast:
if (COMDevice* cd=dynamic_cast<COMDevice>(dl)) // nullptr if it isn't a COMDevice
cd->COMFunction();
else ...
It's difficult to advise on more specific patterns, without knowing the purpose of the link specific functions and how they relate in the context of your application.
Upvotes: 2