Notes and exercises for learning design patterns
Exercise 1 adapted one object into one compatible object.
Exercise 2 adapts one object into many compatible objects.
LegacyBatch
-> TemperatureReading
-> TemperatureReading
-> TemperatureReading
The client code only needs an iterable:
for reading in readings:
...
So the adapter implements __iter__.
class LegacyBatchToReadingsAdapter:
def __init__(self, batch: LegacyBatch):
self._readings = tuple(
TemperatureReading(
sensor_id=batch.device_id(),
timestamp=timestamp,
celsius=round((fahrenheit - 32) * 5 / 9, 2),
)
for timestamp, fahrenheit in batch.raw_rows()
if fahrenheit is not None
)
def __iter__(self):
return iter(self._readings)
def __len__(self):
return len(self._readings)
The solution stores normalized readings as a tuple:
self._readings: tuple[TemperatureReading, ...]
That gives the adapter a stable internal result. Callers can iterate over it many times without re-reading the legacy batch.
It also prevents accidental mutation of the adapter’s internal collection.
A common mistake is to make __iter__ return a list directly:
def __iter__(self):
return self._readings
That is wrong because __iter__ should return an iterator.
Use:
return iter(self._readings)
The adapter now owns three kinds of legacy-specific knowledge:
That keeps the rest of the application clean.