class Car:
def honk(self):
print('Honk!')
Cool, now we have a car, but it only honks. But we only needed it to honk, so that's fine. class RoadTrip:
def __init__(self, car, destination):
self.car = car
def go(self):
self.car.start()
self.car.drive_to(destination)
Oh no, we built a car but it does nothing that a car does. Let's add this functionality. class Car:
def __init__(self):
self.started = False
self.gps = []
self.location = cool_app.get_current_location()
def honk(self):
print('Honk!')
def start(self):
self.started = True
@property
def location(self):
return self.gps[-1]
@location.setter
def location(self, new_location):
self.gps.append(location)
def drive_to(self, location):
self.location = location
Super easy. Except a different user of our library implemented this functionality already for a different reason, but in a different way. class CarEngine:
def __init__(self, car):
self.car = car
self.started = False
def start(self):
if self.started:
raise Exception('Already started!')
self.started = True
def turn_off(self):
if not self.started:
raise Exception('Not started!')
self.started = False
class CarGasTank(self, car):
def __init__(self, gallons):
self.car = car
self.capacity = gallons
self.level = gallons
@property
def empty(self):
return self.level == 0
class MotorCar:
def __init__(self, engine, gas_tank, mpg, location):
self.engine = engine
self.gas_tank = gas_tank
self.location = location
self.mpg = mpg
@property
def started(self):
return self.engine.started
def start(self):
return self.engine.start()
def drive_to(self, location):
distance = location - self.location
fuel_required = distance / self.mpg
if fuel_required > self.gas_tank.gallons:
raise Exception('Not enough gas!')
self.gas_tank.gallons -= fuel_required
self.location = location
I've lost count of the number of times I've seen stuff like this. Sometimes the original implementation is in a different library that's hard to change. Sometimes other code rely on specific details of the original implementation so changing it requires changing that code too. Sometimes additions seem "out of scope" so they are actively pushed to dependencies.It might feel like this is a process problem -- like there should have been better upfront design or communication -- but these "failure cases" are actually the success cases for code. You want code to rely on your libraries. You want to consider all users of your libraries when making changes.
The problem is actually YAGNI, because that mentality encourages us to churn out big, incoherent bags of functions when we should be thinking about code responsibilities and designing whole systems.
You can see this in action with JavaScript most infamously. Its standard library is extremely YAGNI, and it led to whole new programming languages being built on top of it because it was so anemic. The complexity has to live somewhere, and if you don't deal with it in a coherent and orthogonal way, someone else will have to deal with it and their options will be a lot more limited than yours.
Following simple mantras like DRY, YAGNI, and whatever else fits on a poster is the surest way to mess up your design. It requires thoughtfulness and experience, and there are no shortcuts.