DEV Community

Wasim
Wasim

Posted on

Copying objects of unknown class in Java requires using reflection. It's hideous.

In my 'Software Design and Architecture' course, we are currently learning design patterns such as the Command pattern. From Wikipedia:

... a command object is used to encapsulate all information needed to perform an action or trigger an event at a later time. This information includes the method name, the object that owns the method and values for the method parameters.

Here's a UML diagram. The purpose is to provide flexibility so that a receiver can handle many different commands without needing to be modified. An example given for our assignment is a 'universal' remote control for various household appliances: how does the manufacturer of the remote control integrate with a variety of appliances from different manufacturers?

The solution is to provide an interface which appliance manufacturers can adapt their software to and to encapsulate the commands as objects that are passed to the client. Our project assignment involved implementing a demo with an "undo" button capable of undo-ing multiple actions in a row. Here's where the headache with Java comes in:

Naturally, I created a Stack of 'Commands' and upon each call to commandName.execute(), the commandName is pushed to the stack and if the user hits 'undo', the command is popped in order to call commandName.undo():

public void onButtonWasPushed(int slot) {
    onCommands[slot].execute();
    undoCommands.push(onCommands[slot]);
}
Enter fullscreen mode Exit fullscreen mode

Here is the test driver provided by our instructor:

    RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();

    CeilingFan ceilingFan = new CeilingFan("Living Room");

    CeilingFanLowCommand ceilingFanLow = 
            new CeilingFanLowCommand(ceilingFan);
    CeilingFanMediumCommand ceilingFanMedium = 
            new CeilingFanMediumCommand(ceilingFan);
    CeilingFanHighCommand ceilingFanHigh = 
            new CeilingFanHighCommand(ceilingFan);
    CeilingFanOffCommand ceilingFanOff = 
            new CeilingFanOffCommand(ceilingFan);

    remoteControl.setCommand(0, ceilingFanLow, ceilingFanOff);
    remoteControl.setCommand(1, ceilingFanMedium, ceilingFanOff);
    remoteControl.setCommand(2, ceilingFanHigh, ceilingFanOff);

    System.out.println("4 on buttons");
    remoteControl.onButtonWasPushed(1); //ceiling fan goes to medium
    remoteControl.onButtonWasPushed(0); // ->low
    remoteControl.onButtonWasPushed(2); // ->high
    remoteControl.onButtonWasPushed(1); // ->medium
    // undo stack has 
    //   ceilingFanMedium at top
    //   ceilingFanHigh  
    //   ceilingFanLow 
    //   ceilingFanMedium at bottom

    System.out.println("undo command");
    remoteControl.undoButtonWasPushed(); // go back to high
    System.out.println("redo command");
    remoteControl.redoButtonWasPushed(); // return to medium

    // undo stack has 
    //   ceilingFanMedium at top
    //   ceilingFanHigh  
    //   ceilingFanLow 
    //   ceilingFanMedium at bottom     

    System.out.println("undo 4 commands");
    remoteControl.undoButtonWasPushed(); // go back to high
    remoteControl.undoButtonWasPushed(); // go back to low
    remoteControl.undoButtonWasPushed(); // go back to medium
    remoteControl.undoButtonWasPushed(); // SHOULD go back to off but does not in my implementation
Enter fullscreen mode Exit fullscreen mode

Now of course this did not have the output I had expected because in the driver application code, we are passing the same command object instance to the stack each time we select a particular setting (instead of a new instance of that command), and the Command knows how to undo() itself based on a non-public field where it stores the previous command:

CeilingFan ceilingFan;
int prevSpeed;

public CeilingFanMediumCommand(CeilingFan ceilingFan) {
    this.ceilingFan = ceilingFan;
}

public void execute() {
    prevSpeed = ceilingFan.getSpeed();
    ceilingFan.medium();
}

public void undo() {
    if (prevSpeed == CeilingFan.HIGH) {
        ceilingFan.high();
    } else if (prevSpeed == CeilingFan.MEDIUM) {
        ceilingFan.medium();
    } else if (prevSpeed == CeilingFan.LOW) {
        ceilingFan.low();
    } else if (prevSpeed == CeilingFan.OFF) {
        ceilingFan.off();
    }
}
Enter fullscreen mode Exit fullscreen mode

So the result of the final call to remotecontrol.undoButtonWasPushed() in the driver is to go back to 'high' setting because the reference to the Command at the bottom of the stack points to the same instance as the Command referenced at the top of the stack! That command is trying to 'undo()' something that has already been undone.

I realize I need a new instance each time commandName.execute() is called, so I look up Java's clone() method, where the object being cloned must implement it's own deep copy. Unfortunately, this requires modifying the vendor code, which the remote manufacturer wouldn't have access to. I could try a copy constructor instead but that again assumes at least knowledge of the vendor code. If I want to keep my code decoupled or I don't know the details of the vendor implementation, then I can't rely on creating a new object and copying each field directly.

What does that leave us with? Well, I realize my only option seems to be using Java's reflection API and ... I don't like it. I'm obviously a beginner so share your thoughts, is there a better way to copy unknown objects in Java or is reflection not so convoluted after all?

There is no general mechanism for this in Java. For a class to allow copying objects, it should implement a copying mechanism (for example Cloneable).

In principle, I suppose it would be possible to copy objects using reflection, picking one member at a time and building a copy, but it will be difficult to make it work. Note that it means you have to access private members too. And even if you succeed, there is no guarantee that it will work as you expect for classes that you don't control…

Top comments (3)

Collapse
 
vilfrim profile image
vilfrim

You don't tell which part is your code and which is given but if you cannot create copies of commands for your history then maybe commands should support multiple previous states? And yes, it took way too much time from me to get this example to work and I do this for living:)

Collapse
 
wasimanitoba profile image
Wasim

Yep, no better way around it besides to require changes to the Command interface (our prof says the solution is for the Command to implement Cloneable)

Collapse
 
vilfrim profile image
vilfrim

Ok..I hope that your prof answered little bit longer than that because Cloneable interface is empty. It just indicates to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class (ref. docs.oracle.com/javase/7/docs/api/...). And because remote control should be only be were of Command interface, which doesn't have a copy-method, you still have the problem orginal problem how do you create a copy of an object which implements Command interface without doing something nasty. My solution would be that the Command interface would look something like this:

void execute();
void undo();
<T extends Command> T createCopy();

Now the remote control can create easily copies.

But yeah I think this is enough chit chat about this topic :)