Skip to content

Commit 08fe98c

Browse files
committed
Add Dependency Inversion
1 parent 64ee320 commit 08fe98c

File tree

1 file changed

+90
-1
lines changed

1 file changed

+90
-1
lines changed

README.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,96 @@ def view(request):
13021302

13031303
### **Dependency Inversion Principle (DIP)**
13041304

1305-
*Coming soon*
1305+
> “Depend upon abstractions, not concrete details”,
1306+
> Uncle Bob.
1307+
1308+
Imagine we wanted to write a web view that returns an HTTP response that
1309+
streams rows of a CSV file we create on the fly. We want to use the CSV writer
1310+
that is provided by the standard library.
1311+
1312+
**Bad**
1313+
```python
1314+
import csv
1315+
from io import StringIO
1316+
1317+
1318+
class StreamingHttpResponse:
1319+
"""A streaming HTTP response"""
1320+
... # implementation code goes here
1321+
1322+
1323+
def some_view(request):
1324+
rows = (
1325+
['First row', 'Foo', 'Bar', 'Baz'],
1326+
['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"]
1327+
)
1328+
# Define a generator to stream data directly to the client
1329+
def stream():
1330+
buffer_ = StringIO()
1331+
writer = csv.writer(buffer_, delimiter=';', quotechar='"')
1332+
for row in rows:
1333+
writer.writerow(row)
1334+
buffer_.seek(0)
1335+
data = buffer_.read()
1336+
buffer_.seek(0)
1337+
buffer_.truncate()
1338+
yield data
1339+
1340+
# Create the streaming response object with the appropriate CSV header.
1341+
response = StreamingHttpResponse(stream(), content_type='text/csv')
1342+
response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
1343+
1344+
return response
1345+
1346+
```
1347+
1348+
Our first implementation works around the CSV's writer interface by manipulating
1349+
a `StringIO` object (which is file-like) and performing several low level
1350+
operations in order to farm out the rows from the writer. It's a lot of work
1351+
and not very elegant.
1352+
1353+
A better way is to leverage the fact that the writer just needs an object with
1354+
a `.write()` method to do our bidding. Why not pass it a dummy object that
1355+
immediately returns the newly assembled row, so that the `StreamingHttpResponse`
1356+
class can immediate stream it back to the client?
1357+
1358+
**Good**
1359+
```python
1360+
import csv
1361+
1362+
1363+
class Echo:
1364+
"""An object that implements just the write method of the file-like
1365+
interface.
1366+
"""
1367+
def write(self, value):
1368+
"""Write the value by returning it, instead of storing in a buffer."""
1369+
return value
1370+
1371+
def some_streaming_csv_view(request):
1372+
"""A view that streams a large CSV file."""
1373+
rows = (
1374+
['First row', 'Foo', 'Bar', 'Baz'],
1375+
['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"]
1376+
)
1377+
writer = csv.writer(Echo(), delimiter=';', quotechar='"')
1378+
return StreamingHttpResponse(
1379+
(writer.writerow(row) for row in rows),
1380+
content_type="text/csv",
1381+
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
1382+
)
1383+
1384+
```
1385+
1386+
Much better, and it works like a charm! The reason it's superior to the previous
1387+
implementation should be obvious: less code (and more performant) to achieve
1388+
the same result. We decided to leverage the fact that the writer class depends
1389+
on the `.write()` abstraction of the object it receives, without caring about
1390+
the low level, concrete details of what the method actually does.
1391+
1392+
This example was taken from
1393+
[a submission made to the Django documentation](https://code.djangoproject.com/ticket/21179)
1394+
by this author.
13061395

13071396
**[⬆ back to top](#table-of-contents)**
13081397

0 commit comments

Comments
 (0)