Fluid, Fluent APIs

1,481 views
1,352 views

Published on

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.

Published in: Technology
0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,481
On SlideShare
0
From Embeds
0
Number of Embeds
11
Actions
Shares
0
Downloads
19
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

Fluid, Fluent APIs

  1. 1. Fluent, Fluid APIs by Erik Rose “A poet is, before anything else, a person who is passionately in love with language.” —W. H. AudenWelcome 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 headinto another without surgery.So I want to tell you a story about a man who didn’t have this ability for27 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—andeven the more basic power of human words.
  2. 2. Poetic APIs by Erik Rose “A poet is, before anything else, a person who is passionately in love with language.” —W. H. AudenWelcome 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 headinto another without surgery.So I want to tell you a story about a man who didn’t have this ability for27 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—andeven the more basic power of human words.
  3. 3. Poetic APIs programmer by Erik Rose ^ “A poet is, before anything else, a person who is passionately in love with language.” —W. H. AudenWelcome 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 headinto another without surgery.So I want to tell you a story about a man who didn’t have this ability for27 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—andeven the more basic power of human words.
  4. 4. Ildefansodeafhearing parents, siblings, stupid?Susan Schallercoweringmimicimaginery studentpointing“Oh! Everything has a name!”
  5. 5. “Oh! Everything has a name!”Now, this was a 27-year-old man who had been around. He didn’tlive in the jungle. But, as Susan describes it, the look on his facewas as if he had never seen a window before. The window becamea different thing by having a symbol attached to it.
  6. 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. 7. Inventing Language “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” —Martin FowlerIn creating an API, your task is not only to make something that works—but to put your categories for the universe into somebody elses 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 tohave 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 timearen’t the abstractions he’s built his world on.Before he had lang, he could do simple math with quantites up to about10, 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 afteryoufail: frustrating all those who come after youExample: urllib2 vs. Requests
  8. 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 thecommon concept of an HTTP get—productivity multiplierThis is why API design makes greatest demands on fluency:Youre drawing out borders around concepts which have to fit a varietyof other people’s brains and be adaptable enough for situations youhaven’t forseen.I’m going to give you 7 rules of thumb to increase your chances.The first one is…
  9. 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_codeurllib2: 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 thecommon concept of an HTTP get—productivity multiplierThis is why API design makes greatest demands on fluency:Youre drawing out borders around concepts which have to fit a varietyof other people’s brains and be adaptable enough for situations youhaven’t forseen.I’m going to give you 7 rules of thumb to increase your chances.The first one is…
  10. 10. Don’t Be An Architecture AstronautMy first step when designing a new library is actually to not designa 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’renot continually tested and brought back to earth by reality.You end up with alchemy rather than chemistry.
  11. 11. Very impressive to look at and academically interesting but it’smerely a self-consistent system of nonsense,not anything of practical use.So, to bring our efforts back into the realm of science, rememberthat the best libraries are extracted, not invented.
  12. 12. The best libraries are extracted, not invented.What this means is that you should have an application that alreadydoes 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 interestedin and factor them up.
  13. 13. For example, I wrote a plugin nose-progressive for the popularPython test framework nose,and its purpose was to display a progress bar and just generallymake output easier to follow.It made use of a lot of terminal escape codes, for things likebolding, coloring, and positioning.The code looked something like this.
  14. 14. For example, I wrote a plugin nose-progressive for the popularPython test framework nose,and its purpose was to display a progress bar and just generallymake output easier to follow.It made use of a lot of terminal escape codes, for things likebolding, coloring, and positioning.The code looked something like this.
  15. 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 youre crazy enough to pipe this to a file or something, # dont 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 codethat actually kept track of tests.Raw terminal capability names like sgr0 and rc that I had to keeplooking up. [TODO: red underlines]Every time I’d have to do some formatting I’d think “Geez, I wishthere were a decent abstraction around this so I wouldn’t have tolook at it.” It’s at this point—when you have a real, useful programwith a library struggling to get out—that you can come down fromorbit and start thinking about library design.So it was time for blessings—my terminal formatting library—to beborn.
  16. 16. TasksThe first thing to do is to dump everything out on the workbench.What sort of things ought the library to do? bam bamNow, what tools have we got at our workbench? Well…all the stuff inour language…
  17. 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 bamNow, what tools have we got at our workbench? Well…all the stuff inour language…
  18. 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 bamNow, what tools have we got at our workbench? Well…all the stuff inour language…
  19. 19. Language Constructs Functions, arguments, keyword arguments Decorators Context managers Classes (really more of an implementation detail)bam bam bam bamcommon patterns…:bam bam bamThen we shake the workbench up and see if we can find some goodpairings between tools and tasks. What guides you? Consistency.
  20. 20. Language Constructs Functions, arguments, keyword arguments Decorators Context managers Classes (really more of an implementation detail) å Patterns, Protocols, Interfaces, and Conventions Sequences Iterators Mappingsbam bam bam bamcommon patterns…:bam bam bamThen we shake the workbench up and see if we can find some goodpairings between tools and tasks. What guides you? Consistency.
  21. 21. Consistency “Think like a wise man, but communicate in the language of the people.” —William Butler YeatsYou have the entire technical and cultural weight of the language itselfbehind you as well as accumulated habits of influential libs and theprogrammer community.When were designing web sites, we think of it this way: people spend 90%of their time on other peoples web sites, so…make the logo link to the home pageput the log-in link in the upper rightand so onWhen writing an API, realize:users will spend 90% time using other APIsYou can be weird and clever if you need to be, but if you can manage tostick with conventions, you’ll get these bonuses…1. conveys respect for your users. Youre not some outsider who is goingto make their code a mishmash of clashing styles.I cant tell you how many times Ive encountered a Java API masqueradingas Python one and thrown it away in disgust.getters and setters + violation of case conventions → Im going to move on.
  22. 22. Brilliant thing about early Mac project: explicit HIGsEvery program same commands: open, close, quit, copy, paste. In samemenus w/same keyboard shortcuts.Learn one program → learned em allIf you implement get(key, default) and call it that, people will pick it upfaster and remember it better than fetch(default, key).3. Polymorphism. Can use calling code on dicts or whatever your class is.
  23. 23. get(key, default) > fetch(default, key)Brilliant thing about early Mac project: explicit HIGsEvery program same commands: open, close, quit, copy, paste. In samemenus w/same keyboard shortcuts.Learn one program → learned em allIf you implement get(key, default) and call it that, people will pick it upfaster and remember it better than fetch(default, key).3. Polymorphism. Can use calling code on dicts or whatever your class is.
  24. 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 tasksagainst existing language features and conventions?Well, I like to get as many alternatives out on the workbench aspossible. 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 multiplestatements inside—even calls out to other functions.And then the first can be written in terms of it, if you like.
  25. 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 thingSo, coming back to our tasks at hand, how do we bang those tasksagainst existing language features and conventions?Well, I like to get as many alternatives out on the workbench aspossible. 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 multiplestatements inside—even calls out to other functions.And then the first can be written in terms of it, if you like.
  26. 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.normalHere are sketches for text formatting. We see some squarebrackets here, some dots, some string concatenation, somefunction calls with kwargs and with positional args. Most separatethe printing from the formating, but one combines them.I have a couple of favorites here.this…common function call syntax…composable. It frankly lookslike HTML.trouble: no portable definition of a "turn [bold, for example] off"escape code, so statehave to embed it in the string or in a parallel data structure-> enormous code complexity & mental load for userswent with attrs...we save a great deal of complexity—both internally and for thecallerConsistent: use with Python’s built-in templating language: less to
  27. 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.normalHere are sketches for text formatting. We see some squarebrackets here, some dots, some string concatenation, somefunction calls with kwargs and with positional args. Most separatethe printing from the formating, but one combines them.I have a couple of favorites here.this…common function call syntax…composable. It frankly lookslike HTML.trouble: no portable definition of a "turn [bold, for example] off"escape code, so statehave to embed it in the string or in a parallel data structure-> enormous code complexity & mental load for userswent with attrs...we save a great deal of complexity—both internally and for thecallerConsistent: use with Python’s built-in templating language: less to
  28. 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.normalHere are sketches for text formatting. We see some squarebrackets here, some dots, some string concatenation, somefunction calls with kwargs and with positional args. Most separatethe printing from the formating, but one combines them.I have a couple of favorites here.this…common function call syntax…composable. It frankly lookslike HTML.trouble: no portable definition of a "turn [bold, for example] off"escape code, so statehave to embed it in the string or in a parallel data structure-> enormous code complexity & mental load for userswent with attrs...we save a great deal of complexity—both internally and for thecallerConsistent: use with Python’s built-in templating language: less to
  29. 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.normalBut, if you’re going to do something a little weird, at least be self-consistent. This mashed-together syntax is used for everything inBlessings: formatting, foreground color, background color. It’s hard to getwrong; 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. Theyinternalize one slightly odd idea, and that carries them through all their usecases.
  30. 30. Consistency warning signsSo 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 pyelasticsearchFeeling like you’re inventing novel syntax. Use judgment: designs payingoff rather than showing off.
  31. 31. Consistency warning signs Frequent references to your docs or sourceSo 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 pyelasticsearchFeeling like you’re inventing novel syntax. Use judgment: designs payingoff rather than showing off.
  32. 32. Consistency warning signs Frequent references to your docs or source Feeling syntactically cleverSo 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 pyelasticsearchFeeling like you’re inventing novel syntax. Use judgment: designs payingoff rather than showing off.
  33. 33. Brevity “The finest language is mostly made up of simple, unimposing words.” —George Eliot3rd rule of thumb is Brevity.Make common things…short (in tokens, sometimes even in terms of chars if it doesn’t hurtunderstanding)
  34. 34. from blessings import Terminal term = Terminal() print term.bold + I am bold! + term.normalexcerpt from earliernow you’ll see why (mashed-together)Since formatting something and then resetting to plain text is the mostcommon operation, we make it the shortest.Not everything has to be brief.Another way to think about it: pay for what you eat.
  35. 35. from blessings import Terminal term = Terminal() print term.bold + I am bold! + term.normal print term.bold(I am bold!)excerpt from earliernow you’ll see why (mashed-together)Since formatting something and then resetting to plain text is the mostcommon operation, we make it the shortest.Not everything has to be brief.Another way to think about it: pay for what you eat.
  36. 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 earliernow you’ll see why (mashed-together)Since formatting something and then resetting to plain text is the mostcommon operation, we make it the shortest.Not everything has to be brief.Another way to think about it: pay for what you eat.
  37. 37. Brevity warning signs
  38. 38. Brevity warning signsCopying and pasting when writing against your API
  39. 39. Brevity warning signsCopying and pasting when writing against your API Typing something irrelevant while grumbling “Why can’t it just assume the obvious thing?”
  40. 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-ExuperyMaking your abstractions composable means being able to reusethem in many different situations and plug them together indifferent ways.Million ways to say this: flexibility, loose coupling, small piecesloosely joined.It all comes down to the minimization of assumptions.There are two ways you can do this, one of them wrong.
  41. 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 overtelnet using Blessings. Multiprocess design. Most of the action,including formatting, happens in parent process, and childrenhandle 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 andprinting all in one go.The wrong way to solve this is to add an option. Print to some filehandle. But then you have to set up a file handle, even if you justwant to keep a string in memory. Plus it makes the API morecomplicated: you have to document the option, and there’s a branchin the code for it. Then you have to test it.Every time I think about adding an option, I look around for anyother escape route first.Better way: break that coupling.
  42. 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 overtelnet using Blessings. Multiprocess design. Most of the action,including formatting, happens in parent process, and childrenhandle 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 andprinting all in one go.The wrong way to solve this is to add an option. Print to some filehandle. But then you have to set up a file handle, even if you justwant to keep a string in memory. Plus it makes the API morecomplicated: you have to document the option, and there’s a branchin the code for it. Then you have to test it.Every time I think about adding an option, I look around for anyother escape route first.Better way: break that coupling.
  43. 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 overtelnet using Blessings. Multiprocess design. Most of the action,including formatting, happens in parent process, and childrenhandle 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 andprinting all in one go.The wrong way to solve this is to add an option. Print to some filehandle. But then you have to set up a file handle, even if you justwant to keep a string in memory. Plus it makes the API morecomplicated: you have to document the option, and there’s a branchin the code for it. Then you have to test it.Every time I think about adding an option, I look around for anyother escape route first.Better way: break that coupling.
  44. 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 overtelnet using Blessings. Multiprocess design. Most of the action,including formatting, happens in parent process, and childrenhandle 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 andprinting all in one go.The wrong way to solve this is to add an option. Print to some filehandle. But then you have to set up a file handle, even if you justwant to keep a string in memory. Plus it makes the API morecomplicated: you have to document the option, and there’s a branchin the code for it. Then you have to test it.Every time I think about adding an option, I look around for anyother escape route first.Better way: break that coupling.
  45. 45. Composabilty warning signsWatch out for these red flags:1. Classes with lots of state make me think there are multiple little classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  46. 46. Composabilty warning signs Classes with lots of stateWatch out for these red flags:1. Classes with lots of state make me think there are multiple little classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  47. 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 classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  48. 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 wont 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 classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  49. 49. Composabilty warning signs Classes with lots of state Deep inheritance hierarchiesWatch out for these red flags:1. Classes with lots of state make me think there are multiple little classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  50. 50. Composabilty warning signs Classes with lots of state Deep inheritance hierarchies Violations of the Law of DemeterWatch out for these red flags:1. Classes with lots of state make me think there are multiple little classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  51. 51. Composabilty warning signs Classes with lots of state Deep inheritance hierarchies Violations of the Law of Demeter Mocking in testsWatch out for these red flags:1. Classes with lots of state make me think there are multiple little classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  52. 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 classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  53. 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 classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  54. 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 classeshiding inside, each serving a single purpose. For example,pyelasticsearch connection object…needed penalty box for downednodes…tempting to add directly…better to separate…now can reuse inother projects. Doesn’t even know about connections—just strings—sorepresent whatever you want.2. Deep inheritance hierarchies. People say “composition is better thaninheritance”, and here’s why. If you inherit, your object gets easy access tothe functionality of the parent—true. But it also inherits all the invariantbaggage of its direct parent and all the parents up the line. It’s required tomaintain 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 acollection of state.Instead it’s often better to instantiate the object you need inside yours andstick it in an instance var.
  55. 55. Plain Data “All the great things are simple, and many can be expressed in a single word…” —Sir Winston ChurchillAnother rule of thumb that helps with reusability is plain data.Whenever possible, your API should speak with its callers usingsimple, built-in types, not big hairy data structures.The aim is to reduce barriers to reuse. What’s a barrier look like?
  56. 56. It looks like the ConfigParser class out of the Python stdlib. Here’s anexcerpt of its API. Its purpose in life is to read INI files.Unfortunately, it’s not just a parser that does its jobs and translatesthe parsed content into something more usable, like a dictionary.Instead, the config ends up represented as this unwieldy ConfigParserobject with lots of custom-named methods that don’t conform to anyPython idiom. This means that any code that uses ConfigParser endsup written in its terms.And, if you want the flexibility to pull in configuration from somesource that isn’t an INI file, you’ll have to write your own abstractionlayer on top of ConfigParser.I’m going to continue to pick on ConfigParser (and pretend it has aslightly worse API for illustrative purposes). (Actually takes multiplefilenames and does so for a fairly good reason.)But let’s pretend it takes a single config file name. This might feel likea convenience, since config often comes from files, but it’s really abarrier.
  57. 57. ✚It looks like the ConfigParser class out of the Python stdlib. Here’s anexcerpt of its API. Its purpose in life is to read INI files.Unfortunately, it’s not just a parser that does its jobs and translatesthe parsed content into something more usable, like a dictionary.Instead, the config ends up represented as this unwieldy ConfigParserobject with lots of custom-named methods that don’t conform to anyPython idiom. This means that any code that uses ConfigParser endsup written in its terms.And, if you want the flexibility to pull in configuration from somesource that isn’t an INI file, you’ll have to write your own abstractionlayer on top of ConfigParser.I’m going to continue to pick on ConfigParser (and pretend it has aslightly worse API for illustrative purposes). (Actually takes multiplefilenames and does so for a fairly good reason.)But let’s pretend it takes a single config file name. This might feel likea convenience, since config often comes from files, but it’s really abarrier.
  58. 58. ✚It looks like the ConfigParser class out of the Python stdlib. Here’s anexcerpt of its API. Its purpose in life is to read INI files.Unfortunately, it’s not just a parser that does its jobs and translatesthe parsed content into something more usable, like a dictionary.Instead, the config ends up represented as this unwieldy ConfigParserobject with lots of custom-named methods that don’t conform to anyPython idiom. This means that any code that uses ConfigParser endsup written in its terms.And, if you want the flexibility to pull in configuration from somesource that isn’t an INI file, you’ll have to write your own abstractionlayer on top of ConfigParser.I’m going to continue to pick on ConfigParser (and pretend it has aslightly worse API for illustrative purposes). (Actually takes multiplefilenames and does so for a fairly good reason.)But let’s pretend it takes a single config file name. This might feel likea convenience, since config often comes from files, but it’s really abarrier.
  59. 59. ✚ ✚It looks like the ConfigParser class out of the Python stdlib. Here’s anexcerpt of its API. Its purpose in life is to read INI files.Unfortunately, it’s not just a parser that does its jobs and translatesthe parsed content into something more usable, like a dictionary.Instead, the config ends up represented as this unwieldy ConfigParserobject with lots of custom-named methods that don’t conform to anyPython idiom. This means that any code that uses ConfigParser endsup written in its terms.And, if you want the flexibility to pull in configuration from somesource that isn’t an INI file, you’ll have to write your own abstractionlayer on top of ConfigParser.I’m going to continue to pick on ConfigParser (and pretend it has aslightly worse API for illustrative purposes). (Actually takes multiplefilenames and does so for a fairly good reason.)But let’s pretend it takes a single config file name. This might feel likea convenience, since config often comes from files, but it’s really abarrier.
  60. 60. ✚ ✚ RawConfigParser.parse(string)It looks like the ConfigParser class out of the Python stdlib. Here’s anexcerpt of its API. Its purpose in life is to read INI files.Unfortunately, it’s not just a parser that does its jobs and translatesthe parsed content into something more usable, like a dictionary.Instead, the config ends up represented as this unwieldy ConfigParserobject with lots of custom-named methods that don’t conform to anyPython idiom. This means that any code that uses ConfigParser endsup written in its terms.And, if you want the flexibility to pull in configuration from somesource that isn’t an INI file, you’ll have to write your own abstractionlayer on top of ConfigParser.I’m going to continue to pick on ConfigParser (and pretend it has aslightly worse API for illustrative purposes). (Actually takes multiplefilenames and does so for a fairly good reason.)But let’s pretend it takes a single config file name. This might feel likea convenience, since config often comes from files, but it’s really abarrier.
  61. 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 anexcerpt of its API. Its purpose in life is to read INI files.Unfortunately, it’s not just a parser that does its jobs and translatesthe parsed content into something more usable, like a dictionary.Instead, the config ends up represented as this unwieldy ConfigParserobject with lots of custom-named methods that don’t conform to anyPython idiom. This means that any code that uses ConfigParser endsup written in its terms.And, if you want the flexibility to pull in configuration from somesource that isn’t an INI file, you’ll have to write your own abstractionlayer on top of ConfigParser.I’m going to continue to pick on ConfigParser (and pretend it has aslightly worse API for illustrative purposes). (Actually takes multiplefilenames and does so for a fairly good reason.)But let’s pretend it takes a single config file name. This might feel likea convenience, since config often comes from files, but it’s really abarrier.
  62. 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_upand walk_down.Sexy list compsNotice what this has done: it’s forced the caller to implement theirstuff in terms of your interface. Now they’re stuck with your libraryforever, unless they want to rewrite their code. Why not meet oncommon turf, like this? (Just return a map, or at least something thatacts like one.)This is about the language that your API speaks with its callers. Youhave a couple of choices here:You can speak their language, or you can force them to speak yours.
  63. 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_upand walk_down.Sexy list compsNotice what this has done: it’s forced the caller to implement theirstuff in terms of your interface. Now they’re stuck with your libraryforever, unless they want to rewrite their code. Why not meet oncommon turf, like this? (Just return a map, or at least something thatacts like one.)This is about the language that your API speaks with its callers. Youhave a couple of choices here:You can speak their language, or you can force them to speak yours.
  64. 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_upand walk_down.Sexy list compsNotice what this has done: it’s forced the caller to implement theirstuff in terms of your interface. Now they’re stuck with your libraryforever, unless they want to rewrite their code. Why not meet oncommon turf, like this? (Just return a map, or at least something thatacts like one.)This is about the language that your API speaks with its callers. Youhave a couple of choices here:You can speak their language, or you can force them to speak yours.
  65. 65. Plain Data warning signs(exception: inversion of control. Data, not control.)
  66. 66. Plain Data warning signs Users immediately transforming your output to another format(exception: inversion of control. Data, not control.)
  67. 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. 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 7bI have this idea that users of my APIs should spend most of theirtime “in the groove.” And I don’t just mean performing well ordoing their work easily. I mean it in a more physical sense.Think of programming as walking across a plane. The groove is anice, smooth path where it’s easy to walk. You could even walk itwith your eyes closed, because the sloping sides would guide yougently back to the center. Note that there’s nothing keeping youfrom stepping out of the groove and walking somewhere else if youlike; it’s not a wall you have to vault over—we’ll get to those in aminute. But the groove is very attractive, and new users are drawnthere first. Grooves are about guidance, not limitations.Here are some ideas on how to cut grooves in your APIs.
  69. 69. Grooviness Groove WallI have this idea that users of my APIs should spend most of theirtime “in the groove.” And I don’t just mean performing well ordoing their work easily. I mean it in a more physical sense.Think of programming as walking across a plane. The groove is anice, smooth path where it’s easy to walk. You could even walk itwith your eyes closed, because the sloping sides would guide yougently back to the center. Note that there’s nothing keeping youfrom stepping out of the groove and walking somewhere else if youlike; it’s not a wall you have to vault over—we’ll get to those in aminute. But the groove is very attractive, and new users are drawnthere first. Grooves are about guidance, not limitations.Here are some ideas on how to cut grooves in your APIs.
  70. 70. Avoid nonsense representations.Avoid nonsense representationsI 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. 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 representationsI 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. 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 snorkAvoid nonsense representationsI 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. 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 usingthe q argument and JSON-style queries using the body one.Of course, this meant nothing stopped you from doing this or eventhis. But those are nonsense. You shouldn’t even be able to get thatfar, not to mention the code that has to exist in your library to checkfor and reject such silliness.New pyelasticsearch replaces that mess with a single requiredargument to which you can pass either a string or a JSON dictionary.You can’t go wrong, because the interpreter won’t let you. Muchgroovier!Incidentally, this turns up another principle: fail shallowly. Inlanguages that give you tracebacks, this helps. If, heaven forbid,you’re using a language that doesn’t, this is vital.
  74. 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 usingthe q argument and JSON-style queries using the body one.Of course, this meant nothing stopped you from doing this or eventhis. But those are nonsense. You shouldn’t even be able to get thatfar, not to mention the code that has to exist in your library to checkfor and reject such silliness.New pyelasticsearch replaces that mess with a single requiredargument to which you can pass either a string or a JSON dictionary.You can’t go wrong, because the interpreter won’t let you. Muchgroovier!Incidentally, this turns up another principle: fail shallowly. Inlanguages that give you tracebacks, this helps. If, heaven forbid,you’re using a language that doesn’t, this is vital.
  75. 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 usingthe q argument and JSON-style queries using the body one.Of course, this meant nothing stopped you from doing this or eventhis. But those are nonsense. You shouldn’t even be able to get thatfar, not to mention the code that has to exist in your library to checkfor and reject such silliness.New pyelasticsearch replaces that mess with a single requiredargument to which you can pass either a string or a JSON dictionary.You can’t go wrong, because the interpreter won’t let you. Muchgroovier!Incidentally, this turns up another principle: fail shallowly. Inlanguages that give you tracebacks, this helps. If, heaven forbid,you’re using a language that doesn’t, this is vital.
  76. 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 usingthe q argument and JSON-style queries using the body one.Of course, this meant nothing stopped you from doing this or eventhis. But those are nonsense. You shouldn’t even be able to get thatfar, not to mention the code that has to exist in your library to checkfor and reject such silliness.New pyelasticsearch replaces that mess with a single requiredargument to which you can pass either a string or a JSON dictionary.You can’t go wrong, because the interpreter won’t let you. Muchgroovier!Incidentally, this turns up another principle: fail shallowly. Inlanguages that give you tracebacks, this helps. If, heaven forbid,you’re using a language that doesn’t, this is vital.
  77. 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 usingthe q argument and JSON-style queries using the body one.Of course, this meant nothing stopped you from doing this or eventhis. But those are nonsense. You shouldn’t even be able to get thatfar, not to mention the code that has to exist in your library to checkfor and reject such silliness.New pyelasticsearch replaces that mess with a single requiredargument to which you can pass either a string or a JSON dictionary.You can’t go wrong, because the interpreter won’t let you. Muchgroovier!Incidentally, this turns up another principle: fail shallowly. Inlanguages that give you tracebacks, this helps. If, heaven forbid,you’re using a language that doesn’t, this is vital.
  78. 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 usingthe q argument and JSON-style queries using the body one.Of course, this meant nothing stopped you from doing this or eventhis. But those are nonsense. You shouldn’t even be able to get thatfar, not to mention the code that has to exist in your library to checkfor and reject such silliness.New pyelasticsearch replaces that mess with a single requiredargument to which you can pass either a string or a JSON dictionary.You can’t go wrong, because the interpreter won’t let you. Muchgroovier!Incidentally, this turns up another principle: fail shallowly. Inlanguages that give you tracebacks, this helps. If, heaven forbid,you’re using a language that doesn’t, this is vital.
  79. 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 firstconstruct one, it isn’t poppable. You can pop a balloon that has noair in it. You might imagine the documentation says “Before poppingthe balloon, fill it with the desired amount of air.” That’s brokendesign. A poppable balloon should be poppable, full-stop. The usershouldn’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. Theuser 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 noisewhen you prick it.RAII is a more specific case of “Don’t have invariants that aren’t.”
  80. 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_muchAnother way to provide grooves is RAII.Here’s a poppable balloon class. But, as you can see, when you firstconstruct one, it isn’t poppable. You can pop a balloon that has noair in it. You might imagine the documentation says “Before poppingthe balloon, fill it with the desired amount of air.” That’s brokendesign. A poppable balloon should be poppable, full-stop. The usershouldn’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. Theuser 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 noisewhen you prick it.RAII is a more specific case of “Don’t have invariants that aren’t.”
  81. 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_muchAnother way to provide grooves is RAII.Here’s a poppable balloon class. But, as you can see, when you firstconstruct one, it isn’t poppable. You can pop a balloon that has noair in it. You might imagine the documentation says “Before poppingthe balloon, fill it with the desired amount of air.” That’s brokendesign. A poppable balloon should be poppable, full-stop. The usershouldn’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. Theuser 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 noisewhen you prick it.RAII is a more specific case of “Don’t have invariants that aren’t.”
  82. 82. Compelling examplesA final way of providing grooves is through compelling examples.These are the most overt sort of grooves. The user knows he’s beingadvised.This screenshot from MacPaint 1.0, with its palettes on the left andmenus on the top should look familiar to everybody in this room—even if they never used it—because Photoshop and Illustrator andthe GIMP and Pixelmator and Paintshop Pro have been following itsexample 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 writeexample code in a very pedagogical fashion, with outrageously longvariable names and inefficiency everywhere. I ended up having tostop that, because users of my libs were copying and pasting mycode for use in production! Oftentimes, they would even leave thecomments!So dont underestimate the docility of your users; they will do whatyou suggest. This isnt because theyre stupid. Its just that theyre
  83. 83. Compelling examplesA final way of providing grooves is through compelling examples.These are the most overt sort of grooves. The user knows he’s beingadvised.This screenshot from MacPaint 1.0, with its palettes on the left andmenus on the top should look familiar to everybody in this room—even if they never used it—because Photoshop and Illustrator andthe GIMP and Pixelmator and Paintshop Pro have been following itsexample 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 writeexample code in a very pedagogical fashion, with outrageously longvariable names and inefficiency everywhere. I ended up having tostop that, because users of my libs were copying and pasting mycode for use in production! Oftentimes, they would even leave thecomments!So dont underestimate the docility of your users; they will do whatyou suggest. This isnt because theyre stupid. Its just that theyre
  84. 84. Grooviness warning signs
  85. 85. Grooviness warning signs Nonsense states or representations
  86. 86. Grooviness warning signs Nonsense states or representations Invariants that aren’t
  87. 87. Grooviness warning signs Nonsense states or representations Invariants that aren’t Users unclear where to get started
  88. 88. Safety “I wish it was easier to hurt myself.” —Nobody, everNow we get to the walls.Walls are to keep you from hurting yourself or others. Just as in thephysical metaphor, there’s a continuum between grooves and walls.The more damage you can do with a feature, the higher the wallshould be in front of it.
  89. 89. Safety Groove WallNow we get to the walls.Walls are to keep you from hurting yourself or others. Just as in thephysical metaphor, there’s a continuum between grooves and walls.The more damage you can do with a feature, the higher the wallshould be in front of it.
  90. 90. update things set frob=2 where frob=1;Here is a horrible API with not enough walls. Does anyone see what’swrong with it? Maybe it’s bitten you in the past.Who here has wiped out a whole DB column because they forgot theWHERE clause?One way to fix this might be to require you to say all if you meanall. Might be less mathematically beautiful, but imagine the hours itwould 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 isseriously bad news. Don’t hit Return by accident!Maybe we could require -f for bare asterisks.
  91. 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’swrong with it? Maybe it’s bitten you in the past.Who here has wiped out a whole DB column because they forgot theWHERE clause?One way to fix this might be to require you to say all if you meanall. Might be less mathematically beautiful, but imagine the hours itwould 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 isseriously bad news. Don’t hit Return by accident!Maybe we could require -f for bare asterisks.
  92. 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’swrong with it? Maybe it’s bitten you in the past.Who here has wiped out a whole DB column because they forgot theWHERE clause?One way to fix this might be to require you to say all if you meanall. Might be less mathematically beautiful, but imagine the hours itwould 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 isseriously bad news. Don’t hit Return by accident!Maybe we could require -f for bare asterisks.
  93. 93. update things set frob=2 where frob=1; update things set frob=2; update things set frob=2 all; rm *.pycHere is a horrible API with not enough walls. Does anyone see what’swrong with it? Maybe it’s bitten you in the past.Who here has wiped out a whole DB column because they forgot theWHERE clause?One way to fix this might be to require you to say all if you meanall. Might be less mathematically beautiful, but imagine the hours itwould 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 isseriously bad news. Don’t hit Return by accident!Maybe we could require -f for bare asterisks.
  94. 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’swrong with it? Maybe it’s bitten you in the past.Who here has wiped out a whole DB column because they forgot theWHERE clause?One way to fix this might be to require you to say all if you meanall. Might be less mathematically beautiful, but imagine the hours itwould 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 isseriously bad news. Don’t hit Return by accident!Maybe we could require -f for bare asterisks.
  95. 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’swrong with it? Maybe it’s bitten you in the past.Who here has wiped out a whole DB column because they forgot theWHERE clause?One way to fix this might be to require you to say all if you meanall. Might be less mathematically beautiful, but imagine the hours itwould 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 isseriously bad news. Don’t hit Return by accident!Maybe we could require -f for bare asterisks.
  96. 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 thedelete method in pyelasticsearch used to look like. It deletes adocument 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 happilydelete all documents. And id here is OPTIONAL! I can imaginemyself coding along and managing to close your parentheses early.Or I could leave out one of the previous arguments so the value Iintended as the ID gets interpreted as one of the other argumentsinstead.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 calleddelete_all.
  97. 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 thedelete method in pyelasticsearch used to look like. It deletes adocument 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 happilydelete all documents. And id here is OPTIONAL! I can imaginemyself coding along and managing to close your parentheses early.Or I could leave out one of the previous arguments so the value Iintended as the ID gets interpreted as one of the other argumentsinstead.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 calleddelete_all.
  98. 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 thedelete method in pyelasticsearch used to look like. It deletes adocument 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 happilydelete all documents. And id here is OPTIONAL! I can imaginemyself coding along and managing to close your parentheses early.Or I could leave out one of the previous arguments so the value Iintended as the ID gets interpreted as one of the other argumentsinstead.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 calleddelete_all.
  99. 99. Error reporting: exceptions are typically safer.The other thing to consider is how to report errors. You have 2choices in most languages: return something, or throw anexception. Raising an exception is typically the safer of the two, as itreduces the chance of accidentally swallowing an error.
  100. 100. Safetywarning signsIn general, Ask yourself, "How can I use this to hurt myself andothers?"People will blame themselves.
  101. 101. Safetywarning signs Surprisingly few—people will blame themselves.In general, Ask yourself, "How can I use this to hurt myself andothers?"People will blame themselves.
  102. 102. non-astronautics consistency brevity composability plain data safety groovinessWhile 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. 103. non-astronautics consistency Elegance? brevity composability “It is the ineffable will of Bob.” —Old Thrashbarg plain data safety groovinessWhile 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. 104. Thank You Merci https://joind.in/7995 twitter: ErikRose erik@mozilla.com

×