@@ -20,9 +20,10 @@ about Application-Controlled Identifiers, you'll find those at the end of post,
2020after a bunch of stuff about ORMs, CQRS, and some casual trolling of junior
2121programmers.
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 ) ,
2728the principle states:
2829
@@ -35,6 +36,7 @@ Referential transparency is an important concept from functional programming.
3536Briefly, a function is referentially transparent if you could replace it with a
3637static value.
3738
39+ ``` python
3840class 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
4951In this class, the is_on method is referentially transparent - I can replace it
5052with 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?
7173The answer is "it depends". The lowest-cost option is just to re-use your
7274repositories in your UI entrypoints.
7375
76+ ``` python
7477@app.route (" /issues" )
7578def 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
8184This is totally fine unless you have complex formatting, or multiple entrypoints
8285to your system. The problem with using your repositories directly in this way is
8386that it's a slippery slope. Sooner or later you're going to have a tight
8487deadline, and a simple requirement, and the temptation is to skip all the
8588command/handler nonsense and do it directly in the web api.
8689
90+ ``` python
8791@app.route (' /issues/<issue_id>' , methods = [' DELETE' ])
8892def 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
9599Super convenient, but then you need to add some error handling and some logging
96100and an email notification.
97101
102+ ``` python
98103@app.route (' /issues/<issue_id>' , methods = [' DELETE' ])
99104def 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
122128Aaaaand, 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
128134reads are complex, or because you're trying to stay pure, then instead we could
129135define our views explicitly.
130136
131-
137+ ``` python
132138class OpenIssuesList :
133139
134140 def __init__ (self , sessionmaker ):
@@ -146,45 +152,47 @@ class OpenIssuesList:
146152def list_issues():
147153 view_builder = OpenIssuesList(session_maker)
148154 return jsonify(view_builder.fetch())
149-
155+ ```
150156
151157This is my favourite part of teaching ports and adapters to junior programmers,
152158because 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?
183190In my experience, there are two ways that teams go wrong when using ORMs. The
184191most common mistake is not paying enough attention to the boundaries of their
185192use cases. This leads to the application making far too many calls to the
186193database 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
199207ORMs 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
233241are changing state. When viewing state, we can almost always accept a weaker
234242consistency model.
235243
236- CQRS is CQS at a system-level
244+ # ## CQRS is CQS at a system-level
245+
237246CQRS stands for Command- Query Responsibility Segregation, and it' s an
238247architectural pattern that was popularised by Greg Young. A lot of people
239248misunderstand 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.
263272For the write- side of the system, use an ORM , for the read side, use whatever is
264273a) fast, and b) convenient.
265274
266- Application Controlled Identifiers
275+ # ## Application Controlled Identifiers
276+
267277At 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
276286The way I would recommend you handle this is simple - instead of letting your
277287database choose ids for you, just choose them yourself.
278288
289+ ```python
279290@ api.route(' /issues' , methods = [' POST' ])
280291def 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
291302There' 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
296308all issues, and one to view a single issue. I' m using UUIDs as my identifiers,
297309but 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
302314Okay, quick spot- check - how are we shaping up against our original Ports and
0 commit comments