Skip to content

Commit 549efe7

Browse files
authored
Merge pull request #2 from phalt/v-1-1
Clarification on where logic lives
2 parents be9d1b5 + 7b9e7a9 commit 549efe7

File tree

1 file changed

+41
-29
lines changed

1 file changed

+41
-29
lines changed

README.md

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
| Version | Author(s) | Date |
66
| ------- |-------------------------------------------|------------|
77
| 1.0 | Paul Hallett paulandrewhallett@gmail.com | 01-02-2019 |
8+
| 1.1 | Paul Hallett paulandrewhallett@gmail.com | DRAFT |
89

910

1011
**Table of contents:**
@@ -17,6 +18,7 @@
1718
- [Styleguide](#styleguide)
1819
- [Visualisation](#visualisation)
1920
- [File structure](#file-structure)
21+
- [Which logic lives where?](#which-logic-lives-where)
2022
- [Files in-depth](#files-in-depth)
2123
- [Models](#models)
2224
- [APIs](#apis)
@@ -43,17 +45,17 @@ In order to overcome these problems, this styleguide tries to achieve the follow
4345

4446
1) Treat Django's `apps` more like software `domains`.
4547
2) Extend Django's `apps` implementation to support strong [bounded context](https://www.martinfowler.com/bliki/BoundedContext.html) patterns between `domains`.
46-
3) Enable separation of domains to happen when it makes sense for **increased development velocity**, not just for **business logic**.
48+
3) Enable separation of domains to happen when it makes sense for **increased development velocity**, not just for **business value**.
4749
4) Design a styleguide that reduces the effort involved in extracting the code for large domains into separate application servers.
4850
5) Make sure the styleguide compliments API-based applications.
4951

5052
## Domains
5153

52-
A [domain](https://en.wikipedia.org/wiki/Domain_(software_engineering)) is considered a distinct _business problem_ within the context of your application.
54+
A [domain](https://en.wikipedia.org/wiki/Domain_(software_engineering)) provides distinct business value within the context of your application.
5355

5456
Within the context of software, what this styleguide calls a `domain` is roughly an extension of what Django would call an "app". Therefore a _business_ domain **should** have at least one distinct _software_ domain mirroring it.
5557

56-
The examples below will talk about a business problem for a `book shop` that must share details about books. This can be modelled as a _domain_ called `books`, and as a _software domain_ also called `books`.
58+
The examples below will talk about a `book shop` that must share details about books. This can be modelled as a _domain_ called `books`, and as a _software domain_ also called `books`.
5759

5860
We keep the key benefits of Django's `app` pattern - namely Django's [models](https://docs.djangoproject.com/en/2.1/topics/db/models/) to represent tables in a datastore, with an emphasis on **skinny models**. We also retain Django's ability to *package apps as installable components in other applications*. This allows domains to be easily migrated to different codebases or completely different projects.
5961

@@ -76,10 +78,10 @@ An example _software_ domain is provided in the same directory as this styleguid
7678
A domain **must** use the following file structure:
7779

7880
```
79-
apis.py - Public functions and access points.
81+
apis.py - Public functions and access points, presentation logic.
8082
interfaces.py - Integrations with other domains or external services.
81-
models.py - Object models and storage.
82-
services.py - Business and functional logic.
83+
models.py - Object models and storage, simple information logic.
84+
services.py - coordination and transactional logic.
8385
```
8486

8587
In addition, any existing files from a standard Django app are still allowed, such as `urls.py`, `apps.py` and `migrations/*`. `Views.py` in [Django's pattern](https://docs.djangoproject.com/en/dev/#the-view-layer) is **explicitly not allowed** in this styleguide pattern as we only focus on API-based applications. Most logic that used to live in Django's `views.py` would now be separated into `apis.py` and `services.py`.
@@ -110,10 +112,18 @@ from domain.apis import Foo
110112

111113
This keeps namespaces tidy and does not leak domain details.
112114

113-
* A domain **does not** need to have all these files if it is not using them. For example - a domain that just co-ordinates API calls to other domains does not need to have `models.py` as it is probably not storing anything in a datastore.
115+
* A domain **does not** need to have all these files if it is not using them. For example - a domain that just coordinates API calls to other domains does not need to have `models.py` as it is probably not storing anything in a datastore.
114116

115117
* A domain **can have** additional files when it makes sense (such as `utils.py` or `enums.py` or `serializers.py`) to separate out parts of the code that aren't covered by the styleguide pattern.
116118

119+
## Which logic lives where?
120+
121+
It's common in programming to end up confused about what type of logic should live where - should it go in the `apis.py`, the `models.py`, or the `services.py`? There are many cases where it's difficult to decide, and the best advice is to **pick a pattern and stick to it**, but for simpler things, this guide emphasises the following:
122+
123+
- apis.py - logic about presentation (Where should I show this data to the user? Where do I define the API schema?)
124+
- services.py - logic around coordination and transactions (Where do I coordinate updating many models in one domain? Where do I dispatch a single action out to other domains?)
125+
- models.py - logic around information (Where can I store this data? Where can I do any post/pre-save actions?) and derived information computed by already existing information
126+
117127
<hr>
118128

119129
# Files in-depth
@@ -122,7 +132,7 @@ In the examples below we imagine a service with two domains - one for books, and
122132

123133
## Models
124134

125-
Models defines how a data model/ database table looks. This is a Django convention that remains mostly unchanged. The key difference here is that you use _skinny models_ - no functional or business logic should live here. In the past Django has recommended an [active record](https://docs.djangoproject.com/en/2.1/misc/design-philosophies/#models) style for it's models. In practice, we have found that this encourages developers to make `models.py` bloated and do too much - often binding the presentation and business logic of a domain too tightly. This makes it very hard to have abstract presentations of the data in a domain. Putting all the logic in one place also makes it difficult to scale the number of developers working in this part of the codebase.
135+
Models defines how a data model/ database table looks. This is a Django convention that remains mostly unchanged. The key difference here is that you use _skinny models_ - no complex functional logic should live here. In the past Django has recommended an [active record](https://docs.djangoproject.com/en/2.1/misc/design-philosophies/#models) style for it's models. In practice, we have found that this encourages developers to make `models.py` bloated and do too much - often binding the presentation and functional logic of a domain too tightly. This makes it very hard to have abstract presentations of the data in a domain. Putting all the logic in one place also makes it difficult to scale the number of developers working in this part of the codebase. See the _"Where should logic live?"_ section above for clarification.
126136

127137
A models.py file can look like:
128138

@@ -144,10 +154,11 @@ class Book(models.Model):
144154

145155
```
146156

147-
- Models **must not** have any complex business logic functions attached to them.
157+
- Models **must not** have any complex functional logic in them.
158+
- Models **should** own informational logic related to them.
148159
- Models **can** have computed properties where it makes sense.
149160
- Models **must not** import services, interfaces, or apis from their own domain or other domains.
150-
- Table dependencies (such as ForeignKeys) **must not** exist across domains. Use a UUID field instead, and have your `services.py` control the relationship between models. You **can** use ForeignKeys between tables in one domain. Be aware that this might hinder future refactoring.
161+
- Table dependencies (such as ForeignKeys) **must not** exist across domains. Use a UUID field instead, and have your Services control the relationship between models. You **can** use ForeignKeys between tables in one domain. Be aware that this might hinder future refactoring.
151162

152163

153164
## APIs
@@ -175,26 +186,26 @@ class BookAPI:
175186

176187
```
177188

178-
- `Apis.py` **must be** used as the entry point for all other consumers who wish to use this domain.
179-
- Internal APIs **should** just be functions.
189+
- APIs **must be** used as the entry point for all other consumers who wish to use this domain.
190+
- APIs **should** own presentational logic and schema declarations.
191+
- Internal domain-to-domain APIs **should** just be functions.
180192
- You **can** group interal API functions under a class if it makes sense for organisation.
181193
- If you are using a class for your internal APIs, it **must** use the naming convention `MyDomainAPI`.
182-
- Internal functions in apis.py **must** use type annotations.
183-
- Internal functions in apis.py **must** use keyword arguments.
194+
- Internal functions in APIs **must** use type annotations.
195+
- Internal functions in APIs **must** use keyword arguments.
184196
- You **should** log API call functions.
185-
- All data returned from `apis.py` **must be** JSON serializable.
186-
- `Apis.py` **must** talk to `services.py` to get data.
187-
- It **must not** talk to `models.py` directly.
188-
- It **must not** do any business logic.
189-
- `Apis.py` **can** do simple business logic like transforming data for the outside world, or taking external data and transforming it for the domain to understand.
190-
- Objects represented through the API **do not** have to map directly to internal database representations of data.
197+
- All data returned from APIs **must be** JSON serializable.
198+
- APIs **must** talk to Services to get data.
199+
- APIs **must not** talk to Models directly.
200+
- APIs **should** do simple logic like transforming data for the outside world, or taking external data and transforming it for the domain to understand.
201+
- Objects represented through APIs **do not** have to map directly to internal database representations of data.
191202

192203

193204
## Interfaces
194205

195-
Your domain may need to communicate with another domain. That domain can be in another web server across the web, or it could be within the same server. It could even be a third-party service. When your domain needs to talk to other domains, you should define **all interactions with it in the interfaces.py file**. Combined with `apis.py` (see above), this forms the bounded context of the domain, and prevents business logic leaking in.
206+
Your domain may need to communicate with another domain. That domain can be in another web server across the web, or it could be within the same server. It could even be a third-party service. When your domain needs to talk to other domains, you should define **all interactions with it in the interfaces.py file**. Combined with APIs (see above), this forms the bounded context of the domain, and prevents domain logic leaking in.
196207

197-
Consider interfaces.py like a mini _Anti-Corruption Layer_. Most of the time it won't change and it'll just pass on arguments to an API function. But when the other domain moves - say you extract it into it's own web service, your domain only needs to update the `interfaces.py` to reflect the change. No complex refactoring needed, woohoo!
208+
Consider interfaces.py like a mini _Anti-Corruption Layer_. Most of the time it won't change and it'll just pass on arguments to an API function. But when the other domain moves - say you extract it into it's own web service, your domain only needs to update the code in `interfaces.py` to reflect the change. No complex refactoring needed, woohoo!
198209

199210
An interfaces.py may look like:
200211

@@ -234,23 +245,23 @@ class AuthorInterface:
234245

235246
```
236247

237-
- The primary components of interfaces.py **should** be functions.
248+
- The primary components of Interfaces **should** be functions.
238249
- You **can** group functions under a class if it makes sense for organisation.
239250
- If you are using a class, it **must** use the naming convention `MyDomainInterface`.
240-
- Functions in interfaces.py **must** use type annotations.
241-
- Functions in interfaces.py **must** use keyword arguments.
251+
- Functions in Interfaces **must** use type annotations.
252+
- Functions in Interfaces **must** use keyword arguments.
242253

243254
## Services
244255

245-
Everything in a domain comes together in `services.py`.
256+
Everything in a domain comes together in Services.
246257

247-
Services defines all the business-problem logic that might be needed for this domain. What is considered a bussiness-problem? Here are a few examples:
258+
Services gather all the business value for this domain. What type of logic should live here? Here are a few examples:
248259

249260
- When creating a new instance of a model, we need to compute a field on it before saving.
250261
- When querying some content, we need to collect it from a few different places and gather it together in a python object.
251262
- When deleting an instance we need to send a signal to another domain so it can do it's own logic.
252263

253-
Anything that is specific to the domain problem should live in `services.py`. As most API projects expose single functional actions such as Create, Read, Update, and Delete, `services.py` has been designed specifically to compliment stateless, single-action functions.
264+
Anything that is specific to the domain problem and **not** basic informational logic should live in Services. As most API projects expose single functional actions such as Create, Read, Update, and Delete, Services has been designed specifically to compliment stateless, single-action functions.
254265

255266
A services.py file could look like:
256267

@@ -317,7 +328,8 @@ class PGMNodeService:
317328

318329
```
319330

320-
- The primary components of `services.py` **should** be functions.
331+
- The primary components of Services **should** be functions.
332+
- Services **should** own co-ordination and transactional logic.
321333
- You **can** group functions under a class if it makes sense for organisation.
322334
- If you are using a class, it **must** use the naming convention `MyDomainService`.
323335
- Functions in services.py **must** use type annotations.

0 commit comments

Comments
 (0)