In an effort to apply SOLID principles to a Python project that has grown organically and is in need of re-factoring, I am trying to understand how the Interface Segregation Principle can be applied to the Python language, when Interfaces don't exist as a language feature?
An interface is something that you can type hint against, literally in source code or simply informally in documentation. Python 3 supports function annotations, 3.5+ actual type hints, and even if all that wasn't there, you could still informally type hint simply in the documentation. A type hint simply says that a specific parameter is expected to have specific characteristics.
In more concrete terms:
interface Foo {
string public function bar();
}
function baz(Foo obj) { .. }
All this does is declare that whatever parameter is passed into baz
shall be an object with a method bar
which takes no arguments and returns a string. Even if Python did not implement anything at the language level to enforce this, you can still declare these things any number of ways.
Python does support two important things though: abstract classes and multiple inheritance.
Instead of interface Foo
, in Python you do this:
import abc
class Foo(abc.ABC):
@abc.abstractmethod
def bar() -> str:
pass
Instead of implements Foo
, you do:
class MyClass(Foo):
def bar() -> str:
return 'string'
Instead of function baz(Foo obj)
, you do:
def baz(obj: Foo):
obj.bar()
Due to the multiple inheritance feature, you can segregate your interfaces/abstract classes as finely as you like.
Python is based on the duck-typing principle, so instead of enforcing all this through interface declarations and inheritance, it's usually more loosely defined in terms of "parameter must be an iterable" and such, and the caller simply needs to ensure that the arguments are iterable. Abstract classes and function annotations, coupled with the right development tools, can aid developers in conforming to such contracts at various levels of enforcement.
Keeping interfaces small and to the point decreases coupling. Coupling refers to how closely connected two pieces of software are. The more an interface defines, the more an implementing class needs to do as well. That makes that class less reusable.
Let’s take a look at a simple example:
from abc import abstractmethod
class Machine:
def print(self, document):
raise NotImplementedError()
def fax(self, document):
raise NotImplementedError()
def scan(self, document):
raise NotImplementedError()
# ok if you need a multifunction device
class MultiFunctionPrinter(Machine):
def print(self, document):
pass
def fax(self, document):
pass
def scan(self, document):
pass
class OldFashionedPrinter(Machine):
def print(self, document):
# ok - print stuff
pass
def fax(self, document):
pass # do-nothing
def scan(self, document):
"""Not supported!"""
raise NotImplementedError('Printer cannot scan!')
class Printer:
@abstractmethod
def print(self, document): pass
class Scanner:
@abstractmethod
def scan(self, document): pass
# same for Fax, etc.
class MyPrinter(Printer):
def print(self, document):
print(document)
class Photocopier(Printer, Scanner):
def print(self, document):
print(document)
def scan(self, document):
pass # something meaningful
class MultiFunctionDevice(Printer, Scanner): # , Fax, etc
@abstractmethod
def print(self, document):
pass
@abstractmethod
def scan(self, document):
pass
class MultiFunctionMachine(MultiFunctionDevice):
def __init__(self, printer, scanner):
self.printer = printer
self.scanner = scanner
def print(self, document):
self.printer.print(document)
def scan(self, document):
self.scanner.scan(document)
printer = OldFashionedPrinter()
printer.fax(123) # nothing happens
printer.scan(123) # oops!
© 2022 - 2024 — McMap. All rights reserved.