You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -43,17 +45,17 @@ In order to overcome these problems, this styleguide tries to achieve the follow
43
45
44
46
1) Treat Django's `apps` more like software `domains`.
45
47
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**.
47
49
4) Design a styleguide that reduces the effort involved in extracting the code for large domains into separate application servers.
48
50
5) Make sure the styleguide compliments API-based applications.
49
51
50
52
## Domains
51
53
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.
53
55
54
56
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.
55
57
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`.
57
59
58
60
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.
59
61
@@ -76,10 +78,10 @@ An example _software_ domain is provided in the same directory as this styleguid
76
78
A domain **must** use the following file structure:
77
79
78
80
```
79
-
apis.py - Public functions and access points.
81
+
apis.py - Public functions and access points, presentation logic.
80
82
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.
83
85
```
84
86
85
87
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
110
112
111
113
This keeps namespaces tidy and does not leak domain details.
112
114
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.
114
116
115
117
* 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.
116
118
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
+
117
127
<hr>
118
128
119
129
# Files in-depth
@@ -122,7 +132,7 @@ In the examples below we imagine a service with two domains - one for books, and
122
132
123
133
## Models
124
134
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.
126
136
127
137
A models.py file can look like:
128
138
@@ -144,10 +154,11 @@ class Book(models.Model):
144
154
145
155
```
146
156
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.
148
159
- Models **can** have computed properties where it makes sense.
149
160
- 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.
151
162
152
163
153
164
## APIs
@@ -175,26 +186,26 @@ class BookAPI:
175
186
176
187
```
177
188
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.
180
192
- You **can** group interal API functions under a class if it makes sense for organisation.
181
193
- 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.
184
196
- 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.
191
202
192
203
193
204
## Interfaces
194
205
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.
196
207
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!
198
209
199
210
An interfaces.py may look like:
200
211
@@ -234,23 +245,23 @@ class AuthorInterface:
234
245
235
246
```
236
247
237
-
- The primary components of interfaces.py**should** be functions.
248
+
- The primary components of Interfaces**should** be functions.
238
249
- You **can** group functions under a class if it makes sense for organisation.
239
250
- 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.
242
253
243
254
## Services
244
255
245
-
Everything in a domain comes together in `services.py`.
256
+
Everything in a domain comes together in Services.
246
257
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:
248
259
249
260
- When creating a new instance of a model, we need to compute a field on it before saving.
250
261
- When querying some content, we need to collect it from a few different places and gather it together in a python object.
251
262
- When deleting an instance we need to send a signal to another domain so it can do it's own logic.
252
263
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.
254
265
255
266
A services.py file could look like:
256
267
@@ -317,7 +328,8 @@ class PGMNodeService:
317
328
318
329
```
319
330
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.
321
333
- You **can** group functions under a class if it makes sense for organisation.
322
334
- If you are using a class, it **must** use the naming convention `MyDomainService`.
323
335
- Functions in services.py **must** use type annotations.
0 commit comments