Logic Gate Simulator - Proper Encapsulation So I was checking out the courses on InteractivePython, and I came across this example to demonstrate inheritance.

My focus is on the Connector class and how it interacts with BinaryGate and UnaryGate via the setNextPin method.



class LogicGate:

    def __init__(self,n):
        self.name = n
        self.output = None

    def getName(self):
        return self.name

    def getOutput(self):
        self.output = self.performGateLogic()
        return self.output


class BinaryGate(LogicGate):

    def __init__(self,n):
        LogicGate.__init__(self,n)

        self.pinA = None
        self.pinB = None

    def getPinA(self):
        if self.pinA == None:
            return int(input("Enter Pin A input for gate "+self.getName()+"--&gt;"))
        else:
            return self.pinA.getFrom().getOutput()

    def getPinB(self):
        if self.pinB == None:
            return int(input("Enter Pin B input for gate "+self.getName()+"--&gt;"))
        else:
            return self.pinB.getFrom().getOutput()

    def setNextPin(self,source):
        if self.pinA == None:
            self.pinA = source
        else:
            if self.pinB == None:
                self.pinB = source
            else:
                print("Cannot Connect: NO EMPTY PINS on this gate")


class AndGate(BinaryGate):

    def __init__(self,n):
        BinaryGate.__init__(self,n)

    def performGateLogic(self):

        a = self.getPinA()
        b = self.getPinB()
        if a==1 and b==1:
            return 1
        else:
            return 0

class OrGate(BinaryGate):

    def __init__(self,n):
        BinaryGate.__init__(self,n)

    def performGateLogic(self):

        a = self.getPinA()
        b = self.getPinB()
        if a ==1 or b==1:
            return 1
        else:
            return 0

class UnaryGate(LogicGate):

    def __init__(self,n):
        LogicGate.__init__(self,n)

        self.pin = None

    def getPin(self):
        if self.pin == None:
            return int(input("Enter Pin input for gate "+self.getName()+"--&gt;"))
        else:
            return self.pin.getFrom().getOutput()

    def setNextPin(self,source):
        if self.pin == None:
            self.pin = source
        else:
            print("Cannot Connect: NO EMPTY PINS on this gate")


class NotGate(UnaryGate):

    def __init__(self,n):
        UnaryGate.__init__(self,n)

    def performGateLogic(self):
        if self.getPin():
            return 0
        else:
            return 1


class Connector:

    def __init__(self, fgate, tgate):
        self.fromgate = fgate
        self.togate = tgate

        tgate.setNextPin(self)

    def getFrom(self):
        return self.fromgate

    def getTo(self):
        return self.togate


def main():
   g1 = AndGate("G1")
   g2 = AndGate("G2")
   g3 = OrGate("G3")
   g4 = NotGate("G4")
   c1 = Connector(g1,g3)
   c2 = Connector(g2,g3)
   c3 = Connector(g3,g4)
   print(g4.getOutput())

main()


And so the output might look like this:

Input Pin A for gate G1 --&gt; 0
Input Pin B for gate G1 --&gt; 0
Input Pin A for gate G2 --&gt; 1
Input Pin B for gate G2 --&gt; 0
1


So everything is working as intended, but my concern is the "two-way access" between, for example, BinaryGate and Connector.

It's bothering me that BinaryGate.getPinA can call (Connector).getFrom().getOutput() in the line return self.pinA.getFrom().getOutput(), while Connector.__init__() calls BinaryGate.setNextPin

In my mind, there should only be one direction of "visibility" between the two objects - either a *Gate can access members of a Connector, or vice-versa, but not both ways at once. Is this a flawed assumption?

Does the code as-is break encapsulation? If so, how can the code be refactored to have proper encapsulation? If not, why not?

Edit:

Just to be clear for future viewers, this is not my code, it's from InteractivePython's Data Structures course.
You are mixing up lots of different responsibilities in your classes. For instance, AndGate has to handle both the And part and all the pin input parts. It is weird that an AndGate doesn't actually take references to its input pins. Furthermore, how in this model do we connect gates to each other? By your design, a "pin" is a value comparable to an integer. What if I wanted to model something like (picking a random circuit from google):



How would I do that in your system? The Connectors confuse the logic, since in your example you're creating a bunch of gates that are floating around - and then just to see that G1 and G2 go into G3 is split across several lines, rather than ideally:

g3 = OrGate('G3', g1, g2)


As a first go, gates should know their inputs and be able to perform logic to determine their outputs:

class NandGate(LogicGate):
    def __init__(self, name, a, b):
        LogicGate.__init__(self, name)
        self.a = a
        self.b = b

    @property
    def output(self):
        return not (self.a.output and self.b.output)


This separates the concern of input handling (determining the pins) from the concern of actually emulating the gates (determining the outputs) - and is what will let us have NandGates as input to other NandGates:

A = ConstantPin('A', True)
B = ConstantPin('B', False)
N1 = NandGate('N1', A, B)
N2 = NandGate('N2', A, N1)
N3 = NandGate('N3', N1, B)
N4 = NandGate('N4', N2, N3)
N5 = NandGate('N5', N4, N4)
Q = N5.output