Search code examples
javapolymorphismdowncast

Downcasting or discard polymorphism - what's the smaller evil?


So imagine we have a base class Message:

public abstract Class Message {

    Object content;

    public Message(Object content) {
        this.content = content;
    }
}

And various implementations:

public Class Packet extends Message {

    public Packet(Long largeNumber) {
        super(largeNumber);
    }

    public Long unpack() {
        return (Long) content;
    }
}

public Class Letter extends Message {

    public Letter(Short smallNumber) {
        super(smallNumber);
    }

    public Short unpack() {
        return (Short) content;
    }
}

Now suppose we have a sender class, that sends the Messages somewhere.

public Class Sender {

    public send(Message msg) {
        // send it somewhere
    }
}

And a receiver class, that receives the Message:

public Class Receiver {

    receive(Message msg) {
       // do something with the msg 
   }
}

The receiver class however just gets the Super class Message and doesn't know beforehand, which subclass it will receive. So how would I now "unpack" the message?

If we assume that I knew exactly what message would land where, I could use downcasting like this:

Packet packet = (Packet) msg;

But somehow this feels wrong as it kind of dismisses the point of polymorphism to begin with. Would it be better to just send the absolute sub-messages? Or is there a solution to such a problem I don't see (e.g. using Generics in some variation - I'm not too familiar with them)?


Solution

  • Unpacking the message can be done by the message itself, using the visitor pattern:

    public abstract Class Message {
        void send(Receiver r) {
            r.receive(this); // Catch-all
        }
    }
    
    public Class Packet extends Message {
        void send(Receiver r) {
            r.receive(this); // Overload for packets
        }
    }
    
    public Class Letter extends Message {
        void send(Receiver r) {
            r.receive(this); // Overload for letters
        }
    }
    
    public Class Receiver {
        // There is an overload for each subclass
        receive(Packet packet) {
        }
        receive(Letter letter) {
        }
        // This is the catch-all implementation
        receive(Message msg) {
        }
    }
    

    This approach lets receiver process letters and packets separately, in a statically-typed context. Catch-all implementation is often used for error reporting.