Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to design a type safe message API in Java?

Tags:

java

api

I have a Java client which wants to communicate with a device through messages over serial communication. The client should be able to use a clean API, abstracting the ugly details of the serial communication. The client can send many types of messages through that API and gets responses. I'm searching for advice which way is best to implement this API.

For simplicity, say we have only two message types: HelloMessage which triggers a HelloResponse and InitMessage which triggers an InitResponse (in reality, there are many more)

Designing the API (that is, the Java abstraction of the device) I could have:

One method per message type:

public class DeviceAPI {
  public HelloResponse sendHello(HelloMessage){...}
  public InitResponse sendInit(InitMessage){...}
  ... and many more message types ....

This is nicely type safe. (It could also be many times the same send() method, overloaded, but that's about the same). But it is very explicit, and not very flexible - we cannot add messages without modification of the API.

I could also have a single send method, which takes all message types:

class HelloMessage implements Message
class HelloResponse implements Response   
...
public class DeviceAPI {
  public Response send(Message msg){
    if(msg instanceof HelloMessage){
       // do the sending, get the response
       return theHelloResponse
    } else if(msg instanceof ...

This simplifies the API (only one method) and allows for additional Message types to be added later without changing the API. At the same time, it requires the Client to check the Response type and cast it to the right type.

Client code:

DeviceAPI api = new DeviceAPI();
HelloMessage msg = new HelloMessage();
Response rsp = api.send(msg);
if(rsp instanceOf HelloResponse){
    HelloResponse hrsp = (HelloResponse)rsp;
    ... do stuff ...

This is ugly in my opinion.

What do you recommend? Are there other approaches which give cleaner results?

References welcome! How did others solve this?

like image 349
Philipp Avatar asked Dec 11 '13 09:12

Philipp


2 Answers

Here is a way to do it in type-safe (and extensible) way using generics:

public interface MessageType {    
    public static final class HELLO implements MessageType {};
}

public interface Message<T extends MessageType> {
    Class<T> getTypeClass();
}

public interface Response<T extends MessageType> {
}

public class HelloMessage implements Message<MessageType.HELLO> {

    private final String name;

    public HelloMessage(final String name) {
        this.name = name;
    }

    @Override
    public Class<MessageType.HELLO> getTypeClass() {
        return MessageType.HELLO.class;
    }

    public String getName() {
        return name;
    }

}

public class HelloResponse implements Response<MessageType.HELLO> {

    private final String name;

    public HelloResponse(final String name) {
        this.name = name;
    }

    public String getGreeting() {
        return "hello " + name;
    }

}

public interface MessageHandler<T extends MessageType, M extends Message<T>, R extends Response<T>> {
    R handle(M message);
}

public class HelloMessageHandler 
    implements MessageHandler<MessageType.HELLO, HelloMessage, HelloResponse> {
    @Override
    public HelloResponse handle(final HelloMessage message) {
        return new HelloResponse(message.getName());
    }
}

import java.util.HashMap;
import java.util.Map;

public class Device {

    @SuppressWarnings("rawtypes")
    private final Map<Class<? extends MessageType>, MessageHandler> handlers =
        new HashMap<Class<? extends MessageType>, MessageHandler>();

    public <T extends MessageType, M extends Message<T>, R extends Response<T>> 
        void registerHandler(
            final Class<T> messageTypeCls, final MessageHandler<T, M, R> handler) {
        handlers.put(messageTypeCls, handler);
    }

    @SuppressWarnings("unchecked")
    private <T extends MessageType, M extends Message<T>, R extends Response<T>> 
        MessageHandler<T, M, R> getHandler(final Class<T> messageTypeCls) {
        return handlers.get(messageTypeCls);
    }


    public <T extends MessageType, M extends Message<T>, R extends Response<T>> 
        R send(final M message) {
        MessageHandler<T, M, R> handler = getHandler(message.getTypeClass());
        R resposnse = handler.handle(message);
        return resposnse;
    }

}

public class Main {
    public static void main(final String[] args) {
        Device device = new Device();
        HelloMessageHandler helloMessageHandler = new HelloMessageHandler();
        device.registerHandler(MessageType.HELLO.class, helloMessageHandler);

        HelloMessage helloMessage = new HelloMessage("abhinav");
        HelloResponse helloResponse = device.send(helloMessage);
        System.out.println(helloResponse.getGreeting());
    }
}

To add support for a new message type, implement MessageType interface to create a new message type, implement Message, Response and MessageHandler interfaces for the new MessageType class and register the handler for the new message type by calling Device.registerHandler.

like image 161
Abhinav Sarkar Avatar answered Oct 23 '22 08:10

Abhinav Sarkar


I've got a fully working example now of what you want:

To define the types of messages:

public interface MessageType {
    public static class INIT implements MessageType { }
    public static class HELLO implements MessageType { }
}

Base Message and Response classes:

public class Message<T extends MessageType> {

}

public class Response<T extends MessageType> {

}

Create custom init messages and responses:

public class InitMessage extends Message<MessageType.INIT> {
    public InitMessage() {
        super();
    }

    public String getInit() {
        return "init";
    }
}

public class InitResponse extends Response<MessageType.INIT> {
    public InitResponse() {
        super();
    }

    public String getInit() {
        return "init";
    }
}

Create custom hello messages and responses:

public class HelloMessage extends Message<MessageType.HELLO> {
    public HelloMessage() {
        super();
    }

    public String getHello() {
        return "hello";
    }
}

public class HelloResponse extends Response<MessageType.HELLO> {
    public HelloResponse() {
        super();
    }

    public String getHello() {
        return "hello";
    }
}

The DeviceAPI:

public class DeviceAPI {
    public <T extends MessageType, R extends Response<T>, M extends Message<T>> R send(M message) {
        if (message instanceof InitMessage) {
            InitMessage initMessage = (InitMessage)message;
            System.out.println("api: " + initMessage.getInit());
            return (R)(new InitResponse());
        }
        else if (message instanceof HelloMessage) {
            HelloMessage helloMessage = (HelloMessage)message;
            System.out.println("api: " + helloMessage.getHello());
            return (R)(new HelloResponse());
        }
        else {
            throw new IllegalArgumentException();
        }
    }
}

Note that it does require an instanceof-tree, but you need that to handle what kind of message it is.

And a working example:

public static void main(String[] args) {
    DeviceAPI api = new DeviceAPI();

    InitMessage initMsg = new InitMessage();
    InitResponse initResponse = api.send(initMsg);
    System.out.println("client: " + initResponse.getInit());

    HelloMessage helloMsg = new HelloMessage();
    HelloResponse helloResponse = api.send(helloMsg);
    System.out.println("client: " + helloResponse.getHello());
}

Output:

api: init
client: init
api: hello
client: hello

UPDATE: Added example on how to get input from the messages the client wants to send.

like image 41
skiwi Avatar answered Oct 23 '22 09:10

skiwi