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.