Can Interface Segregation Principle be applied to Python objects?
Asked Answered
A

2

15

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?

Anvers answered 9/10, 2015 at 11:50 Comment(1)
I didn't mention that this was asked about a 2.7 project, but I am interested in the answers for 3.x and 2.7.Anvers
W
19

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.

Writeoff answered 9/10, 2015 at 12:13 Comment(3)
Thanks for the comprehensive answer. I forgot to mention this is a 2.7 project, but you cover that too. I look forward to exploring the more advanced features of Python 3 in the future.Anvers
Because of Python's famous duck typing feature, interfaces are not used as extensively as in other programming languages. If an object walks like a duck and swims like a duck, then Python is happy to treat it as a duck, even if it is a swan!Haight
Thanks @deceze. It seems like, in Python, interface segregation comes down to dependency injection: as long as my function/class takes its dependency as a parameter rather than declaring it itself, and as long as the dependency "walks and quacks like a duck", then interface segregation has more or less been achieved.Sirloin
H
2

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!
Hindrance answered 7/3, 2021 at 22:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.