Abstract method with differing concrete signatures

I’m writing a class to abstract a connection to a remote device. The connection can currently be over serial or TCP, with others possibly added in the future.

The base class ConnectionHandler is abstract. Children must implement a connect method (among others), but the signature of this method necessarily differs between connection types. For example:

class SerialHandler(ConnectionHandler):
    def connect(self, port: str, baudrate: int) -> None:
        ...

class TCPHandler(ConnectionHandler):
    def connect(self, address: str, port: int) -> None:
        ...

The differing definitions of the port parameter in these methods is also unfortunate.

My question: Is this bad design? It feels like bad design. What could a better design look like?

How likely is it that you’ll want to make more than one .connect call on the same handler, with different addresses or baud rates? It’s reasonable to choose not to support that (users that want to do so, must then make two handlers, or change the attr), and put baudrate and address in __init__, (store them in that, and take their values off attrs of self in connect)

2 Likes

At the very least, it violates the LSP principle and what is generally understood under an abstract base class. If you have an instance of ConnectionHandler, you can’t actually call the connect method, so it shouldn’t be part of that class.

1 Like

I’d be looking at how to break this up using delegation instead of inheritance. Which goes beyond the question of the OP.

But either way one needs (?) more information:

  • what is the responsibility of the ABC? What functionality does it provide?
  • what is the functionality that’s added by subclass, besides this abstract method?
  • what is the role of the connect method? How does it relate to the thing you connect to?
1 Like

Unlikely.

From a LSP perspective, is it OK for concrete classes B and C inheriting from ABC A to have different __init__ methods? If so, this is the way to go, I think.

Concrete ConnectionHandler classes must implement a number of methods for reading and writing data to the remote device. With the exception of connect, these methods all obey LSP.

The base class also provides some concrete high-level methods, e.g. for checking hardware and firmware version of the remote device.

The SerialHandler subclass additionally provides device autodetection via serial port scanning. Otherwise they currently provide the same functionality.

The remote device is controlled by writing bytes to either one of its two UART buses (UART0 and UART1). UART0 is connected to an onboard USB/UART adapter, UART1 is connected to an ESP8266 chip.

If UART0 is used, the underlying connection is provided by a pyserial.Serial instance. If UART1 is used, the underlying connection is provided by a socket.socket(socket.AF_INET, socket.SOCK_STREAM) instance. Each of these objects provide connect, with basically the same semantics as described in the OP. The SerialHandler.connect and TCPHandler.connect methods simply passthrough their arguments to the underlying connect methods.

2 Likes

Technically no, but it is a very common thing that happens in python and that type checkers deal with. It doesn’t really violate the LSP for A, but for type[A]. It means you can’t easily generally construct instances of these classes which is far less common that calling methods on these instances.

3 Likes

If TCP and Serial connections aren’t naturally substitutable for each other, violating LSP is a pragmatic design decision, that’s perfectly reasonable to make in my opinion.

The interface A is already defined. That’s the natural level for substitutability to be supported (isn’t that the point of the ABC ?). If LSP’s desirable, kwargs could be added to every method.
But that’s going to need dummy baud rates for the TCP and a dummy port for instances of the Serial.

The intrinsic difference between the two subclasses has to be dealt with somewhere. Whether that’s in a factory function, two different incompatible __init__ methods, or multiple possible call signatures to a common __init__ method (the latter with a few of extra edge cases to handle), there are pros and cons for each.

3 Likes

For the purposes of my application, they are interchangeable for the most part. The initial connection is the main difference, after that is just a matter of providing a common interface for data exchange.

Actually, in my case B is already defined. The SerialHandler is an old class, and until recently inherited object. Due to a recent addition of wireless support in the target hardware, I’m adding TCPHandler and a common base class for them. The idea is to generalize the connection as seamlessly as possible for any current users of SerialHandler.

2 Likes