http://blog.8thlight.com/uncle-bob/2011/11/22/Clean-Architecture.html
http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
THE USE OF SUB-ROUTINES IN PROGRAMMES
D. J. Wheeler
Cambridge & Illinois Universities
“This last task may be the most difficult.”
Hiding complexity
import requests # Listing 1
from urllib import urlencode
def find_definition(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
response = requests.get(url)
data = response.json()
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def find_definition(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
response = requests.get(url)
data = response.json()
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def find_definition(word): # Listing 2
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
data = call_json_api(url)
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def call_json_api(url):
response = requests.get(url)
data = response.json()
return data
def call_json_api(url):
response = requests.get(url)
data = response.json()
return data
import json
...
response = requests.get(url)
text = response.text
data = json.loads(text)
def call_json_api(url):
response = requests.get(url)
data = response.json()
return data
def find_definition(word): # Listing 2
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
data = call_json_api(url)
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def call_json_api(url):
response = requests.get(url)
data = response.json()
return data
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
def build_url(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
return url
def pluck_definition(data):
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
# Build the URL
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
Replace comments with names:
def build_url(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
↘
procedure
↘
procedure
↘
i/o procedure
↘
procedure
↘
pure function
↘
i/o procedure
↘
pure function
2004 — Martin Fowler
import requests
def find_definition(word, requests=requests):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
response = requests.get(url)
data = response.json()
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
class FakeRequestsLibrary(object):
def get(self, url):
self.url = url
return self
def json(self):
return self.data
def test_find_definition():
fake = FakeRequestsLibrary()
fake.data = {u'Definition': 'abc'}
definition = find_definition(
'testword', requests=fake)
assert definition == 'abc'
assert fake.url == (
'http://api.duckduckgo.com/'
'?q=define+testword&format=json')
↘
big_procedure(web=web, db=db, fs=fs)
↘
smaller_procedure(web=web, db=db)
↘
little_helper(web=web)
from mock import patch
def test_find_definition():
fake = FakeRequestsLibrary()
fake.data = {u'Definition': u'abc'}
with patch('requests.get', fake.get):
definition = find_definition('testword')
assert definition == 'abc'
assert fake.url == (
'http://api.duckduckgo.com/'
'?q=define+testword&format=json')
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
def build_url(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
return url
def pluck_definition(data):
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
def test_build_url():
assert build_url('word') == (
'http://api.duckduckgo.com/'
'?q=define+word&format=json')
def test_build_url_with_punctuation():
assert build_url('what?!') == (
'http://api.duckduckgo.com/'
'?q=define+what%3F%21&format=json')
def test_build_url_with_hyphen():
assert build_url('hyphen-ate') == (
'http://api.duckduckgo.com/'
'?q=define+hyphen-ate&format=json')
import pytest
def test_pluck_definition():
assert pluck_definition(
{u'Definition': u'something'}
) == 'something'
def test_pluck_definition_missing():
with pytest.raises(ValueError):
pluck_definition(
{u'Definition': u''}
)
call_test(good_url, good_data)
call_test(bad_url1, whatever)
call_test(bad_url2, whatever)
call_test(bad_url3, whatever)
call_test(good_url, bad_data1)
call_test(good_url, bad_data2)
call_test(good_url, bad_data3)
So let’s talk architecture
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
def build_url(word):
q = 'define ' + word
url = 'http://api.duckduckgo.com/?'
url += urlencode({'q': q, 'format': 'json'})
return url
def pluck_definition(data):
definition = data[u'Definition']
if definition == u'':
raise ValueError('that is not a word')
return definition
PyCon talks:
def find_definition(word): # Listing 3
url = build_url(word)
data = call_json_api(url)
return pluck_definition(data)
LISP, Haskell, Clojure, F#
# I/O as a side effect
def uppercase_words(wordlist):
for word in wordlist:
word = word.upper()
print word
# Logic with zero side-effects
def process_words(wordlist):
return [word.upper() for word in wordlist]
# I/O goes outside of logic
def procedural_glue(wordlist):
upperlist = process_words(wordlist)
for word in upperlist:
print word
McIlroy vs. Knuth
Knuth — Literate programming
Knuth: 10 pages of Pascal
McIlroy: 6-line shell script
tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
So why immutability?
for i in range(len(items)):
item[i] = transform(item[i])
items = [tranform(item) for item in items]
items = list(load_items())
items.sort()
for item in items:
...
for item in sorted(items):
..
for college in university.colleges:
for school in college.schools:
for department in school.departments:
...
def all_departments(college):
for college in university.colleges:
for school in college.schools:
for department in school.departments:
yield department
def process1():
...
for department in all_departments(college):
....
def process2():
...
names = [department.name for department
in all_departments(college)]
....
r = get_resource()
r.get_ready()
try:
use(r)
finally:
r.close()
with get_resource() as r:
use(r)
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
...
A → B → C → D
B
↗↙
A ←→ C
↘↖
D
Let’s return to Wheeler
☘
@brandon_rhodes