Clean code 4: Dependency injection in practice: Handling commands
This article aims to give you some foundational tools towards mastering CQRS and Hexagonal architectural pattern.
Instead of going through a thorough overview of those patterns explaining the whys, I'll start by giving you pieces of the hows.
A whole views of those patterns will --in my opinion– make more much sense with a solid grasp on the basic bricks it's composed of.
Without further ado, let's get into it.
Useful concepts
DTO
A DTO (for Database Transfer Object) is a short lived object meant
to contain data and carry it (Initially to a Database, but it's common
to refer to objects that fits this definition as DTO eventhough no DB is
involved).
Note: DTO Should be immutables. For the sake of only focusing on our
topic, we'll keep that aspect for a future part.
Command and Query
A command is simply a DTO, that will express an intention and contain data
needed to perform the action intended. An action is whenever a mutation occurs
A query is pretty much the same thing, except its goal is to access data rather than inducing a mutation
Handler
A handler is the function associated with a command or a query (also events, that's a pretty generic term. Basically, a handler is a bunch of code meant to handle stuff)
Repository
A repository is a class that implements an interface and take care of interactions
with the persistence layer of your application.
Where to our project
Oh, I didn't tell you. From this article on, we're getting real ! We're boostraping a real world, actual project.
Where to start: Use cases
When you build a traditional CRUD application, it's common to start
by modeling your database schema.
Either with an ERM (Entity relationship models) modeling tools (as MySQL workbench)
or doing it as code by writing entities using your favorite ORM.
While nothing forbids that in the world of CQRS, Use Cases are
a much better starting point.
This is not a subject of itself. It's a whole bunch of subjects. DDD, BDD, Cucumber...
The goal of this part is to give you an understanding of some of CQRS/Hexagonal architecture base concepts. What this is definitely not: a thorough guide on all the good processes and practices to do that yourself in prod
(But we'll come to that, that's exactly our intended destination)
So rather than
Customer |
---|
column | type |
---|---|
id | int |
varchar(100) | |
address | ... |
(And so on...)
What your first step will look like should more be along the lines of:
As a customer I want to add articles to my basket
(Which could then be declined into several scenarios, on which base you
may write or generate tests. But hey, we're not aiming for prod ready
processes here. Only the basics of CQRS. So (B|D)DD will have to wait
a later part)
Here come the C and and the Q of CQRS.
Uses cases will reflect as interactions with our system
Those interactions will fall in one of two categories:
- Getting stuff (Query)
- Doing stuff (Command)
Let's get real
Use cases
That's it guys we're actually coding our application !
Here are the use cases we'll want to implement in our first iteration:
-
As a non-user I want to be able to register an account
-
As an user I want to create TodoLists
-
As an user with a TodoList, I want to add it tasks
-
TodoLists should have:
- A title
- A description
-
Tasks should have:
- A title
- A description
- A date of creation
- A date at which the task should be done
- A data at which MUST be done (which we'll call deadline)
We'll stick to that for now :)
(Actual scenarios, Cucumber. Those are two items that would
make our process real world ready. We'll tackle that in the future.
Just keep in mind world readiness is not what we're aiming yet)
Architecture
Requirements and project structure
Create a file named requirements.txt
with this content
rich
pytest
toolz
flask
flask-login
SQLAlchemy
Flask-SQLAlchemy
psycopg2
werkzeug
Then create the project structure
python3.8 -m pip install -r requirements.txt
mkdir -p faire/{aggregate,command,query,repository,cqrs,middleware,tests,utils}
touch faire/{aggregate,command,query,repository,cqrs,middleware,tests,utils}/__init__.py
touch faire/{aggregate,command,query,repository}/user.py
touch faire/aggregate/aggregate.py
touch faire/command/command.py
Quick side note: Aggregates are, for now, simple entities. We'll see
in future parts how they're not. What conceptually tell them appart from
regular entities. For this part, just assume we could have called them
Entities
Dependency injector
Dependency Injection is something you need to use and understand thoroughly.
Especially in large applications, it's not always totally simple to use.
here are two hurdles that come with it:
Verbosity
If you need to instantiate an object, that depends on an other, that depends on
an other...
my_foo = Foo(BarImplementation(BazImplementation(BlaImplementation())))
It gets confusing
Single instance
It may happen that you need to use an object on two distinct parts of
your code. And you want to use the SAME object.
It's not that hard to achieve, but many ways to do so will be painful
and prone to bug.
There is a pattern for that, it's called Dependency Injector or
Dependency Container
Create the file faire/utils/dependency_injector.py
with this code:
from typing import Any
class DependencyInjector:
"""
Dead Simple dependency injector
Assumption: Only one type and instance per interface
"""
def register_instance(self, instance: Any, *aliases) -> None:
pass
def get(self, cls):
pass
Here's what we want it to do:
Given FooImpl implementing FooInterface,
Given BlaRegistery has FooInterface as a dependecy (and signal that fact by type hint)
assuming foo is an instance of FooImpl an DI of DependencyInjector
- If DI.register_instance(foo_impl, 'foo_bar', 'bar_foo')
- DI.get(FooInterface) is DI.get(FooImpl) is DI.get('bar_foo') is DI.get('foo_bar')
In the same scope
- If DI.get(BlaRegistry) on its first call will return a new instance of
BlaRegistry with the correct dependency injected
We have scenarios !
We can write tests
create faire/tests/dependency_injector_test.py
with this
from faire.utils.dependency_injector import DependencyInjector
def test_DependencyInjector():
class FooInterface:
__interface__ = True
class FooImpl(FooInterface):
pass
class BlaRegistry:
def __init__(self, foo: FooInterface):
self.foo = foo
DI = DependencyInjector()
DI.register_instance(FooImpl(), "foo_bar", "bar_foo")
assert (
DI.get(FooInterface)
is DI.get(FooImpl)
is DI.get("foo_bar")
is DI.get("bar_foo")
is DI.get(BlaRegistry).foo
is not FooImpl()
)
A last thing
DependencyInjector implements the
Singleton
pattern.
DepencyInjector.instance returns always the same instance, which is created if needed
Which translate in test as: (append this to the same file)
def test_DependencyInjectorSingleton():
assert DependencyInjector.instance is DependencyInjector.instance
run in terminal
python3.8 -m pytest faire/tests/dependency_injector_test.py
Which should fail. If it doesn't fail, that's in itself a fail
(Wait, would it means it's therefore a success ?)
We'll first take care of the Singleton
mechanism
create the file faire/utils/singleton.py
with this
from typing import TypeVar
T = TypeVar("T")
class Singleton(type):
"""
Dead simple singleton helper
:use:
```python
class Bla(metaclass=Singleton):
def __init__(self): # <= Assumes __init__ awaits no parameter
...
Bla.instance <= unique instance of Bla
"""
@property
def instance(cls: T) -> T:
# TODO type hint with generic
cls._instance = getattr(cls, "_instance", cls())
return cls._instance
then, replace the content of faire/utils/dependency_injector.py
with
from itertools import cycle
from typing import Any
from toolz import valmap
from faire.utils.singleton import Singleton
class DependencyInjector(metaclass=Singleton):
"""
Dead Simple dependency injector
Assumption: Only one type and instance per interface
"""
def __init__(self):
self._instances = {}
def register_instance(self, instance: Any, *aliases) -> None:
"""
:Note to myself: Perhaps too much magic. TODO: Test limit cases
"""
for base in instance.__class__.__bases__:
if getattr(base, "__interface__", False):
aliases = [*aliases, base]
aliases = [*aliases, instance.__class__]
self._instances.update(dict(zip(aliases, cycle([instance]))))
def _create_instance(self, cls):
# TODO Type hint with Generics
self.register_instance(
cls(
**valmap(
lambda type_: self._instances[type_],
cls.__init__.__annotations__,
)
)
)
def get(self, cls):
if not cls in self._instances:
self._create_instance(cls)
return self._instances[cls]
Again run
python3.8 -m pytest faire/tests/dependency_injector_test.py
Which should work
Important note
What I'm aiming at, writing this series of articles, is talk to folks in the shoes I was when I first started code.
Though, there is an hidden purpose behind this dependency injector. Which
is to talk to the version of myself of not so long ago and yell as this idiot "Dude, that's too much magic".
See, magic in code may be great, but must be handled carefully.
For now this is a mere not real world ready note of caution.
At this point, and for all intents and purposes of that article,
DependencyInjector
behaviour is just fine.
A future part will address how too much magic auto-wiring may happen
to be disastrous.
EndPoint, Flask, SQLAlchemy
Our application will present an HTTP API, and persistance will be done thanks
to SQLAlchemy + PostgreSQL.