How to correctly use abstract classes (problems with testing)

Viewed 73

I came over some problem with abstract classes and it is that there is problem to test them (the common part of the code without creating concrete implementation) and I would like to ask how should solve this problem correctly.

I need to have class representing current status (I use observer pattern here) of variables which I read from PLC and I also need to write to some of these variables. I would like this part to be connection independent (OPC UA, MODBUS ...) That's why I created class PlcData which represents all variables I need to write or read, this was an abstract class and I wanted to create different implementations based on connection type (PlcDataOpcua, PlcDataModbus ...)

But when I was trying to write unit tests I came over this problem, that I can not test the common code shared between all PlcData.

Based on the second most upvoted answer here I came up with something like this: enter image description here

But because the Connection needs to update informations in PlcData it needs to keep track of the PlcData instance. For me it seams more complicated than the original solution but of course it would be "testable".

Could anyone tell me which way to go or came up with better solution?

3 Answers

The PLC data and the connection type are now separated in a clean way. I often see designs like your original one. They try to implement exchangable functionality by inheriting from the "common case". After a while they get really messy.

To enable the Connections to update data in PlcData you should consider the observer pattern, which enables you to keep the connections decoupled. The connections would provide ways of registering "callbacks" that would be called on certain state changes. If the amount of data is small, it could be passed along in the call or the callback function would "turn around" and pull the data from the connection.

interface Connection {
    void registerLevelChange(Runnable callback);
}

class ModBusConnection implements Connection {
    private List<Runnable> levelChangeCallbacks = new ArrayList<Runnable>();

    public void registerLevelChange(Runnable callback) {
        levelChangeCallbacks.add(callback);
    }

    void processIncomingData() {
        if (level.hasChanged()) {
            for (var cb: levelChangeCallbacks) {
                cb.run();
            }
        }
    }
}

class PlcData {
    private Connection connection;

    void someMethod() {
        ...
        connection.registerLevelChange(() -> handleLevelChange());
        ...
    }    

    void handleLevelChange() {
        ...
    }
}

You can also create a parallel test case class hierarchy. See my answer here.

Related