4
\$\begingroup\$

I've been watching digital technology in the last few days. This has aroused my interest. I would like to realize a program that simulates basic components of circuit technology and their functions. I have developed a small approach on how to implement the elements of circuit technology on an object-oriented level. Is that a good way of thinking, with which you can continue working? Or could one formulate the building blocks more elegantly in code?

Output.java

/* objects of classes that implement this interface can be used as signaler */

public interface Output {
    public boolean getOutput();
}

Signal.java

public class Signal implements Output {
    private boolean signal;

    public Signal(boolean value) {
        setSignal(value);
    }

    public void setSignal(boolean value) {
        signal = value;
    }

    public boolean getOutput() {
        return signal;
    }
}

LinkingElements.java

abstract public class LinkingElement {
    Output signal1;
    Output signal2;

    public LinkingElement(Output signal1, Output signal2) { 
        this.signal1 = signal1;
        this.signal2 = signal2;
    }
}

And.java

public class And extends LinkingElement implements Output {
    public And(Output signal1, Output signal2) { 
        super(signal1, signal2);
    }

    public boolean getOutput() {
        return signal1.getOutput() && signal2.getOutput();
    }
}

Or.java

public class Or extends LinkingElement implements Output {
    public Or(Output signal1, Output signal2) {
        super(signal1, signal2);
    }

    public boolean getOutput() {
        return signal1.getOutput() || signal2.getOutput();
    }
}

LightBulb.java

public class LightBulb {
    private Output signal;

    public LightBulb(Output signal) {
        this.signal = signal;
    }

    public void isActive() {
        if(signal.getOutput()) {
            System.out.println("The lightbulb is shining.");
        } else {
            System.out.println("The lightbulb is switched off.");
        }
    }
}

Main.java

This class is just for testing:

public class Main {
    public static void main(String[] args) {
        Signal s1 = new Signal(true);
        Signal s2 = new Signal(true);
        Signal s3 = new Signal(true);
        Signal s4 = new Signal(false);
        And a1 = new And(s1, s2);
        Or a2 = new Or(s3, s4);
        And a3 = new And(a1, a2);
        LightBulb lightBulb = new LightBulb(a3);
        lightBulb.isActive();
    }
}
\$\endgroup\$
1
\$\begingroup\$

The comments of @SimonForsberg point to some issues in my solution:

depending on the order of enum constants

    Signal output = inputs.values().stream() // Java8 functional programming
            .max((s1, s2) -> s1.compareTo(s2))// depends on order in enum!
            .get();

Instead of getting the "maximum" of the input signal we should check if there is any occurrence of Signal.ON in the input values and return Signal.OFF otherwise. This can also easily solved using Java8 streams:

    Signal output = inputs.values().stream() // Java8 functional programming
            .filter((s) -> s.equals(Signal.ON))// keeps only Signal.ON
            .findFirst() // returns an Optional
            .orElse(Signal.OFF);

constructor leakage

This means that other code gets access to the object being constructed before the constructor finished the initialization of the object. I'm doing this here:

class LightBulb implements SignalReceiver {
    public LightBulb(SignalSource signalSource) {
        signalSource.triggers(this); // leaks "this"
    }

Problem is that signalSource might immediate call the method consume() defined in SignalReceiver implemented by LightBulb. There is no problem in this concrete example tough, but what if the code looks like this:

class LightBulb implements SignalReceiver {
    private Signal status;
    public LightBulb(SignalSource signalSource) {
        signalSource.triggers(this);
        status = Signal.OFF;
    }

    @Override
    public void consume(Signal signal, SignalSource from) {
        if(!status.equals(signal)) { // NullPointerException
            System.out.println("light is " + signal);
            status=signal; 
        }
    }
}

This would raise a NullPointerException in case that an implementation of triggers() immediately calls consume(). Such bugs are hard to find.

The suggested solution is not to pass this but an anonymous inner class object or lambdalike this:

    public LightBulb(SignalSource signalSource) {
        signalSource.triggers((signal,source)->consume(signal,source));
    }

But this is only half of the solution. You also must execute the call to your own method in a differed Thread so that the current thread gets time to finish the initialization:

    public LightBulb(SignalSource signalSource) {
        signalSource.triggers(
            (signal,source)-> 
                 new Thread(()->consume(signal,source))
                 .start());
    }

Though this will not guarantee that the NPE from my example is never raised. This is because it depends on the complexity of the code in the triggers() method, the systems load and most likely some other hidden conditions. But it drastically reduces the likelihood.

So the best way is not to call methods on constructor parameters at all especially if they require passing this.

But In our example the problem is that the LogicElement objects need to know how all of their inputs. So we have to choose from 3 evil options:

  1. passing SignalSource objects to SignalReceiver constructor and register with them there risking leakage of this.
  2. passing SignalSource objects to SignalReceiver constructor but register with them outside the constructor risking to forget that at the other place.
  3. not passing SignalSource objects to SignalReceiver constructor and do both, register SignalReceiver with SignalSource objects and vice versa at that other place. The downside of this is that we must extend the interface of SignalReceiver with an additional method which is no needed for all implementations.

So yes: you should avoid calling methods on constructor parameters passing this to them but sometimes it is the lesser evil...

duplicated code

My approach has potentially duplicated code. I.e.: the And gate would look almost the same like the class Or. The solution to this is to create a generic class having the common behavior and injecting the differences.

Therefore I first create the class And:

class And implements LogicElement {
    private final Collection<SignalReceiver> receivers = new HashSet<>();
    private final Map<SignalSource, Signal> inputs = new HashMap<>();

    And(SignalSource... signalSources) { // "vararg" parameter, accepts many sources
        for (SignalSource signalSource : signalSources) {
            signalSource.triggers(this);
            inputs.put(signalSource, Signal.OFF);
        }
    }

    @Override
    public void triggers(SignalReceiver r) {
        receivers.add(r);
    }

    @Override
    public void consume(Signal signal, SignalSource from) {
        inputs.put(from, signal);
        Signal output = inputs.values().stream() // Java8 functional programming
                .filter((s) -> s.equals(Signal.OFF))// keeps only Signal.ON
                .findFirst() // returns an Optional
                .orElse(Signal.ON);
        for (SignalReceiver signalReceiver : receivers) {
            signalReceiver.consume(output, this);
        }
    }
}

The only difference is the position of the constants Signal.OFF and Signal.ON which is not behavior but data. This means there should be only one class (defining the behavior) which is configured through the constructor:

class MultiInputLogic implements LogicElement {
    private  final Signal signalTorReturnForEmptyFilteredInput ;
    private  final Signal signalToFilter ;
    private final Collection<SignalReceiver> receivers = new HashSet<>();
    private final Map<SignalSource, Signal> inputs = new HashMap<>();

    MultiInputLogic(Signal signalTorReturnForEmptyFilteredInput, Signal signalToFilter ,SignalSource... signalSources) {
        this.signalTorReturnForEmptyFilteredInput = signalTorReturnForEmptyFilteredInput;
        this.signalToFilter = signalToFilter;
        for (SignalSource signalSource : signalSources) {
            signalSource.triggers(this);
            inputs.put(signalSource, Signal.OFF);
        }
    }

    @Override
    public void triggers(SignalReceiver r) {
        receivers.add(r);
    }

    @Override
    public void consume(Signal signal, SignalSource from) {
        inputs.put(from, signal);
        Signal output = inputs.values().stream() // Java8 functional programming
                .filter((s) -> s.equals(signalToFilter))// keeps only Signal.ON
                .findFirst() // returns an Optional
                .orElse(signalTorReturnForEmptyFilteredInput);
        for (SignalReceiver signalReceiver : receivers) {
            signalReceiver.consume(output, this);
        }
    }
}

But this lead to another "issue" when we use this new class:

    new LightBulb(new MultiInputLogic(Signal.OFF,Signal.ON, s1, s2)); // and
    new LightBulb(new MultiInputLogic(Signal.ON,Signal.OFF, s1, s2)); // or

The order of the two new parameters of same type decides what function is implemented. This is called primitive obsession. To resolve this we need to create more data objects, e.g. as a new enum:

enum Logical{ 
  And{
     Signal getFilterSignal(){
        return Signal.ON;
     }
     Signal getReturnSignal(){
        return Signal.OFF;
     },
  OR{
     Signal getFilterSignal(){
        return Signal.OFF;
     }
     Signal getReturnSignal(){
        return Signal.ON;
     };
  abstract Signal  getFilterSignal();
  abstract Signal  getReturnSignal();
}

We change the constructor of our class MultiInputLogic:

MultiInputLogic(Logical  function,SignalSource... signalSources) {
            this.signalTorReturnForEmptyFilteredInput = function.getReturnSignal();
            this.signalToFilter = function.getFilterSignal();

Then the usage is:

    new LightBulb(new MultiInputLogic(Logical.AND, s1, s2)); // and
    new LightBulb(new MultiInputLogic(Logical.OR, s1, s2)); // or
\$\endgroup\$
1
\$\begingroup\$

Thanks for sharing your code.

What you posted is a nice data structure.

But OOP is about behavior. (we still need data structures to store state but usually objects have behavior.

Let see is this way: There are 3 Types of component in an electronic system:

  1. signal source (such as switches and sensors)
  2. signal receivers (such as Light bulbs, LEDs or electric engines)
  3. logical elements (such as And, OR NOT).

The signals are no objects (with behavior since they are just messages transfered between the objects...

So I'd suggest classes like this:

// signal are still data structure
enum Signal { OFF, ON }

// some interfaces for abstraction
interface SignalReceiver {
    void consume(Signal signal, SignalSource from);
}
interface SignalSource {
    void triggers(SignalReceiver r);
}
interface LogicElement extends SignalReceiver, SignalSource {}

// a switch is a typica "signalSource"
class Switch implements SignalSource {
    private final Collection<SignalReceiver> receivers = new HashSet<>();
    private Signal state = Signal.OFF;
    private final String name;

    public Switch(String name) {
        super();
        this.name = name;
    }

    @Override
    public void triggers(SignalReceiver r) {
        receivers.add(r);
    }

    public void doSwitch() {
        state = Signal.values()[1 - state.ordinal()]; // get the other signal
        System.out.print("Switch "+name+" is "+state +" ");
        for (SignalReceiver signalReceiver : receivers) {
            signalReceiver.consume(state, this);
        }
    }
}

class LightBulb implements SignalReceiver {
    public LightBulb(SignalSource signalSource) {
        signalSource.triggers(this); // tells the source "trigger me!"
    }

    @Override
    public void consume(Signal signal, SignalSource from) {
        System.out.println("light is " + signal);
    }
}

class Or implements LogicElement {
    private final Collection<SignalReceiver> receivers = new HashSet<>();
    private final Map<SignalSource, Signal> inputs = new HashMap<>();

    Or(SignalSource... signalSources) { // "vararg" parameter, accepts many sources
        for (SignalSource signalSource : signalSources) {
            signalSource.triggers(this); // tells the source "trigger me!"
            inputs.put(signalSource, Signal.OFF);
        }
    }

    @Override
    public void triggers(SignalReceiver r) {
        receivers.add(r);
    }

    @Override
    public void consume(Signal signal, SignalSource from) {
        inputs.put(from, signal);
        Signal output = inputs.values().stream() // Java8 functional programming
                .max((s1, s2) -> s1.compareTo(s2))// depends on order in enum!
                .get();
        for (SignalReceiver signalReceiver : receivers) {
            signalReceiver.consume(output, this);
        }
    }
}

And this is the test:

public class Electronics {
    public static void main(String[] args) {
        // circuit configuration
        Switch s1 = new Switch("left");
        Switch s2 = new Switch("right");
        new LightBulb(new Or(s1, s2));

        // operation        
        s1.doSwitch(); // light on
        s1.doSwitch(); // light off
        s2.doSwitch(); // light on
        s1.doSwitch(); // light on
        s2.doSwitch(); // light on
        s1.doSwitch(); // light off
    }
}

Yo may notice that I my approach has no "getter" method.
This is because "getter" as well as "setter" violate one of the most important OO principles: information hiding / encapsulation. You should avoid them on classes implementing actual behavior (on pure data objects they are OK...) The "rule of thumb" is: "Tell!, don't ask!"

\$\endgroup\$
  • 2
    \$\begingroup\$ I wouldn't recommend depending on the order of an enum. I've been there, done that, caused bugs whenever I switched the order. Leaking the this object from a constructor is also not recommended. And if you want to implement several different logical gates there would be a lot of code duplication at the moment. You could extract that logic to a lambda and inject it into the logical gate. \$\endgroup\$ – Simon Forsberg Dec 9 '17 at 17:58
  • \$\begingroup\$ @SimonForsberg Thanks for the comments. Eventually I was thinking of the same problems before posting but the OP is new to this stuff and I wanted this to introduce too much concepts. On the other hand you ear right: posting code having issues may be less helpful... \$\endgroup\$ – Timothy Truckle Dec 9 '17 at 20:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.