The Dependency Inversion Principle

The Dependency Inversion Principle (DIP) has an unfortunate name; the word inversion seems to imply there's some kind of obvious dependency and we should invert the obvious dependency rules. Practically, the principle is described as having class dependencies based on the most abstract superclass possible, not on a specific, concrete implementation class.

In languages with formal type declarations, for example, Java or C++, this advice to refer to abstract superclasses can be helpful to avoid complex recompiles for small changes. These languages also need fairly complex dependency injection frameworks to be sure that classes can be altered via runtime configuration changes. In Python, the runtime flexibility means the advice changes somewhat.

Because Python uses duck typing, there isn't always a single, abstract superclass available to summarize a variety of alternative implementations. We may, for example, define a function parameter to be Iterable, telling mypy to permit any object that follows the Iterable protocol: this will include iterators as well as collections. 

In Python, the DIP leads us to two techniques:

  • Type hints should be as abstract as possible. In many cases, it will name a relevant protocol used by a method or a function.
  • Concrete type names should be parameterized.

In our preceding examples, the various DominoBoneYard class definitions all suffer from a dependency problem: they all refer to concrete class names when creating the initial pool of Domino objects and when creating the Hand objects. We are not free to replace these classes as needed, but need to create subclasses to replace a reference.

A more flexible definition of the class is shown in the following example:

class DominoBoneYard3c:

domino_class: Type[Domino] = Domino

hand_class: Type[Hand] = Hand3

hand_size: int = 7

def __init__(self, limit: int = 6) -> None:
self._dominoes = [
self.domino_class(x, y) for x in range(limit + 1) for y in range(x + 1)
]
random.shuffle(self._dominoes)

def hand_iter(self, players: int = 4) -> Iterator[Hand]:
for p in range(players):
hand = self.hand_class(
self._dominoes[:self.hand_size])
self._dominoes = self._dominoes[self.hand_size:]
yield hand

This example shows how the dependencies can be defined in a central place, as attributes of the class definition. This refactors the dependencies from deep within several methods to a much more visible position. We've provided a complete type hint in order to help spot potential misuse of the type expectations. For the Domino class, we don't have any alternatives, and the hint, Type[Domino], does seem redundant. For the Hand3 class, however, we've provided the hint of Type[Hand] to show the most abstract class that will be usable here.

Because these values are variables, it becomes very easy to perform dependency injection and supply configuration information at runtime. We can use code along the lines of DominoBoneYard3c.hand_class = Hand4 to change the class used to build a hand. Generally, this should be done before any instances are created. The class identification can be taken from a configuration file and used to tailor the details of the application's operation.

We can imagine a top-level program that includes the following:

configuration = get_configuration()
DominoBoneYard3c.hand_class = configuration['hand_class']
DominoBoneYard3c.domino_class = configuration['domino_class']

Once the class definitions have the proper dependencies injected into them, the application can then work with those configured class definitions. For more ideas on providing the configuration, see Chapter 14, Configuration Files and Persistence. It's important to note that the type hints are not used to check runtime configuration values. The type hints are only used to confirm that the source code appears to be consistent in its use of objects and types.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset