PPOOD
Pt. 2 Dependencies
Recognizing Dependencies
Powerful and Perilous Collaboration
Your design challenge is to manage dependencies so that each class has the fewest possible; a class should know just enough to do its job and not one thing more.
from dataclasses import dataclass
@dataclass
class Wheel:
rim: int
tire: int
def diameter(self):
return self.rim + (self.tire * 2)
class Gear:
def __init__(self, chainring, cog, rim, tire):
self._chainring = chainring
self._cog = cog
self._rim = rim
self._tire = tire
def ratio(self):
return self._chainring / self._cog
def gear_inches(self):
wheel = Wheel(self._rim, self._tire)
return self.ratio() * wheel.diameter()from dataclasses import dataclass
@dataclass
class Wheel:
rim: int
tire: int
def diameter(self):
return self.rim + (self.tire * 2)
class Gear:
def __init__(self, chainring, cog, rim, tire):
self._chainring = chainring
self._cog = cog
self._rim = rim
self._tire = tire
def ratio(self):
return self._chainring / self._cog
def gear_inches(self):
wheel = Wheel(self._rim, self._tire)
return self.ratio() * wheel.diameter()The name of another class.
The arguments that a message requires.
Argument order that a message requires.
Message name on object other than self.
Left unchecked, unmanaged dependencies cause an entire application to become an entangled mess. A day will come when it’s easier to rewrite everything than to change anything.

Writing Loosely Coupled Code
Injecting Dependencies
def gear_inches(self):
wheel = Wheel(self._rim, self._tire)
return self.ratio() * wheel.diameter()Knowing the name of something.
Coupling Consequences:
- Forced change when the `Wheel` class changes it's name.
- `Gear` is saying it only works with instances of `Wheel` and refuses to collaborate with any other type of object.
Gear becomes less useful it knows more about other objects; if it knew less it could do more.
class Gear:
def __init__(self, chainring, cog, wheel):
self._chainring = chainring
self._cog = cog
self._wheel = wheel
def ratio(self):
return self._chainring / self._cog
def gear_inches(self):
return self.ratio() * self._wheel.diameter()Isolating external messages
def gear_inches(self):
return self.ratio() * self._wheel.diameter()
# imagine there were many more uses of `self._wheel.diameter()`Knowing about messages to external objects.
Coupling Consequences:
- `Gear` still has knowledge that another object exists named `_wheel` which responds to `diameter()`.
- If the message is used many places, changes will be forced in every location.
class Gear:
def __init__(self, chainring, cog, wheel):
self._chainring = chainring
self._cog = cog
self._wheel = wheel
def _diameter(self):
return self._wheel.diameter()
def ratio(self):
return self._chainring / self._cog
def gear_inches(self):
return self.ratio() * self._diameter()
# Imagine other messages that now use `self._diameter()`.
# This helps isolate where the `_wheel` object is known.Removing argument
order dependence
gear = Gear(52, 11, Wheel(26, 1.5))Knowing argument ordering.
Even knowing how to send a message is a form of coupling.
class Gear:
def __init__(**kwargs):
self._chainring = kwargs["chainring"]
self._cog = kwargs["cog"]
self._wheel = kwargs["wheel"]
g1 = Gear(wheel=Wheel(26, 1.5), chainring=52, cog=11)Now argument order does not matter.
def divide(**kw):
return kw["a"] / kw["b"]
# OH COME ON!
divide(b=2, a=3)
divide(3, 2)Downsides:
- Adds verbosity
- Can make code harder to read/edit
It's up to us to decide when this adds value, and when it hurts.
Good spots:
- When working with a long parameter list
- When working with unstable, rapidly changing API.
- Building a framework for use by others.
def csv_reader(
csvfile,
sep=",",
line_terminator="\r\n",
quote_char='"',
skip_initial_space=False
):
pass
r = csv_reader(open("users.csv"))
r = csv_reader(open("users.csv"), sep='"', quote_char=",")Happy medium:
Message takes a few very stable and predictable arguments, and some number of less stable and order-independent defaulted arguments.
In case you run into a comma-quoted quote-separated data file.
Understanding the causes of change
Concretions and Abstractions
Abstraction - From Mirriam-Webster:
- disassociated from any specific instance
- difficult to understand
- expressing a quality apart from an object
from dataclasses import dataclass
@dataclass
class Wheel:
rim: int
tire: int
def diameter(self):
return self.rim + (self.tire * 2)
class Gear:
def __init__(self, chainring, cog, wheel):
self._chainring = chainring
self._cog = cog
self._wheel = wheel
def ratio(self):
return self._chainring / self._cog
def gear_inches(self):
return self.ratio() * self._wheel.diameter()
"""
When we injected ``Wheel`` into ``Gear`` and made it able
to depend on any object that could respond to ``diameter``,
we made an abstract interface. It is an abstract idea to
make ``Gear`` work with a "diameter-able" object interface
instead of a concrete instance of ``Wheel``.
"""Our programs should be philosophers. It doesn't matter if it's an `Apple` or an `Orange`; all that matters to the `rabid_hunger.py` program is that it adheres to the `IEatable` interface.
Dealing with big classes.
The pain of big classes:
- More objects to know about, more reason and opportunities to break.
- Has many reasons to change, but is hard to change due to complexity.
- All paths lead to the big class.
How to fix:
- It needs to be split up into multiple smaller classes with fewer dependencies and dependents, and clear single responsibility.
Dependent-Laden Classes
Comments
&
Questions
PPOOD - Dependencies
By Will Vaughn
PPOOD - Dependencies
- 420