Let's say I have an application which uses a special network socket, which transmits data over a socket in XML format called XMLSocket.
XMLSocket inherits BaseSocket, and satisfies the Liskov Substitution Principle. We can interchange them.
And I have a client application which uses XMLSocket:
class Client:
def start(sock: BaseSocket):
sock.connect(ip, port)
sock.send(some_request)
some_response = sock.recv()
Now, we can pass XMLSocket or BaseSocket into Client.start(), and this code works.
We could even add on more derivations of BaseSocket such as JSONSocket, and as long as the overall flow, function signatures, and return types are the same, it would still work with any of our socket classes in the hierarchy. Yet, this violates the dependency inversion principle.
Now let's create an interface ISocket:
class ISocket(ABC):
def connect(ip, port):
pass
def send(request):
pass
def recv() -> Response:
pass
And make Client.start() depend on our interface instead, therefore satisfying the dependency inversion principle:
class Client:
def start(sock: ISocket):
sock.connect(ip, port)
sock.send(some_request)
some_response = sock.recv()
Well, it seems like we've accomplished the same exact thing as the base class did. We can pass in any variation of a socket we can dream of, as long as it behaves like a socket.
So, why must it depend on an abstraction, instead of a base class?