Note
|
placeholder chapter, under construction |
Congratulations on reading an appendix! Not everyone does. And yet we hide so much good stuff in here…
OK at the end of [chapter_12_dependency_injection] we’d left a slightly ugly thing — there’s a circular dependency between flask_app.py and redis_pubsub.py. Also we had some duplication of boilerplate setup/init code in those two entrypoints, which felt a bit rough.
Explicitly defining a single "entrypoint" or bootstrap script or "configuration root" in OO parlance, is a pattern that can help us to keep things tidy. Let’s take a look.
Where do we declare our defaults? config.py is one place, but we also do some in, eg, unit_of_work.py, which declares the "default" database session manager. maybe that’s not too bad…
DEFAULT_SESSION_FACTORY = sessionmaker(bind=create_engine(
config.get_postgres_uri(),
isolation_level="SERIALIZABLE",
))
There is some other config spread around though, like what our "normal" dependencies are, and we haven’t even spoken about cross-cutting concerns like logging.
Defaults are maybe feeling a bit messy, but so are some other aspects of the
initial setup or "bootsrapping" of our application; orm.start_mappers()
for
example, or uow.set_bus()
. We call them in various places in our tests, and at
least twice in our "real" application…
app = Flask(__name__)
orm.start_mappers()
uow = unit_of_work.SqlAlchemyUnitOfWork()
bus = messagebus.MessageBus(
uow=uow,
...
)
uow.set_bus(bus)
@app.route("/add_batch", methods=['POST'])
def add_batch():
Let’s bring all this stuff together into a single "bootstrap script" and see if we end up in a better position.
Here’s what a bootstrap script could look like:
def bootstrap(
start_orm=orm.start_mappers,
session_factory=DEFAULT_SESSION_FACTORY,
notifications=None,
publish=redis_pubsub.publish,
):
start_orm()
uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory=session_factory)
if notifications is None:
notifications = EmailNotifications(smtp_host=EMAIL_HOST, port=EMAIL_PORT)
bus = messagebus.MessageBus(uow=uow, notifications=notifications, publish=publish)
uow.set_bus(bus)
return bus
-
it declares default dependencies but allows you to override them
-
it does the "init" stuff that we need to get our app going in one place
-
it gives us back the core of our app, the messagebus
In our application’s entrypoints, we just call bootstrap.bootstrap()
to get a messagebus, rather than configuring a UoW and the rest of it.
app = Flask(__name__)
bus = bootstrap.bootstrap()
@app.route("/add_batch", methods=['POST'])
def add_batch():
...
bus.handle(cmd)
return 'OK', 201
And in tests, we can use our bootstrap.bootstrap()
with overridden defaults
to get a custom messagebus:
@pytest.fixture
def sqlite_bus(sqlite_session_factory):
bus = bootstrap.bootstrap(
start_orm=lambda: None,
session_factory=sqlite_session_factory,
notifications=mock.Mock(),
publish=mock.Mock(),
)
yield bus
clear_mappers()
def test_allocations_view(sqlite_bus):
sqlite_bus.handle(commands.CreateBatch('b1', 'sku1', 50, None))
sqlite_bus.handle(commands.CreateBatch('b2', 'sku2', 50, date.today()))
sqlite_bus.handle(commands.Allocate('o1', 'sku1', 20))
sqlite_bus.handle(commands.Allocate('o1', 'sku2', 20))
assert views.allocations('o1', sqlite_bus.uow) == [
{'sku': 'sku1', 'batchref': 'b1'},
{'sku': 'sku2', 'batchref': 'b2'},
]
TODO: bootstrapper as class instead?
In chapter 9 (Dependency graph for chapter 9 (it’s a mess)), it’s a real mess:
By chapter 10 (Dependency graph for chapter 10 (it’s better)), when we introduce DI, things are much better:
Does the bootstrap script help? As Dependency graph with bootstrap script shows, the answer is: "kinda."
Well kinda-not actually. That Redis circular dependency is still there and looking ugly.
One fix is to split the "pub" from the "sub", as in Dependency graph with bootstrap script and no circular deps:
Now we have what our esteemed tech reviewer David Seddon would call a "rocky road architecture": all the dependencies flow in one direction.
TODO: alternative fix by making an abstract redis thingie?