Skip to content

Commit 2c7819a

Browse files
committed
more syntax fixing in one of the old blog posts
1 parent 021b1dd commit 2c7819a

1 file changed

Lines changed: 57 additions & 45 deletions

File tree

_posts/2017-09-13-commands-and-queries-handlers-and-views.md

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ about Application-Controlled Identifiers, you'll find those at the end of post,
2020
after a bunch of stuff about ORMs, CQRS, and some casual trolling of junior
2121
programmers.
2222

23-
What is CQS ?
24-
The [Command Query Separation](https://martinfowler.com/bliki/CommandQuerySeparation.html) principle was
25-
first described by Bertrand Meyer in the late Eighties. Per
23+
### What is CQS ?
24+
25+
The [Command Query Separation](https://martinfowler.com/bliki/CommandQuerySeparation.html)
26+
principle was first described by Bertrand Meyer in the late Eighties. Per
2627
[wikipedia](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation),
2728
the principle states:
2829

@@ -35,6 +36,7 @@ Referential transparency is an important concept from functional programming.
3536
Briefly, a function is referentially transparent if you could replace it with a
3637
static value.
3738

39+
```python
3840
class LightSwitch:
3941

4042
def toggle_light(self):
@@ -44,7 +46,7 @@ class LightSwitch:
4446
@property
4547
def is_on(self):
4648
return self.light_is_on
47-
49+
```
4850

4951
In this class, the is_on method is referentially transparent - I can replace it
5052
with the value True or False without any loss of functionality, but the method
@@ -71,30 +73,33 @@ data back out of our model? What is the equivalent port for queries?
7173
The answer is "it depends". The lowest-cost option is just to re-use your
7274
repositories in your UI entrypoints.
7375

76+
```python
7477
@app.route("/issues")
7578
def list_issues():
7679
with unit_of_work_manager.start() as unit_of_work:
7780
open_issues = unit_of_work.issues.find_by_status('open')
7881
return json.dumps(open_issues)
79-
82+
```
8083

8184
This is totally fine unless you have complex formatting, or multiple entrypoints
8285
to your system. The problem with using your repositories directly in this way is
8386
that it's a slippery slope. Sooner or later you're going to have a tight
8487
deadline, and a simple requirement, and the temptation is to skip all the
8588
command/handler nonsense and do it directly in the web api.
8689

90+
```python
8791
@app.route('/issues/<issue_id>', methods=['DELETE'])
8892
def delete_issue(issue_id):
8993
with unit_of_work_manager.start() as uow:
9094
issue = uow.issues[issue_id]
9195
issue.delete()
9296
uow.commit()
93-
97+
```
9498

9599
Super convenient, but then you need to add some error handling and some logging
96100
and an email notification.
97101

102+
```python
98103
@app.route('/issues/<issue_id>', methods=['DELETE'])
99104
def delete_issue(issue_id):
100105
logging.info("Handling DELETE of issue "+str(issue_id))
@@ -117,6 +122,7 @@ def delete_issue(issue_id):
117122
else:
118123
logging.info("Issue already deleted. NOOP")
119124
return "Deleted!", 202
125+
```
120126

121127

122128
Aaaaand, we're back to where we started: business logic mixed with glue code,
@@ -128,7 +134,7 @@ it's all good, you have my blessing. If you want to avoid this, because your
128134
reads are complex, or because you're trying to stay pure, then instead we could
129135
define our views explicitly.
130136

131-
137+
```python
132138
class OpenIssuesList:
133139

134140
def __init__(self, sessionmaker):
@@ -146,45 +152,47 @@ class OpenIssuesList:
146152
def list_issues():
147153
view_builder = OpenIssuesList(session_maker)
148154
return jsonify(view_builder.fetch())
149-
155+
```
150156

151157
This is my favourite part of teaching ports and adapters to junior programmers,
152158
because the conversation inevitably goes like this:
153159

154-
smooth-faced youngling: Wow, um... are you - are we just going to hardcode that
155-
sql in there? Just ... run it on the database?
160+
> smooth-faced youngling: Wow, um... are you - are we just going to hardcode that
161+
> sql in there? Just ... run it on the database?
156162

157-
grizzled old architect: Yeah, I think so. Do The Simplest Thing That Could
158-
Possibly Work, right? YOLO, and so forth.
163+
> grizzled old architect: Yeah, I think so. Do The Simplest Thing That Could
164+
> Possibly Work, right? YOLO, and so forth.
159165

160-
sfy: Oh, okay. Um... but what about the unit of work and the domain model and
161-
the service layer and the hexagonal stuff? Didn't you say that "Data access
162-
ought to be performed against the aggregate root for the use case, so that we
163-
maintain tight control of transactional boundaries"?
166+
> sfy: Oh, okay. Um... but what about the unit of work and the domain model and
167+
> the service layer and the hexagonal stuff? Didn't you say that "Data access
168+
> ought to be performed against the aggregate root for the use case, so that we
169+
> maintain tight control of transactional boundaries"?
164170

165-
goa: Ehhhh... I don't feel like doing that right now, I think I'm getting
166-
hungry.
171+
> goa: Ehhhh... I don't feel like doing that right now, I think I'm getting
172+
> hungry.
167173

168-
sfy: Right, right ... but what if your database schema changes?
174+
> sfy: Right, right ... but what if your database schema changes?
169175

170-
goa: I guess I'll just come back and change that one line of SQL. My acceptance
171-
tests will fail if I forget, so I can't get the code through CI.
176+
> goa: I guess I'll just come back and change that one line of SQL. My acceptance
177+
> tests will fail if I forget, so I can't get the code through CI.
172178

173-
sfy: But why don't we use the Issue model we wrote? It seems weird to just
174-
ignore it and return this dict... and you said "Avoid taking a dependency
175-
directly on frameworks. Work against an abstraction so that if your dependency
176-
changes, that doesn't force change to ripple through your domain". You know we
177-
can't unit test this, right?
179+
> sfy: But why don't we use the Issue model we wrote? It seems weird to just
180+
> ignore it and return this dict... and you said "Avoid taking a dependency
181+
> directly on frameworks. Work against an abstraction so that if your dependency
182+
> changes, that doesn't force change to ripple through your domain". You know we
183+
> can't unit test this, right?
178184

179-
goa: Ha! What are you, some kind of architecture astronaut? Domain models! Who
180-
needs 'em.
185+
> goa: Ha! What are you, some kind of architecture astronaut? Domain models! Who
186+
> needs 'em.
187+
188+
### Why have a separate read-model?
181189

182-
Why have a separate read-model?
183190
In my experience, there are two ways that teams go wrong when using ORMs. The
184191
most common mistake is not paying enough attention to the boundaries of their
185192
use cases. This leads to the application making far too many calls to the
186193
database because people write code like this:
187194

195+
```python
188196
# Find all users who are assigned this task
189197
# [[and]] notify them and their line manager
190198
# then move the task to their in-queue
@@ -193,7 +201,7 @@ for assignee in task.assignees:
193201
assignee.manager.notifications.add(notification)
194202
assignee.notifications.add(notification)
195203
assignee.queues.inbox.add(task)
196-
204+
```
197205

198206

199207
ORMs make it very easy to "dot" through the object model this way, and pretend
@@ -233,7 +241,8 @@ noting that transactional consistency is usually only a real requirement when we
233241
are changing state. When viewing state, we can almost always accept a weaker
234242
consistency model.
235243

236-
CQRS is CQS at a system-level
244+
### CQRS is CQS at a system-level
245+
237246
CQRS stands for Command-Query Responsibility Segregation, and it's an
238247
architectural pattern that was popularised by Greg Young. A lot of people
239248
misunderstand CQRS, and think you need to use separate databases and crazy
@@ -263,19 +272,21 @@ my queries are fundamentally different than the requirements for my commands.
263272
For the write-side of the system, use an ORM, for the read side, use whatever is
264273
a) fast, and b) convenient.
265274

266-
Application Controlled Identifiers
275+
### Application Controlled Identifiers
276+
267277
At this point, a non-junior programmer will say
268278

269-
Okay, Mr Smarty-pants Architect, if our commands can't return any values, and
270-
our domain models don't know anything about the database, then how do I get an
271-
ID back from my save method?
272-
Let's say I create an API for creating new issues, and when I have POSTed the
273-
new issue, I want to redirect the user to an endpoint where they can GET their
274-
new Issue. How can I get the id back?
279+
> Okay, Mr Smarty-pants Architect, if our commands can't return any values, and
280+
> our domain models don't know anything about the database, then how do I get an
281+
> ID back from my save method?
282+
> Let's say I create an API for creating new issues, and when I have POSTed the
283+
> new issue, I want to redirect the user to an endpoint where they can GET their
284+
> new Issue. How can I get the id back?
275285

276286
The way I would recommend you handle this is simple - instead of letting your
277287
database choose ids for you, just choose them yourself.
278288

289+
```python
279290
@api.route('/issues', methods=['POST'])
280291
def report_issue(self):
281292
# uuids make great domain-controlled identifiers, because
@@ -286,17 +297,18 @@ def report_issue(self):
286297
cmd = ReportIssueCommand(issue_id, **request.get_json())
287298
handler.handle(cmd)
288299
return "", 201, { 'Location': '/issues/' + str(issue_id) }
289-
300+
```
290301

291302
There's a few ways to do this, the most common is just to use a UUID, but you
292-
can also implement something like hi-lo
293-
[https://pypi.python.org/pypi/sqlalchemy-hilo/0.1.2]. In the new code sample
294-
[https://github.com/bobthemighty/blog-code-samples/tree/master/ports-and-adapters/03]
295-
, I've implemented three flask endpoints, one to create a new issue, one to list
303+
can also implement something like
304+
[hi-lo](https://pypi.python.org/pypi/sqlalchemy-hilo/0.1.2).
305+
In the new
306+
[code sample](https://github.com/bobthemighty/blog-code-samples/tree/master/ports-and-adapters/03),
307+
I've implemented three flask endpoints, one to create a new issue, one to list
296308
all issues, and one to view a single issue. I'm using UUIDs as my identifiers,
297309
but I'm still using an integer primary key on the issues table, because using a
298-
GUID in a clustered index leads to table fragmentation and sadness
299-
[http://sqlmag.com/database-performance-tuning/clustered-indexes-based-upon-guids]
310+
GUID in a clustered index leads to table fragmentation and
311+
[sadness](http://sqlmag.com/database-performance-tuning/clustered-indexes-based-upon-guids)
300312
.
301313

302314
Okay, quick spot-check - how are we shaping up against our original Ports and

0 commit comments

Comments
 (0)