Interfaces
Well clearly the use of interfaces is paramount here. Think about for any given device, the types of requests you might make of it, then create an interface around it. You'd use a factory to instantiate each instance of this interface, but otherwise your program should not care about the nature of the device or which implementation is used.
interface Device {
Future<Status> getHealthStatus();
int getTimeout();
}
If there is some aspect which is particular to a device, such as the timeout should be longer through a smartphone, then have a configurable timeout property which is by default set to a higher value in the factory or in the instance itself. The program handling the request then need only read a device's timeout value to know how long to wait for a request response therefore eliminating the need to handle any device differently.
Futures
Notice that getHealthStatus()
returns Future<Status>
. By Future, I'm referring to an object representing an asynchronous response. The future instance gets returned immediately (synchronous), however it lets you do work until you need to know the actual response. Traditionally, you would get an instance of Status
by calling Future.get()
. This makes the current thread halt until the Status
instance is resolved. This gives you the flexibility to handle the call synchronously or asynchronously as required (in the former case, you'd call Future.get()
immediately after the call to getHealthStatus()
).
This concept exists in many languages. In javascript it is commonly referred to as a Promise. Using this to organize your synchronous/asynchronous calls is also important to your program no doubt.
API layer
Up to this point, we've only been talking about internal handling of requests to your program, but it is very helpful to create an explicit API layer meant to deal with the low-level grunt work of the requests and responses and to translate them into simple calls to your internal classes. Ideally handling of timeouts would be dealt with here. Be prepared to handle the possibility of timeouts and retries in this layer. The rest of your program should be abstracted away from this particular aspect. The only way the rest of your program would be made aware of problems is a proper timeout Status instance or perhaps a thrown exception, only after the operation has been deemed unrecoverable.
Conclusion
By structuring your program in this way, you have clean breaks in responsibility and device handling. By usage of interfaces for devices, your program should never care to know the type of device on the other end except during its creation. The use of Futures will make asynchronous calls straightforward and easily manageable. By using an API layer, you effectively simplify the program by focusing on the essence of request and response and allow you to easily adapt your program for future changes.