The term DDD comes from the book by Eric Evans: “Domain-Driven Design: Tackling -Complexity in the Heart of Software”. -In his book he describes a set of practices that aim to help us build -maintainable, rich, software systems that solve customer’s problems. The book is -560 pages of dense insight, so you’ll pardon me if my summary elides some -details, but in brief he suggests:
--
-
- Listen very carefully to your domain experts - the people whose job you’re - automating or assisting in software. -
- Learn the jargon that they use, and help them to come up with new jargon, so - that every concept in their mental model is named by a single precise term. -
- Use those terms to model your software; the nouns and verbs of the domain - expert are the classes and methods you should use in modelling. -
- Whenever there is a discrepancy between your shared understanding of the - domain, go and talk to the domain experts again, and then refactor - aggressively. -
This sounds great in theory, but in practice we often find that our business -logic escapes from our model objects; we end up with logic bleeding into -controllers, or into fat “manager” classes. We find that refactoring becomes -difficult: we can’t split a large and important class, because that would -seriously impact the database schema; or we can’t rewrite the internals of an -algorithm because it has become tightly coupled to code that exists for a -different use-case. The good news is that these problems can be avoided, since -they are caused by a lack of organisation in the codebase. In fact, the tools to -solve these problems take up half of the DDD book, but it can be be difficult to -understand how to use them together in the context of a complete system.
-I want to use this series to introduce an architectural style called -Ports and Adapters, -and a design pattern named -Command Handler. -I’ll be explaining the patterns in Python because that’s the language that I use -day-to-day, but the concepts are applicable to any OO language, and can be -massaged to work perfectly in a functional context. There might be a lot more -layering and abstraction than you’re used to, especially if you’re coming from a -Django background or similar, but please bear with me. In exchange for a more -complex system at the outset, we can avoid much of our accidental complexity later.
-The system we’re going to build is an issue management system, for use by a -helpdesk. We’re going to be replacing an existing system, which consists of an -HTML form that sends an email. The emails go into a mailbox, and helpdesk staff -go through the mails triaging problems and picking up problems that they can -solve. Sometimes issues get overlooked for a long time, and the helpdesk team -have invented a complex system of post-it notes and whiteboard layouts to track -work in progress. For a while this system has worked pretty well but, as the -system gets busier, the cracks are beginning to show.
-Our first conversation with the domain expert -“What’s the first step in the process?” you ask, “How do tickets end up in the -mail box?”.
-“Well, the first thing that happens is the user goes to the web page, and they -fill out some details, and report an issue. That sends an email into the issue -log and then we pick issues from the log each morning”.
-“So when a user reports an issue, what’s the minimal set of data that you need -from them?”
-“We need to know who they are, so their name, and email I guess. Uh… and the -problem description. They’re supposed to add a category, but they never do, and -we used to have a priority, but everyone set their issue to EXTREMELY URGENT, so -it was useless.
-“But a category and priority would help you to triage things?”
-“Yes, that would be really helpful if we could get users to set them properly.”
-This gives us our first use case: As a user, I want to be able to report a new -issue.
-Okay, before we get to the code, let’s talk about architecture. The architecture -of a software system is the overall structure - the choice of language, -technology, and design patterns that organise the code and satisfy our -constraints [https://en.wikipedia.org/wiki/Non-functional_requirement]. For our -architecture, we’re going to try and stick with three principles:
--
-
- We will always define where our use-cases begin and end. We won’t have - business processes that are strewn all over the codebase. -
- We will depend on abstractions - [https://en.wikipedia.org/wiki/Dependency_inversion_principle], and not on - concrete implementations. -
- We will treat glue code as distinct from business logic, and put it in an - appropriate place. -
Firstly we start with the domain model. The domain model encapsulates our shared -understanding of the problem, and uses the terms we agreed with the domain -experts. In keeping with principle #2 we will define abstractions for any -infrastructural or technical concerns and use those in our model. For example, -if we need to send an email, or save an entity to a database, we will do so -through an abstraction that captures our intent. In this series we’ll create a -separate python package for our domain model so that we can be sure it has no -dependencies on the other layers of the system. Maintaining this rule strictly -will make it easier to test and refactor our system, since our domain models -aren’t tangled up with messy details of databases and http calls.
-Around the outside of our domain model we place services. These are stateless -objects that do stuff to the domain. In particular, for this system, our command -handlers are part of the service layer.
-Finally, we have our adapter layer. This layer contains code that drives the -service layer, or provides services to the domain model. For example, our domain -model may have an abstraction for talking to the database, but the adapter layer -provides a concrete implementation. Other adapters might include a Flask API, or -our set of unit tests, or a celery event queue. All of these adapters connect -our application to the outside world.
-In keeping with our first principle, we’re going to define a boundary for this -use case and create our first Command Handler. A command handler is an object -that orchestrates a business process. It does the boring work of fetching the -right objects, and invoking the right methods on them. It’s similar to the -concept of a Controller in an MVC architecture.
-First, we create a Command object.
-class ReportIssueCommand(NamedTuple):
- reporter_name: str
- reporter_email: str
- problem_description: str
-A command object is a small object that represents a state-changing action that -can happen in the system. Commands have no behaviour, they’re pure data -structures. There’s no reason why you have to represent them with classes, since -all they need is a name and a bag of data, but a NamedTuple is a nice compromise -between simplicity and convenience. Commands are instructions from an external -agent (a user, a cron job, another service etc.) and have names in the -imperative tense, for example:
--
-
- ReportIssue -
- PrepareUploadUri -
- CancelOutstandingOrders -
- RemoveItemFromCart -
- OpenLoginSession -
- PlaceCustomerOrder -
- BeginPaymentProcess -
We should try to avoid the verbs Create, Update, or Delete (and their synonyms) -because those are technical implementations. When we listen to our domain -experts, we often find that there is a better word for the operation we’re -trying to model. If all of your commands are named “CreateIssue”, “UpdateCart”, -“DeleteOrders”, then you’re probably not paying enough attention to the language -that your stakeholders are using.
-The command objects belong to the domain, and they express the API of your -domain. If every state-changing action is performed via a command handler, then -the list of Commands is the complete list of supported operations in your domain -model. This has two major benefits:
--
-
- If the only way to change state in the system is through a command, then the - list of commands tells me all the things I need to test. There are no other - code paths that can modify data. -
- Because our commands are lightweight, logic-free objects, we can create them - from an HTTP post, or a celery task, or a command line csv reader, or a unit - test. They form a simple and stable API for our system that does not depend - on any implementation details and can be invoked in multiple ways. -
In order to process our new command, we’ll need to create a command handler.
-class ReportIssueCommandHandler:
- def __init__(self, issue_log):
- self.issue_log = issue_log
-
- def __call__(self, cmd):
- reported_by = IssueReporter(
- cmd.reporter_name,
- cmd.reporter_email)
- issue = Issue(reported_by, cmd.problem_description)
- self.issue_log.add(issue)
-Command handlers are stateless objects that orchestrate the behaviour of a -system. They are a kind of glue code, and manage the boring work of fetching and -saving objects, and then notifying other parts of the system. In keeping with -principle #3, we keep this in a separate layer. To satisfy principle #1, each -use case is a separate command handler and has a clearly defined beginning and -end. Every command is handled by exactly one command handler.
-In general all command handlers will have the same structure:
--
-
- Fetch the current state from our persistent storage. -
- Update the current state. -
- Persist the new state. -
- Notify any external systems that our state has changed. -
We will usually avoid if statements, loops, and other such wizardry in our -handlers, and stick to a single possible line of execution. Command handlers are - boring glue code. -Since our command handlers are just glue code, we won’t put any business logic -into them - they shouldn’t be making any business decisions. For example, let’s -skip ahead a little to a new command handler:
-class MarkIssueAsResolvedHandler:
- def __init__(self, issue_log):
- self.issue_log = issue_log
-
- def __call__(self, cmd):
- issue = self.issue_log.get(cmd.issue_id)
- # the following line encodes a business rule
- if (issue.state != IssueStatus.Resolved):
- issue.mark_as_resolved(cmd.resolution)
-This handler violates our glue-code principle because it encodes a business -rule: “If an issue is already resolved, then it can’t be resolved a second -time”. This rule belongs in our domain model, probably in the mark_as_resolved -method of our Issue object. -I tend to use classes for my command handlers, and to invoke them with the call -magic method, but a function is perfectly valid as a handler, too. The major -reason to prefer a class is that it can make dependency management a little -easier, but the two approaches are completely equivalent. For example, we could -rewrite our ReportIssueHandler like this:
-def ReportIssue(issue_log, cmd):
- reported_by = IssueReporter(
- cmd.reporter_name,
- cmd.reporter_email)
- issue = Issue(reported_by, cmd.problem_description)
- issue_log.add(issue)
-If magic methods make you feel queasy, you can define a handler to be a class -that exposes a handle method like this:
-class ReportIssueHandler:
- def handle(self, cmd):
- ...
-However you structure them, the important ideas of commands and handlers are:
--
-
- Commands are logic-free data structures with a name and a bunch of values. -
- They form a stable, simple API that describes what our system can do, and - doesn’t depend on any implementation details. -
- Each command can be handled by exactly one handler. -
- Each command instructs the system to run through one use case. -
- A handler will usually do the following steps: get state, change state, - persist state, notify other parties that state was changed. -
Let’s take a look at the complete system, I’m concatenating all the files into a -single code listing for each of grokking, but in the git repository -[https://github.com/bobthemighty/blog-code-samples/tree/master/ports-and-adapters/01] - I’m splitting the layers of the system into separate packages. In the real -world, I would probably use a single python package for the whole app, but in -other languages - Java, C#, C++ - I would usually have a single binary for each -layer. Splitting the packages up this way makes it easier to understand how the -dependencies work.
-from typing import NamedTuple
-from expects import expect, have_len, equal
-
-# Domain model
-
-class IssueReporter:
- def __init__(self, name, email):
- self.name = name
- self.email = email
-
-
-class Issue:
- def __init__(self, reporter, description):
- self.description = description
- self.reporter = reporter
-
-
-class IssueLog:
- def add(self, issue):
- pass
-
-
-class ReportIssueCommand(NamedTuple):
- reporter_name: str
- reporter_email: str
- problem_description: str
-
-
-# Service Layer
-
-class ReportIssueHandler:
-
- def __init__(self, issue_log):
- self.issue_log = issue_log
-
- def __call__(self, cmd):
- reported_by = IssueReporter(
- cmd.reporter_name,
- cmd.reporter_email)
- issue = Issue(reported_by, cmd.problem_description)
- self.issue_log.add(issue)
-
-
-# Adapters
-
-class FakeIssueLog(IssueLog):
-
- def __init__(self):
- self.issues = []
-
- def add(self, issue):
- self.issues.append(issue)
-
- def get(self, id):
- return self.issues[id]
-
- def __len__(self):
- return len(self.issues)
-
- def __getitem__(self, idx):
- return self.issues[idx]
-
-
-email = "bob@example.org"
-name = "bob"
-desc = "My mouse won't move"
-
-
-class When_reporting_an_issue:
-
- def given_an_empty_issue_log(self):
- self.issues = FakeIssueLog()
-
- def because_we_report_a_new_issue(self):
- handler = ReportIssueHandler(self.issues)
- cmd = ReportIssueCommand(name, email, desc)
-
- handler(cmd)
-
- def the_handler_should_have_created_a_new_issue(self):
- expect(self.issues).to(have_len(1))
-
- def it_should_have_recorded_the_issuer(self):
- expect(self.issues[0].reporter.name).to(equal(name))
- expect(self.issues[0].reporter.email).to(equal(email))
-
- def it_should_have_recorded_the_description(self):
- expect(self.issues[0].description).to(equal(desc))
-There’s not a lot of functionality here, and our issue log has a couple of -problems, firstly there’s no way to see the issues in the log yet, and secondly -we’ll lose all of our data every time we restart the process. We’ll fix the -second of those in the next part -[https://io.made.com/blog/repository-and-unit-of-work-pattern-in-python/].
-
-
-
-
-
-
-
-
-
-
- 







-
- 







-
-
-
-
-
-
-
-
-
-
-
-