Programming is hard, but we can magnify our efforts with excellent API design. Let’s explore how, as we consider compactness, orthogonality, consistency, safety, coupling, state handling, layering, and more, illustrated with practical examples (and gruesome mistakes!) from several popular Python libraries.
1. Fluent,
Fluid APIs by Erik Rose
“A poet is, before anything else, a person
who is passionately in love with language.”
—W. H. Auden
Welcome to Fluid, Fluent APIs or, perhaps more fluently, poetic APIs.
My name is Erik Rose and I work at Mozilla as not a poet.
Nevertheless, I’m in love with language as well—formal languages,
human language, even lexically structured music and dance.
Mark Twain: language is the best way of getting ideas from one head
into another without surgery.
So I want to tell you a story about a man who didn’t have this ability for
27 years.
Nevertheless, all programmers share a love for language as well.
Let me tell you a story about a man who was missing this power—and
even the more basic power of human words.
2. Poetic APIs by Erik Rose
“A poet is, before anything else, a person
who is passionately in love with language.”
—W. H. Auden
Welcome to Fluid, Fluent APIs or, perhaps more fluently, poetic APIs.
My name is Erik Rose and I work at Mozilla as not a poet.
Nevertheless, I’m in love with language as well—formal languages,
human language, even lexically structured music and dance.
Mark Twain: language is the best way of getting ideas from one head
into another without surgery.
So I want to tell you a story about a man who didn’t have this ability for
27 years.
Nevertheless, all programmers share a love for language as well.
Let me tell you a story about a man who was missing this power—and
even the more basic power of human words.
3. Poetic APIs
programmer
by Erik Rose
^
“A poet is, before anything else, a person
who is passionately in love with language.”
—W. H. Auden
Welcome to Fluid, Fluent APIs or, perhaps more fluently, poetic APIs.
My name is Erik Rose and I work at Mozilla as not a poet.
Nevertheless, I’m in love with language as well—formal languages,
human language, even lexically structured music and dance.
Mark Twain: language is the best way of getting ideas from one head
into another without surgery.
So I want to tell you a story about a man who didn’t have this ability for
27 years.
Nevertheless, all programmers share a love for language as well.
Let me tell you a story about a man who was missing this power—and
even the more basic power of human words.
5. “Oh! Everything has a name!”
Now, this was a 27-year-old man who had been around. He didn’t
live in the jungle. But, as Susan describes it, the look on his face
was as if he had never seen a window before. The window became
a different thing by having a symbol attached to it.
6. “The window became a different thing by
having a symbol attached to it.”
He had a category for it. He could talk about a window to someone.
7. Inventing
Language
“Any fool can write code that a computer can understand.
Good programmers write code that humans can understand.”
—Martin Fowler
In creating an API, your task is not only to make something that works—
but to put your categories for the universe into somebody else's head—
to choose symbols that all discourse will be in terms of.
As you do so, take care, because: limits the thoughts users will be able to
have about the problem domain.
Ildefanso trouble w/proper names & time
“When did we last meet?” → “In the dry season” or “At the holidays”.
The abstract names and numbers we give to various divisions of time
aren’t the abstractions he’s built his world on.
Before he had lang, he could do simple math with quantites up to about
10, but he couldn’t get much farther until he had words.
Likewise, when you make an API,
opportunity to succeed: magnifying efforts of all those who come after
you
fail: frustrating all those who come after you
Example: urllib2 vs. Requests
8. req = urllib2.Request(gh_url)
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(
None, 'https://api.github.com', 'user', 'pass')
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)
urllib2.install_opener(opener)
handler = urllib2.urlopen(req)
print handler.getcode()
urllib2: stdlib, horrendously verbose, hard to do common things
(partially because it’s not just for HTTP)
Requests: 3rd-party, de facto standard because it harnesses the
common concept of an HTTP get—productivity multiplier
This is why API design makes greatest demands on fluency:
You're drawing out borders around concepts which have to fit a variety
of other people’s brains and be adaptable enough for situations you
haven’t forseen.
I’m going to give you 7 rules of thumb to increase your chances.
The first one is…
9. req = urllib2.Request(gh_url)
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(
None, 'https://api.github.com', 'user', 'pass')
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)
urllib2.install_opener(opener)
handler = urllib2.urlopen(req)
print handler.getcode()
r = requests.get('https://api.github.com', auth=('user', 'pass'))
print r.status_code
urllib2: stdlib, horrendously verbose, hard to do common things
(partially because it’s not just for HTTP)
Requests: 3rd-party, de facto standard because it harnesses the
common concept of an HTTP get—productivity multiplier
This is why API design makes greatest demands on fluency:
You're drawing out borders around concepts which have to fit a variety
of other people’s brains and be adaptable enough for situations you
haven’t forseen.
I’m going to give you 7 rules of thumb to increase your chances.
The first one is…
10. Don’t Be An
Architecture
Astronaut
My first step when designing a new library is actually to not design
a new library.
It’s all well and good to seek to do good design in the abstract,
but design is basically imagination,
and imagination is essentially hypothesizing,
and we know from history how well hypotheses do when they’re
not continually tested and brought back to earth by reality.
You end up with alchemy rather than chemistry.
11. Very impressive to look at and academically interesting but it’s
merely a self-consistent system of nonsense,
not anything of practical use.
So, to bring our efforts back into the realm of science, remember
that the best libraries are extracted, not invented.
12. The best libraries are extracted, not invented.
What this means is that you should have an application that already
does the things you want your library to do.
[Preferably 2—and as different as possible.]
And then take the bits of them that do the things you’re interested
in and factor them up.
13. For example, I wrote a plugin nose-progressive for the popular
Python test framework nose,
and its purpose was to display a progress bar and just generally
make output easier to follow.
It made use of a lot of terminal escape codes, for things like
bolding, coloring, and positioning.
The code looked something like this.
14. For example, I wrote a plugin nose-progressive for the popular
Python test framework nose,
and its purpose was to display a progress bar and just generally
make output easier to follow.
It made use of a lot of terminal escape codes, for things like
bolding, coloring, and positioning.
The code looked something like this.
15. def terminal_codes(self, stream):
capabilities = ['bold', 'sc', 'rc', 'sgr0', 'el']
if hasattr(stream, 'fileno') and isatty(stream.fileno()):
# Explicit args make setupterm() work even when -s is passed:
setupterm(None, stream.fileno()) # so things like tigetstr() work
codes = dict((x, tigetstr(x)) for x in capabilities)
cup = tigetstr('cup')
codes['cup'] = lambda line, column: tparm(cup, line, column)
else:
# If you're crazy enough to pipe this to a file or something,
# don't output terminal codes:
codes = defaultdict(lambda: '', cup=lambda line, column: '')
return codes
...
self._codes = terminal_codes(stdout)
...
class AtLine(object):
def __enter__(self):
"""Save position and move to progress bar, col 1."""
self.stream.write(self._codes['sc']) # save position
self.stream.write(self._codes['cup'](self.line, 0))
def __exit__(self, type, value, tb):
self.stream.write(self._codes['rc']) # restore position
...
print self._codes['bold'] + results + self._codes['sgr0']
Lots of terminal setup and bookkeeping all intertwined with code
that actually kept track of tests.
Raw terminal capability names like sgr0 and rc that I had to keep
looking up. [TODO: red underlines]
Every time I’d have to do some formatting I’d think “Geez, I wish
there were a decent abstraction around this so I wouldn’t have to
look at it.” It’s at this point—when you have a real, useful program
with a library struggling to get out—that you can come down from
orbit and start thinking about library design.
So it was time for blessings—my terminal formatting library—to be
born.
16. Tasks
The first thing to do is to dump everything out on the workbench.
What sort of things ought the library to do? bam bam
Now, what tools have we got at our workbench? Well…all the stuff in
our language…
17. Tasks
Print some text with formatting.
The first thing to do is to dump everything out on the workbench.
What sort of things ought the library to do? bam bam
Now, what tools have we got at our workbench? Well…all the stuff in
our language…
18. Tasks
Print some text with formatting.
Print some text at a location, then snap back.
The first thing to do is to dump everything out on the workbench.
What sort of things ought the library to do? bam bam
Now, what tools have we got at our workbench? Well…all the stuff in
our language…
19. Language Constructs
Functions, arguments, keyword arguments
Decorators
Context managers
Classes (really more of an implementation detail)
bam bam bam bam
common patterns…:
bam bam bam
Then we shake the workbench up and see if we can find some good
pairings between tools and tasks. What guides you? Consistency.
20. Language Constructs
Functions, arguments, keyword arguments
Decorators
Context managers
Classes (really more of an implementation detail)
å
Patterns, Protocols, Interfaces, and Conventions
Sequences
Iterators
Mappings
bam bam bam bam
common patterns…:
bam bam bam
Then we shake the workbench up and see if we can find some good
pairings between tools and tasks. What guides you? Consistency.
21. Consistency
“Think like a wise man, but communicate
in the language of the people.”
—William Butler Yeats
You have the entire technical and cultural weight of the language itself
behind you as well as accumulated habits of influential libs and the
programmer community.
When we're designing web sites, we think of it this way: people spend 90%
of their time on other people's web sites, so…
make the logo link to the home page
put the log-in link in the upper right
and so on
When writing an API, realize:
users will spend 90% time using other APIs
You can be weird and clever if you need to be, but if you can manage to
stick with conventions, you’ll get these bonuses…
1. conveys respect for your users. You're not some outsider who is going
to make their code a mishmash of clashing styles.
I can't tell you how many times I've encountered a Java API masquerading
as Python one and thrown it away in disgust.
getters and setters + violation of case conventions → I'm going to move on.
22. Brilliant thing about early Mac project: explicit HIGs
Every program same commands: open, close, quit, copy, paste. In same
menus w/same keyboard shortcuts.
Learn one program → learned 'em all
If you implement get(key, default) and call it that, people will pick it up
faster and remember it better than fetch(default, key).
3. Polymorphism. Can use calling code on dicts or whatever your class is.
23. get(key, default) > fetch(default, key)
Brilliant thing about early Mac project: explicit HIGs
Every program same commands: open, close, quit, copy, paste. In same
menus w/same keyboard shortcuts.
Learn one program → learned 'em all
If you implement get(key, default) and call it that, people will pick it up
faster and remember it better than fetch(default, key).
3. Polymorphism. Can use calling code on dicts or whatever your class is.
24. Tasks
Print some text at a location, then snap back.
Print some text with formatting.
So, coming back to our tasks at hand, how do we bang those tasks
against existing language features and conventions?
Well, I like to get as many alternatives out on the workbench as
possible. I call these sketches.
Really only 2 choices for printing at a location.
Both are okay, but the 2nd gives us the flexibility to put multiple
statements inside—even calls out to other functions.
And then the first can be written in terms of it, if you like.
25. Tasks
Print some text at a location, then snap back.
Print some text with formatting.
print_at('Hello world', 10, 2)
with location(10, 2):
print 'Hello world'
for thing in range(10):
print thing
So, coming back to our tasks at hand, how do we bang those tasks
against existing language features and conventions?
Well, I like to get as many alternatives out on the workbench as
possible. I call these sketches.
Really only 2 choices for printing at a location.
Both are okay, but the 2nd gives us the flexibility to put multiple
statements inside—even calls out to other functions.
And then the first can be written in terms of it, if you like.
26. Tasks
Print some text at a location, then snap back.
Print some text with formatting.
print_formatted('Hello world', 'red', 'bold')
print color('Hello world', 'red', 'bold')
print color('<red><bold>Hello world</bold></red>')
print red(bold('Hello') + 'world')
print codes['red'] + codes['bold'] +
'Hello world' + codes['normal']
print '{t.red}Hi{t.bold}Mom{t.normal}'.format(t=terminal)
print terminal.red_bold + 'Hello world' + terminal.normal
Here are sketches for text formatting. We see some square
brackets here, some dots, some string concatenation, some
function calls with kwargs and with positional args. Most separate
the printing from the formating, but one combines them.
I have a couple of favorites here.
this…common function call syntax…composable. It frankly looks
like HTML.
trouble: no portable definition of a "turn [bold, for example] off"
escape code, so state
have to embed it in the string or in a parallel data structure
-> enormous code complexity & mental load for users
went with attrs
...we save a great deal of complexity—both internally and for the
caller
Consistent: use with Python’s built-in templating language: less to
27. Tasks
Print some text at a location, then snap back.
Print some text with formatting.
print_formatted('Hello world', 'red', 'bold')
print color('Hello world', 'red', 'bold')
print color('<red><bold>Hello world</bold></red>')
print red(bold('Hello') + 'world')
print codes['red'] + codes['bold'] +
'Hello world' + codes['normal']
print '{t.red}Hi{t.bold}Mom{t.normal}'.format(t=terminal)
print terminal.red_bold + 'Hello world' + terminal.normal
Here are sketches for text formatting. We see some square
brackets here, some dots, some string concatenation, some
function calls with kwargs and with positional args. Most separate
the printing from the formating, but one combines them.
I have a couple of favorites here.
this…common function call syntax…composable. It frankly looks
like HTML.
trouble: no portable definition of a "turn [bold, for example] off"
escape code, so state
have to embed it in the string or in a parallel data structure
-> enormous code complexity & mental load for users
went with attrs
...we save a great deal of complexity—both internally and for the
caller
Consistent: use with Python’s built-in templating language: less to
28. Tasks
Print some text at a location, then snap back.
Print some text with formatting.
print_formatted('Hello world', 'red', 'bold')
print color('Hello world', 'red', 'bold')
print color('<red><bold>Hello world</bold></red>')
print red(bold('Hello') + 'world')
print codes['red'] + codes['bold'] +
'Hello world' + codes['normal']
print '{t.red}Hi{t.bold}Mom{t.normal}'.format(t=terminal)
print terminal.red_bold + 'Hello world' + terminal.normal
Here are sketches for text formatting. We see some square
brackets here, some dots, some string concatenation, some
function calls with kwargs and with positional args. Most separate
the printing from the formating, but one combines them.
I have a couple of favorites here.
this…common function call syntax…composable. It frankly looks
like HTML.
trouble: no portable definition of a "turn [bold, for example] off"
escape code, so state
have to embed it in the string or in a parallel data structure
-> enormous code complexity & mental load for users
went with attrs
...we save a great deal of complexity—both internally and for the
caller
Consistent: use with Python’s built-in templating language: less to
29. from blessings import Terminal
t = Terminal()
print t.red_bold + 'Hello world' + t.normal
print t.red_on_white + 'Hello world' + t.normal
print t.underline_red_on_green + 'Hello world' + t.normal
But, if you’re going to do something a little weird, at least be self-
consistent. This mashed-together syntax is used for everything in
Blessings: formatting, foreground color, background color. It’s hard to get
wrong; you can put the formatters in any order, and it just works.
Thus, the user doesn’t have to keep making trips back to the docs. They
internalize one slightly odd idea, and that carries them through all their use
cases.
30. Consistency warning signs
So when you’re evaluating your consistency, look out for these red flags:
Frequent refs to your own docs or source code as you’re building it.
(“Hmm, what did I call that?” “What order does it go in?”)
! Example: "index" kwarg vs. "indexes" in pyelasticsearch
Feeling like you’re inventing novel syntax. Use judgment: designs paying
off rather than showing off.
31. Consistency warning signs
Frequent references to your docs or source
So when you’re evaluating your consistency, look out for these red flags:
Frequent refs to your own docs or source code as you’re building it.
(“Hmm, what did I call that?” “What order does it go in?”)
! Example: "index" kwarg vs. "indexes" in pyelasticsearch
Feeling like you’re inventing novel syntax. Use judgment: designs paying
off rather than showing off.
32. Consistency warning signs
Frequent references to your docs or source
Feeling syntactically clever
So when you’re evaluating your consistency, look out for these red flags:
Frequent refs to your own docs or source code as you’re building it.
(“Hmm, what did I call that?” “What order does it go in?”)
! Example: "index" kwarg vs. "indexes" in pyelasticsearch
Feeling like you’re inventing novel syntax. Use judgment: designs paying
off rather than showing off.
33. Brevity
“The finest language is mostly made up
of simple, unimposing words.”
—George Eliot
3rd rule of thumb is Brevity.
Make common things…
short (in tokens, sometimes even in terms of chars if it doesn’t hurt
understanding)
34. from blessings import Terminal
term = Terminal()
print term.bold + 'I am bold!' + term.normal
excerpt from earlier
now you’ll see why (mashed-together)
Since formatting something and then resetting to plain text is the most
common operation, we make it the shortest.
Not everything has to be brief.
Another way to think about it: pay for what you eat.
35. from blessings import Terminal
term = Terminal()
print term.bold + 'I am bold!' + term.normal
print term.bold('I am bold!')
excerpt from earlier
now you’ll see why (mashed-together)
Since formatting something and then resetting to plain text is the most
common operation, we make it the shortest.
Not everything has to be brief.
Another way to think about it: pay for what you eat.
36. from blessings import Terminal
term = Terminal()
print term.bold + 'I am bold!' + term.normal
print term.bold('I am bold!')
print '{t.bold}Very {t.red}emphasized{t.normal}'.format(t=term)
excerpt from earlier
now you’ll see why (mashed-together)
Since formatting something and then resetting to plain text is the most
common operation, we make it the shortest.
Not everything has to be brief.
Another way to think about it: pay for what you eat.
38. Brevity warning signs
Copying and pasting when writing against your API
39. Brevity warning signs
Copying and pasting when writing against your API
Typing something irrelevant while grumbling
“Why can’t it just assume the obvious thing?”
40. Composability
“Perfection is achieved not when there is nothing left to add
but when there is nothing left to take away.”
—Antoine de Saint-Exupery
Making your abstractions composable means being able to reuse
them in many different situations and plug them together in
different ways.
Million ways to say this: flexibility, loose coupling, small pieces
loosely joined.
It all comes down to the minimization of assumptions.
There are two ways you can do this, one of them wrong.
41. Let’s go back to another way we could have done Blessings.
Jeff Quast put together a really neat BBS called x84. It works over
telnet using Blessings. Multiprocess design. Most of the action,
including formatting, happens in parent process, and children
handle connections.
With the above, there’s no way to pass the output to the children,
because we assumed (there’s that coupling) we’d be formatting and
printing all in one go.
The wrong way to solve this is to add an option. Print to some file
handle. But then you have to set up a file handle, even if you just
want to keep a string in memory. Plus it makes the API more
complicated: you have to document the option, and there’s a branch
in the code for it. Then you have to test it.
Every time I think about adding an option, I look around for any
other escape route first.
Better way: break that coupling.
42. print_formatted('Hello world', 'red', 'bold')
Let’s go back to another way we could have done Blessings.
Jeff Quast put together a really neat BBS called x84. It works over
telnet using Blessings. Multiprocess design. Most of the action,
including formatting, happens in parent process, and children
handle connections.
With the above, there’s no way to pass the output to the children,
because we assumed (there’s that coupling) we’d be formatting and
printing all in one go.
The wrong way to solve this is to add an option. Print to some file
handle. But then you have to set up a file handle, even if you just
want to keep a string in memory. Plus it makes the API more
complicated: you have to document the option, and there’s a branch
in the code for it. Then you have to test it.
Every time I think about adding an option, I look around for any
other escape route first.
Better way: break that coupling.
43. print_formatted('Hello world', 'red', 'bold')
print_formatted('Hello world', 'red', 'bold', out=some_file)
Let’s go back to another way we could have done Blessings.
Jeff Quast put together a really neat BBS called x84. It works over
telnet using Blessings. Multiprocess design. Most of the action,
including formatting, happens in parent process, and children
handle connections.
With the above, there’s no way to pass the output to the children,
because we assumed (there’s that coupling) we’d be formatting and
printing all in one go.
The wrong way to solve this is to add an option. Print to some file
handle. But then you have to set up a file handle, even if you just
want to keep a string in memory. Plus it makes the API more
complicated: you have to document the option, and there’s a branch
in the code for it. Then you have to test it.
Every time I think about adding an option, I look around for any
other escape route first.
Better way: break that coupling.
44. print_formatted('Hello world', 'red', 'bold')
print_formatted('Hello world', 'red', 'bold', out=some_file)
print formatted('Hello world', 'red', 'bold')
Let’s go back to another way we could have done Blessings.
Jeff Quast put together a really neat BBS called x84. It works over
telnet using Blessings. Multiprocess design. Most of the action,
including formatting, happens in parent process, and children
handle connections.
With the above, there’s no way to pass the output to the children,
because we assumed (there’s that coupling) we’d be formatting and
printing all in one go.
The wrong way to solve this is to add an option. Print to some file
handle. But then you have to set up a file handle, even if you just
want to keep a string in memory. Plus it makes the API more
complicated: you have to document the option, and there’s a branch
in the code for it. Then you have to test it.
Every time I think about adding an option, I look around for any
other escape route first.
Better way: break that coupling.
45. Composabilty
warning signs
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
46. Composabilty
warning signs
Classes with lots of state
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
47. Composabilty
warning signs
Classes with lots of state
class ElasticSearch(object):
"""
An object which manages connections to elasticsearch and acts as a
go-between for API calls to it"""
def index(self, index, doc_type, doc, id=None, force_insert=False,
query_params=None):
"""Put a typed JSON document into a specific index to make it
searchable."""
def search(self, query, **kwargs):
"""Execute a search query against one or more indices and get back search
hits."""
def more_like_this(self, index, doc_type, id, mlt_fields, body='', query_params=None):
"""Execute a "more like this" search query against one or more fields and
get back search hits."""
. . .
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
48. Composabilty
warning signs
Classes with lots of state
class ElasticSearch(object):
"""
class PenaltyBox(object):
An object which manages connections to elasticsearch and acts as a
"""A thread-safe bucket of servers (or other things) that may have
go-between for API calls to it"""
downtime."""
def index(self, index, doc_type, doc, id=None, force_insert=False,
query_params=None): def get(self):
"""Put a typed JSON document into a specific index server and a bool indicating whether it was from the
"""Return a random to make it
searchable.""" dead list."""
def search(self, query, **kwargs): mark_dead(self, server):
def
"""Execute a search query against one or more indices and get won't search
"""Guarantee that this server back be returned again until a period of
hits.""" time has passed, unless all servers are dead."""
def more_like_this(self, index, def mark_live(self, server):
doc_type, id, mlt_fields, body='', query_params=None):
"""Execute a "more like this" search query against one or dead list to and live one."""
"""Move a server from the more fields the
get back search hits."""
. . .
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
49. Composabilty
warning signs
Classes with lots of state
Deep inheritance hierarchies
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
50. Composabilty
warning signs
Classes with lots of state
Deep inheritance hierarchies
Violations of the Law of Demeter
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
51. Composabilty
warning signs
Classes with lots of state
Deep inheritance hierarchies
Violations of the Law of Demeter
Mocking in tests
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
52. Composabilty
warning signs
Classes with lots of state
Deep inheritance hierarchies
Violations of the Law of Demeter
Mocking in tests
print_formatted('Hello world', 'red', 'bold')
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
53. Composabilty
warning signs
Classes with lots of state
Deep inheritance hierarchies
Violations of the Law of Demeter
Mocking in tests
print_formatted('Hello world', 'red', 'bold')
print formatted('Hello world', 'red', 'bold')
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
54. Composabilty
warning signs
Classes with lots of state
Deep inheritance hierarchies
Violations of the Law of Demeter
Mocking in tests
Options
print_formatted('Hello world', 'red', 'bold')
print formatted('Hello world', 'red', 'bold')
Watch out for these red flags:
1. Classes with lots of state make me think there are multiple little classes
hiding inside, each serving a single purpose. For example,
pyelasticsearch connection object…needed penalty box for downed
nodes…tempting to add directly…better to separate…now can reuse in
other projects. Doesn’t even know about connections—just strings—so
represent whatever you want.
2. Deep inheritance hierarchies. People say “composition is better than
inheritance”, and here’s why. If you inherit, your object gets easy access to
the functionality of the parent—true. But it also inherits all the invariant
baggage of its direct parent and all the parents up the line. It’s required to
maintain those invariants, but it doesn’t get any help from the language,
which say it’s perfectly welcome to muck with private vars however it likes.
Subclassing just increases the amount of code that has to tiptoe around a
collection of state.
Instead it’s often better to instantiate the object you need inside yours and
stick it in an instance var.
55. Plain Data
“All the great things are simple, and many
can be expressed in a single word…”
—Sir Winston Churchill
Another rule of thumb that helps with reusability is plain data.
Whenever possible, your API should speak with its callers using
simple, built-in types, not big hairy data structures.
The aim is to reduce barriers to reuse. What’s a barrier look like?
56. It looks like the ConfigParser class out of the Python stdlib. Here’s an
excerpt of its API. Its purpose in life is to read INI files.
Unfortunately, it’s not just a parser that does its jobs and translates
the parsed content into something more usable, like a dictionary.
Instead, the config ends up represented as this unwieldy ConfigParser
object with lots of custom-named methods that don’t conform to any
Python idiom. This means that any code that uses ConfigParser ends
up written in its terms.
And, if you want the flexibility to pull in configuration from some
source that isn’t an INI file, you’ll have to write your own abstraction
layer on top of ConfigParser.
I’m going to continue to pick on ConfigParser (and pretend it has a
slightly worse API for illustrative purposes). (Actually takes multiple
filenames and does so for a fairly good reason.)
But let’s pretend it takes a single config file name. This might feel like
a convenience, since config often comes from files, but it’s really a
barrier.
57. ✚
It looks like the ConfigParser class out of the Python stdlib. Here’s an
excerpt of its API. Its purpose in life is to read INI files.
Unfortunately, it’s not just a parser that does its jobs and translates
the parsed content into something more usable, like a dictionary.
Instead, the config ends up represented as this unwieldy ConfigParser
object with lots of custom-named methods that don’t conform to any
Python idiom. This means that any code that uses ConfigParser ends
up written in its terms.
And, if you want the flexibility to pull in configuration from some
source that isn’t an INI file, you’ll have to write your own abstraction
layer on top of ConfigParser.
I’m going to continue to pick on ConfigParser (and pretend it has a
slightly worse API for illustrative purposes). (Actually takes multiple
filenames and does so for a fairly good reason.)
But let’s pretend it takes a single config file name. This might feel like
a convenience, since config often comes from files, but it’s really a
barrier.
58. ✚
It looks like the ConfigParser class out of the Python stdlib. Here’s an
excerpt of its API. Its purpose in life is to read INI files.
Unfortunately, it’s not just a parser that does its jobs and translates
the parsed content into something more usable, like a dictionary.
Instead, the config ends up represented as this unwieldy ConfigParser
object with lots of custom-named methods that don’t conform to any
Python idiom. This means that any code that uses ConfigParser ends
up written in its terms.
And, if you want the flexibility to pull in configuration from some
source that isn’t an INI file, you’ll have to write your own abstraction
layer on top of ConfigParser.
I’m going to continue to pick on ConfigParser (and pretend it has a
slightly worse API for illustrative purposes). (Actually takes multiple
filenames and does so for a fairly good reason.)
But let’s pretend it takes a single config file name. This might feel like
a convenience, since config often comes from files, but it’s really a
barrier.
59. ✚ ✚
It looks like the ConfigParser class out of the Python stdlib. Here’s an
excerpt of its API. Its purpose in life is to read INI files.
Unfortunately, it’s not just a parser that does its jobs and translates
the parsed content into something more usable, like a dictionary.
Instead, the config ends up represented as this unwieldy ConfigParser
object with lots of custom-named methods that don’t conform to any
Python idiom. This means that any code that uses ConfigParser ends
up written in its terms.
And, if you want the flexibility to pull in configuration from some
source that isn’t an INI file, you’ll have to write your own abstraction
layer on top of ConfigParser.
I’m going to continue to pick on ConfigParser (and pretend it has a
slightly worse API for illustrative purposes). (Actually takes multiple
filenames and does so for a fairly good reason.)
But let’s pretend it takes a single config file name. This might feel like
a convenience, since config often comes from files, but it’s really a
barrier.
60. ✚ ✚
RawConfigParser.parse(string)
It looks like the ConfigParser class out of the Python stdlib. Here’s an
excerpt of its API. Its purpose in life is to read INI files.
Unfortunately, it’s not just a parser that does its jobs and translates
the parsed content into something more usable, like a dictionary.
Instead, the config ends up represented as this unwieldy ConfigParser
object with lots of custom-named methods that don’t conform to any
Python idiom. This means that any code that uses ConfigParser ends
up written in its terms.
And, if you want the flexibility to pull in configuration from some
source that isn’t an INI file, you’ll have to write your own abstraction
layer on top of ConfigParser.
I’m going to continue to pick on ConfigParser (and pretend it has a
slightly worse API for illustrative purposes). (Actually takes multiple
filenames and does so for a fairly good reason.)
But let’s pretend it takes a single config file name. This might feel like
a convenience, since config often comes from files, but it’s really a
barrier.
61. ✚ ✚
RawConfigParser.parse(string)
charming_parser.parse(file_contents('some_file.ini'))
It looks like the ConfigParser class out of the Python stdlib. Here’s an
excerpt of its API. Its purpose in life is to read INI files.
Unfortunately, it’s not just a parser that does its jobs and translates
the parsed content into something more usable, like a dictionary.
Instead, the config ends up represented as this unwieldy ConfigParser
object with lots of custom-named methods that don’t conform to any
Python idiom. This means that any code that uses ConfigParser ends
up written in its terms.
And, if you want the flexibility to pull in configuration from some
source that isn’t an INI file, you’ll have to write your own abstraction
layer on top of ConfigParser.
I’m going to continue to pick on ConfigParser (and pretend it has a
slightly worse API for illustrative purposes). (Actually takes multiple
filenames and does so for a fairly good reason.)
But let’s pretend it takes a single config file name. This might feel like
a convenience, since config often comes from files, but it’s really a
barrier.
62. class Node(dict):
"""A wrapper around a native Reflect.parse dict
providing some convenience methods and some caching
of expensive computations"""
Even when you have an inherently more complicated data structure,
expose as much of it as possible as built-in interfaces.
Spiderflunky…doubly-linked tree…transform to iterator: walk_up
and walk_down.
Sexy list comps
Notice what this has done: it’s forced the caller to implement their
stuff in terms of your interface. Now they’re stuck with your library
forever, unless they want to rewrite their code. Why not meet on
common turf, like this? (Just return a map, or at least something that
acts like one.)
This is about the language that your API speaks with its callers. You
have a couple of choices here:
You can speak their language, or you can force them to speak yours.
63. class Node(dict):
"""A wrapper around a native Reflect.parse dict
providing some convenience methods and some caching
of expensive computations"""
def walk_up(self):
"""Yield each node from here to the root of the
tree, starting with myself."""
node = self
while node:
yield node
node = node.get('_parent')
Even when you have an inherently more complicated data structure,
expose as much of it as possible as built-in interfaces.
Spiderflunky…doubly-linked tree…transform to iterator: walk_up
and walk_down.
Sexy list comps
Notice what this has done: it’s forced the caller to implement their
stuff in terms of your interface. Now they’re stuck with your library
forever, unless they want to rewrite their code. Why not meet on
common turf, like this? (Just return a map, or at least something that
acts like one.)
This is about the language that your API speaks with its callers. You
have a couple of choices here:
You can speak their language, or you can force them to speak yours.
64. class Node(dict):
"""A wrapper around a native Reflect.parse dict
providing some convenience methods and some caching
of expensive computations"""
def walk_up(self):
"""Yield each node from here to the root of the
tree, starting with myself."""
node = self
while node:
yield node
node = node.get('_parent')
def nearest_scope_holder(self):
"""Return the nearest node that can have its own
scope, potentially including myself.
This will be either a FunctionDeclaration or a
Program (for now).
"""
return first(n for n in self.walk_up() if
n['type'] in ['FunctionDeclaration',
'Program'])
Even when you have an inherently more complicated data structure,
expose as much of it as possible as built-in interfaces.
Spiderflunky…doubly-linked tree…transform to iterator: walk_up
and walk_down.
Sexy list comps
Notice what this has done: it’s forced the caller to implement their
stuff in terms of your interface. Now they’re stuck with your library
forever, unless they want to rewrite their code. Why not meet on
common turf, like this? (Just return a map, or at least something that
acts like one.)
This is about the language that your API speaks with its callers. You
have a couple of choices here:
You can speak their language, or you can force them to speak yours.
65. Plain Data warning signs
(exception: inversion of control. Data, not control.)
66. Plain Data warning signs
Users immediately transforming your output to another format
(exception: inversion of control. Data, not control.)
67. Plain Data warning signs
Users immediately transforming your output to another format
Instantiating one object just to pass it to another
(exception: inversion of control. Data, not control.)
68. Grooviness
“The bad teacher’s words fall on his pupils like harsh rain;
the good teacher’s, as gently as dew.”
—Talmud: Ta’anith 7b
I have this idea that users of my APIs should spend most of their
time “in the groove.” And I don’t just mean performing well or
doing their work easily. I mean it in a more physical sense.
Think of programming as walking across a plane. The groove is a
nice, smooth path where it’s easy to walk. You could even walk it
with your eyes closed, because the sloping sides would guide you
gently back to the center. Note that there’s nothing keeping you
from stepping out of the groove and walking somewhere else if you
like; it’s not a wall you have to vault over—we’ll get to those in a
minute. But the groove is very attractive, and new users are drawn
there first. Grooves are about guidance, not limitations.
Here are some ideas on how to cut grooves in your APIs.
69. Grooviness
Groove Wall
I have this idea that users of my APIs should spend most of their
time “in the groove.” And I don’t just mean performing well or
doing their work easily. I mean it in a more physical sense.
Think of programming as walking across a plane. The groove is a
nice, smooth path where it’s easy to walk. You could even walk it
with your eyes closed, because the sloping sides would guide you
gently back to the center. Note that there’s nothing keeping you
from stepping out of the groove and walking somewhere else if you
like; it’s not a wall you have to vault over—we’ll get to those in a
minute. But the groove is very attractive, and new users are drawn
there first. Grooves are about guidance, not limitations.
Here are some ideas on how to cut grooves in your APIs.
70. Avoid nonsense representations.
Avoid nonsense representations
I maintain a Python API for elasticsearch called pyelasticsearch.
There are 2 ways to query it: JSON and simpler string syntax.
There’s no such thing as a query that uses both formats at once;
you have to choose one.
71. Avoid nonsense representations.
{
"bool" : {
"must" : {
"term" : { "user" : "fred" }
},
"must_not" : {
"range" : {
"age" : { "from" : 12, "to" : 21 }
}
},
"minimum_number_should_match" : 1,
"boost" : 1.0
}
}
Avoid nonsense representations
I maintain a Python API for elasticsearch called pyelasticsearch.
There are 2 ways to query it: JSON and simpler string syntax.
There’s no such thing as a query that uses both formats at once;
you have to choose one.
72. Avoid nonsense representations.
{
"bool" : {
"must" : {
"term" : { "user" : "fred" }
},
"must_not" : {
"range" : {
"age" : { "from" : 12, "to" : 21 }
}
},
"minimum_number_should_match" : 1,
"boost" : 1.0
}
}
frob*ator AND snork
Avoid nonsense representations
I maintain a Python API for elasticsearch called pyelasticsearch.
There are 2 ways to query it: JSON and simpler string syntax.
There’s no such thing as a query that uses both formats at once;
you have to choose one.
73. Old pyelasticsearch
def search(self, q=None, body=None, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
Here’s what pyelasticsearch used to look like.
All its args were optional. You’d pass string-style queries in using
the q argument and JSON-style queries using the body one.
Of course, this meant nothing stopped you from doing this or even
this. But those are nonsense. You shouldn’t even be able to get that
far, not to mention the code that has to exist in your library to check
for and reject such silliness.
New pyelasticsearch replaces that mess with a single required
argument to which you can pass either a string or a JSON dictionary.
You can’t go wrong, because the interpreter won’t let you. Much
groovier!
Incidentally, this turns up another principle: fail shallowly. In
languages that give you tracebacks, this helps. If, heaven forbid,
you’re using a language that doesn’t, this is vital.
74. Old pyelasticsearch
def search(self, q=None, body=None, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
search(q='frob*ator AND snork', body={'some': 'query'})
Here’s what pyelasticsearch used to look like.
All its args were optional. You’d pass string-style queries in using
the q argument and JSON-style queries using the body one.
Of course, this meant nothing stopped you from doing this or even
this. But those are nonsense. You shouldn’t even be able to get that
far, not to mention the code that has to exist in your library to check
for and reject such silliness.
New pyelasticsearch replaces that mess with a single required
argument to which you can pass either a string or a JSON dictionary.
You can’t go wrong, because the interpreter won’t let you. Much
groovier!
Incidentally, this turns up another principle: fail shallowly. In
languages that give you tracebacks, this helps. If, heaven forbid,
you’re using a language that doesn’t, this is vital.
75. Old pyelasticsearch
def search(self, q=None, body=None, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
search(q='frob*ator AND snork', body={'some': 'query'})
search()
Here’s what pyelasticsearch used to look like.
All its args were optional. You’d pass string-style queries in using
the q argument and JSON-style queries using the body one.
Of course, this meant nothing stopped you from doing this or even
this. But those are nonsense. You shouldn’t even be able to get that
far, not to mention the code that has to exist in your library to check
for and reject such silliness.
New pyelasticsearch replaces that mess with a single required
argument to which you can pass either a string or a JSON dictionary.
You can’t go wrong, because the interpreter won’t let you. Much
groovier!
Incidentally, this turns up another principle: fail shallowly. In
languages that give you tracebacks, this helps. If, heaven forbid,
you’re using a language that doesn’t, this is vital.
76. Old pyelasticsearch
def search(self, q=None, body=None, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
search(q='frob*ator AND snork', body={'some': 'query'})
search()
✚
Here’s what pyelasticsearch used to look like.
All its args were optional. You’d pass string-style queries in using
the q argument and JSON-style queries using the body one.
Of course, this meant nothing stopped you from doing this or even
this. But those are nonsense. You shouldn’t even be able to get that
far, not to mention the code that has to exist in your library to check
for and reject such silliness.
New pyelasticsearch replaces that mess with a single required
argument to which you can pass either a string or a JSON dictionary.
You can’t go wrong, because the interpreter won’t let you. Much
groovier!
Incidentally, this turns up another principle: fail shallowly. In
languages that give you tracebacks, this helps. If, heaven forbid,
you’re using a language that doesn’t, this is vital.
77. Old pyelasticsearch
def search(self, q=None, body=None, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
search(q='frob*ator AND snork', body={'some': 'query'})
search()
✚
New pyelasticsearch
def search(self, query, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
Here’s what pyelasticsearch used to look like.
All its args were optional. You’d pass string-style queries in using
the q argument and JSON-style queries using the body one.
Of course, this meant nothing stopped you from doing this or even
this. But those are nonsense. You shouldn’t even be able to get that
far, not to mention the code that has to exist in your library to check
for and reject such silliness.
New pyelasticsearch replaces that mess with a single required
argument to which you can pass either a string or a JSON dictionary.
You can’t go wrong, because the interpreter won’t let you. Much
groovier!
Incidentally, this turns up another principle: fail shallowly. In
languages that give you tracebacks, this helps. If, heaven forbid,
you’re using a language that doesn’t, this is vital.
78. Old pyelasticsearch
def search(self, q=None, body=None, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
search(q='frob*ator AND snork', body={'some': 'query'})
search()
✚
New pyelasticsearch
def search(self, query, indexes=None, doc_types=None):
"""Execute an elasticsearch query, and return the results."""
Fail shallowly.
Here’s what pyelasticsearch used to look like.
All its args were optional. You’d pass string-style queries in using
the q argument and JSON-style queries using the body one.
Of course, this meant nothing stopped you from doing this or even
this. But those are nonsense. You shouldn’t even be able to get that
far, not to mention the code that has to exist in your library to check
for and reject such silliness.
New pyelasticsearch replaces that mess with a single required
argument to which you can pass either a string or a JSON dictionary.
You can’t go wrong, because the interpreter won’t let you. Much
groovier!
Incidentally, this turns up another principle: fail shallowly. In
languages that give you tracebacks, this helps. If, heaven forbid,
you’re using a language that doesn’t, this is vital.
79. Resource acquisition is initialization.
Another way to provide grooves is RAII.
Here’s a poppable balloon class. But, as you can see, when you first
construct one, it isn’t poppable. You can pop a balloon that has no
air in it. You might imagine the documentation says “Before popping
the balloon, fill it with the desired amount of air.” That’s broken
design. A poppable balloon should be poppable, full-stop. The user
shouldn’t have to head into the docs to figure out what’s wrong.
A better way is to take the initial fill amount at construction. The
user is required to provide it and gets a prompt error if he doesn’t,
so there’s no wondering later why the balloon didn’t make any noise
when you prick it.
RAII is a more specific case of “Don’t have invariants that aren’t.”
80. Resource acquisition is initialization.
class PoppableBalloon(object):
"""A balloon you can pop"""
def __init__(self):
self.air = 0
def fill(self, how_much):
self.air = how_much
Another way to provide grooves is RAII.
Here’s a poppable balloon class. But, as you can see, when you first
construct one, it isn’t poppable. You can pop a balloon that has no
air in it. You might imagine the documentation says “Before popping
the balloon, fill it with the desired amount of air.” That’s broken
design. A poppable balloon should be poppable, full-stop. The user
shouldn’t have to head into the docs to figure out what’s wrong.
A better way is to take the initial fill amount at construction. The
user is required to provide it and gets a prompt error if he doesn’t,
so there’s no wondering later why the balloon didn’t make any noise
when you prick it.
RAII is a more specific case of “Don’t have invariants that aren’t.”
81. Resource acquisition is initialization.
class PoppableBalloon(object):
"""A balloon you can pop"""
def __init__(self):
self.air = 0
def fill(self, how_much):
self.air = how_much
class PoppableBalloon(object):
"""A balloon you can pop"""
def __init__(self, initial_fill):
self.air = initial_fill
def fill(self, how_much):
self.air = how_much
Another way to provide grooves is RAII.
Here’s a poppable balloon class. But, as you can see, when you first
construct one, it isn’t poppable. You can pop a balloon that has no
air in it. You might imagine the documentation says “Before popping
the balloon, fill it with the desired amount of air.” That’s broken
design. A poppable balloon should be poppable, full-stop. The user
shouldn’t have to head into the docs to figure out what’s wrong.
A better way is to take the initial fill amount at construction. The
user is required to provide it and gets a prompt error if he doesn’t,
so there’s no wondering later why the balloon didn’t make any noise
when you prick it.
RAII is a more specific case of “Don’t have invariants that aren’t.”
82. Compelling examples
A final way of providing grooves is through compelling examples.
These are the most overt sort of grooves. The user knows he’s being
advised.
This screenshot from MacPaint 1.0, with its palettes on the left and
menus on the top should look familiar to everybody in this room—
even if they never used it—because Photoshop and Illustrator and
the GIMP and Pixelmator and Paintshop Pro have been following its
example for the past 30 years.
[Another example: Nintendo platformers. Picture is in the margin.]
Set a good example, and people will follow it forever. I used to write
example code in a very pedagogical fashion, with outrageously long
variable names and inefficiency everywhere. I ended up having to
stop that, because users of my libs were copying and pasting my
code for use in production! Oftentimes, they would even leave the
comments!
So don't underestimate the docility of your users; they will do what
you suggest. This isn't because they're stupid. It's just that they're
83. Compelling examples
A final way of providing grooves is through compelling examples.
These are the most overt sort of grooves. The user knows he’s being
advised.
This screenshot from MacPaint 1.0, with its palettes on the left and
menus on the top should look familiar to everybody in this room—
even if they never used it—because Photoshop and Illustrator and
the GIMP and Pixelmator and Paintshop Pro have been following its
example for the past 30 years.
[Another example: Nintendo platformers. Picture is in the margin.]
Set a good example, and people will follow it forever. I used to write
example code in a very pedagogical fashion, with outrageously long
variable names and inefficiency everywhere. I ended up having to
stop that, because users of my libs were copying and pasting my
code for use in production! Oftentimes, they would even leave the
comments!
So don't underestimate the docility of your users; they will do what
you suggest. This isn't because they're stupid. It's just that they're
85. Grooviness
warning signs
Nonsense states or representations
86. Grooviness
warning signs
Nonsense states or representations
Invariants that aren’t
87. Grooviness
warning signs
Nonsense states or representations
Invariants that aren’t
Users unclear where to get started
88. Safety
“I wish it was easier to hurt myself.”
—Nobody, ever
Now we get to the walls.
Walls are to keep you from hurting yourself or others. Just as in the
physical metaphor, there’s a continuum between grooves and walls.
The more damage you can do with a feature, the higher the wall
should be in front of it.
89. Safety
Groove Wall
Now we get to the walls.
Walls are to keep you from hurting yourself or others. Just as in the
physical metaphor, there’s a continuum between grooves and walls.
The more damage you can do with a feature, the higher the wall
should be in front of it.
90. update things set frob=2 where frob=1;
Here is a horrible API with not enough walls. Does anyone see what’s
wrong with it? Maybe it’s bitten you in the past.
Who here has wiped out a whole DB column because they forgot the
WHERE clause?
One way to fix this might be to require you to say all if you mean
all. Might be less mathematically beautiful, but imagine the hours it
would have saved since the advent of SQL.
Here’s another old chestnut. This is a pretty common thing to do.
The trouble is—to do it, you first have to type this, which is
seriously bad news. Don’t hit Return by accident!
Maybe we could require -f for bare asterisks.
91. update things set frob=2 where frob=1;
update things set frob=2;
Here is a horrible API with not enough walls. Does anyone see what’s
wrong with it? Maybe it’s bitten you in the past.
Who here has wiped out a whole DB column because they forgot the
WHERE clause?
One way to fix this might be to require you to say all if you mean
all. Might be less mathematically beautiful, but imagine the hours it
would have saved since the advent of SQL.
Here’s another old chestnut. This is a pretty common thing to do.
The trouble is—to do it, you first have to type this, which is
seriously bad news. Don’t hit Return by accident!
Maybe we could require -f for bare asterisks.
92. update things set frob=2 where frob=1;
update things set frob=2;
update things set frob=2 all;
Here is a horrible API with not enough walls. Does anyone see what’s
wrong with it? Maybe it’s bitten you in the past.
Who here has wiped out a whole DB column because they forgot the
WHERE clause?
One way to fix this might be to require you to say all if you mean
all. Might be less mathematically beautiful, but imagine the hours it
would have saved since the advent of SQL.
Here’s another old chestnut. This is a pretty common thing to do.
The trouble is—to do it, you first have to type this, which is
seriously bad news. Don’t hit Return by accident!
Maybe we could require -f for bare asterisks.
93. update things set frob=2 where frob=1;
update things set frob=2;
update things set frob=2 all;
rm *.pyc
Here is a horrible API with not enough walls. Does anyone see what’s
wrong with it? Maybe it’s bitten you in the past.
Who here has wiped out a whole DB column because they forgot the
WHERE clause?
One way to fix this might be to require you to say all if you mean
all. Might be less mathematically beautiful, but imagine the hours it
would have saved since the advent of SQL.
Here’s another old chestnut. This is a pretty common thing to do.
The trouble is—to do it, you first have to type this, which is
seriously bad news. Don’t hit Return by accident!
Maybe we could require -f for bare asterisks.
94. update things set frob=2 where frob=1;
update things set frob=2;
update things set frob=2 all;
rm *.pyc
rm *
Here is a horrible API with not enough walls. Does anyone see what’s
wrong with it? Maybe it’s bitten you in the past.
Who here has wiped out a whole DB column because they forgot the
WHERE clause?
One way to fix this might be to require you to say all if you mean
all. Might be less mathematically beautiful, but imagine the hours it
would have saved since the advent of SQL.
Here’s another old chestnut. This is a pretty common thing to do.
The trouble is—to do it, you first have to type this, which is
seriously bad news. Don’t hit Return by accident!
Maybe we could require -f for bare asterisks.
95. update things set frob=2 where frob=1;
update things set frob=2;
update things set frob=2 all;
rm *.pyc
rm *
rm -f *
Here is a horrible API with not enough walls. Does anyone see what’s
wrong with it? Maybe it’s bitten you in the past.
Who here has wiped out a whole DB column because they forgot the
WHERE clause?
One way to fix this might be to require you to say all if you mean
all. Might be less mathematically beautiful, but imagine the hours it
would have saved since the advent of SQL.
Here’s another old chestnut. This is a pretty common thing to do.
The trouble is—to do it, you first have to type this, which is
seriously bad news. Don’t hit Return by accident!
Maybe we could require -f for bare asterisks.
96. def delete(self, index, doc_type, id=None):
"""Delete a typed JSON document from a specific
index based on its id."""
A very similar thing happened in pyelasticsearch. This is what the
delete method in pyelasticsearch used to look like. It deletes a
document based on its ID. (A document is like a row in a RDB.)
You might not be able to pick out what’s wrong unless you know ES,
but if you make a DELETE API call to it without an ID, it’ll happily
delete all documents. And id here is OPTIONAL! I can imagine
myself coding along and managing to close your parentheses early.
Or I could leave out one of the previous arguments so the value I
intended as the ID gets interpreted as one of the other arguments
instead.
So we fixed that. Here’s how it looks now.
We made ID mandatory and, to support the rare delete-all-
documents case, we added a second routine explicitly called
delete_all.
97. def delete(self, index, doc_type, id=None):
"""Delete a typed JSON document from a specific
index based on its id."""
A very similar thing happened in pyelasticsearch. This is what the
delete method in pyelasticsearch used to look like. It deletes a
document based on its ID. (A document is like a row in a RDB.)
You might not be able to pick out what’s wrong unless you know ES,
but if you make a DELETE API call to it without an ID, it’ll happily
delete all documents. And id here is OPTIONAL! I can imagine
myself coding along and managing to close your parentheses early.
Or I could leave out one of the previous arguments so the value I
intended as the ID gets interpreted as one of the other arguments
instead.
So we fixed that. Here’s how it looks now.
We made ID mandatory and, to support the rare delete-all-
documents case, we added a second routine explicitly called
delete_all.
98. def delete(self, index, doc_type, id=None):
"""Delete a typed JSON document from a specific
index based on its id."""
def delete(self, index, doc_type, id):
"""Delete a typed JSON document from a specific
index based on its id."""
def delete_all(self, index, doc_type):
"""Delete all documents of the given doctype from
an index."""
A very similar thing happened in pyelasticsearch. This is what the
delete method in pyelasticsearch used to look like. It deletes a
document based on its ID. (A document is like a row in a RDB.)
You might not be able to pick out what’s wrong unless you know ES,
but if you make a DELETE API call to it without an ID, it’ll happily
delete all documents. And id here is OPTIONAL! I can imagine
myself coding along and managing to close your parentheses early.
Or I could leave out one of the previous arguments so the value I
intended as the ID gets interpreted as one of the other arguments
instead.
So we fixed that. Here’s how it looks now.
We made ID mandatory and, to support the rare delete-all-
documents case, we added a second routine explicitly called
delete_all.
99. Error reporting: exceptions are typically safer.
The other thing to consider is how to report errors. You have 2
choices in most languages: return something, or throw an
exception. Raising an exception is typically the safer of the two, as it
reduces the chance of accidentally swallowing an error.
100. Safetywarning signs
In general, Ask yourself, "How can I use this to hurt myself and
others?"
People will blame themselves.
101. Safetywarning signs
Surprisingly few—people will blame themselves.
In general, Ask yourself, "How can I use this to hurt myself and
others?"
People will blame themselves.
102. non-astronautics consistency
brevity composability
plain data safety grooviness
While you’re getting your questions together, I want to ask you one.
I feel like…
Elegance?
If you have any ideas, please do share.
103. non-astronautics consistency
Elegance?
brevity composability
“It is the ineffable will of Bob.”
—Old Thrashbarg
plain data safety grooviness
While you’re getting your questions together, I want to ask you one.
I feel like…
Elegance?
If you have any ideas, please do share.
104. Thank You
Merci
https://joind.in/7995
twitter: ErikRose
erik@mozilla.com