Slide 1

Slide 1 text

API Design Tips

Slide 2

Slide 2 text

Who?

Slide 3

Slide 3 text

Who? •Daniel Lindsley

Slide 4

Slide 4 text

Who? •Daniel Lindsley •Toast Driven ( ) is my business

Slide 5

Slide 5 text

Who? •Daniel Lindsley •Toast Driven ( ) is my business •Wrote a couple Django apps •Haystack •Tastypie

Slide 6

Slide 6 text

What?

Slide 7

Slide 7 text

What? •Not HTTP APIs

Slide 8

Slide 8 text

What? •Not HTTP APIs •(though I love those. seriously. come talk to me about them...)

Slide 9

Slide 9 text

What? •Programmatic APIs

Slide 10

Slide 10 text

What? •Programmatic APIs •Think libraries •Especially ones you hand off to other people...

Slide 11

Slide 11 text

Why?

Slide 12

Slide 12 text

Why? •Think about how many times you’ve started with someone else’s library...

Slide 13

Slide 13 text

Why? •Think about how many times you’ve started with someone else’s library... •Used it some...

Slide 14

Slide 14 text

Why? •Think about how many times you’ve started with someone else’s library... •Used it some... •Then wanted to strangle them for the hoops they made you jump through

Slide 15

Slide 15 text

Why? •Other people use your code all the time.

Slide 16

Slide 16 text

Why? •Other people use your code all the time. •They might be wanting to strangle you!

Slide 17

Slide 17 text

Why? •Other people use your code all the time. •They might be wanting to strangle you! •It might even be future you wanting to strangle past you.

Slide 18

Slide 18 text

Why? •Nice APIs beget Happiness

Slide 19

Slide 19 text

Why? •Nice APIs beget Happiness •Happiness begets Recommendations

Slide 20

Slide 20 text

Why? •Nice APIs beget Happiness •Happiness begets Recommendations •Recommendations beget Users/ Community

Slide 21

Slide 21 text

Philosophy

Slide 22

Slide 22 text

You can’t make everyone happy by default. You should still have sane defaults.

Slide 23

Slide 23 text

But more people will be happy if they can tweak it. You can bet they’ll need to.

Slide 24

Slide 24 text

And most people will be happy if it’s easy to tweak. I don’t have a clever followup here.

Slide 25

Slide 25 text

No copy-paste should be needed. Boilerplate sucks.

Slide 26

Slide 26 text

They shouldn’t have to constantly refer to the docs. Especially not for things they use all the freaking time.

Slide 27

Slide 27 text

Good docs matter. Saves you (support) & them (implementation) time. Everyone wins.

Slide 28

Slide 28 text

Real World™ use is the best sanity check. Cleverness goes here.

Slide 29

Slide 29 text

Someone is going to do something weird/insane with your code. It’s inevitable, so design for it up front.

Slide 30

Slide 30 text

Approaches on Design

Slide 31

Slide 31 text

Approaches On Design •Common Methodologies:

Slide 32

Slide 32 text

Approaches On Design •Common Methodologies: •Bottom-up

Slide 33

Slide 33 text

Approaches On Design •Common Methodologies: •Bottom-up •Top-down

Slide 34

Slide 34 text

Approaches On Design •Common Methodologies: •Bottom-up •Top-down •WARNING: OPINIONS FOLLOW

Slide 35

Slide 35 text

Approaches On Design •Bottom-up sucks.

Slide 36

Slide 36 text

Approaches On Design •Bottom-up sucks. •Sure, you built little pieces that work.

Slide 37

Slide 37 text

Approaches On Design •Bottom-up sucks. •Sure, you built little pieces that work. •But do they work well together?

Slide 38

Slide 38 text

Approaches On Design •Bottom-up sucks. •Sure, you built little pieces that work. •But do they work well together? •Likely not. •HOW IS BABBY PHP FORMED

Slide 39

Slide 39 text

Approaches On Design •Top-down feels better.

Slide 40

Slide 40 text

Approaches On Design •Top-down feels better. •Everything fits together right.

Slide 41

Slide 41 text

Approaches On Design •Top-down feels better. •Everything fits together right. •Less duplication.

Slide 42

Slide 42 text

Approaches On Design •Top-down feels better. •Everything fits together right. •Less duplication. •Resist the urge to duct tape.

Slide 43

Slide 43 text

Bonus to Top-Down?

Slide 44

Slide 44 text

Instant TDD

Slide 45

Slide 45 text

Approaches On Design •With (some) instant TDD, you get your tests started from the get-go.

Slide 46

Slide 46 text

Approaches On Design •With (some) instant TDD, you get your tests started from the get-go. •Fewer massive, painful refactorings down the road.

Slide 47

Slide 47 text

Things You Should Do

Slide 48

Slide 48 text

Small Components Worked for UNIX, it’ll work for you.

Slide 49

Slide 49 text

Small Components class DoEverything(object): # ... def update_cache(self, timeout=600): # Stuff... def post_to_twitter(self, user): # More stuff... def make_me_a_sandwich(self, sudo=False): # Even more stuff...

Slide 50

Slide 50 text

Small Components class Cachable(object): timeout = 600 def update(self): # Cache myself... class SocialPosts(object): def twitter(self, user): # Post a tweet class SandwichMaker(object): def __init__(self, sudo=False): self.sudo = sudo def make(self): # Maybe yes, maybe no.

Slide 51

Slide 51 text

Composition >= Inheritance Why do the work yourself when you can delegate?

Slide 52

Slide 52 text

Composition >= Inheritance class CachedView(View): def get(self, request, **kwargs): # Cache the output & serve. class GhettoAPIView(View): def get(self, request, **kwargs): # Return either JSON or HTML. # Now try to use both. :/ class MyView(CachedView, GhettoAPIView): # Oh crap, they stomp on each other. pass

Slide 53

Slide 53 text

Composition >= Inheritance class MyView(BetterView): def get(self, **kwargs): response = self.preprocess() # Do stuff. return self.render('template.html', {}) def preprocess(self): # Delegate! cached = ViewCache() return cached.get(self, self.request) def render(self, template, context): output = GhettoAPI(self.request) return output.render(template, context)

Slide 54

Slide 54 text

Reflection If data can flow one way, add the opposite direction as well.

Slide 55

Slide 55 text

Reflection class FakeDate(BadDate): def to_iso8801(self): return '-'.join( self.year, self.month, self.day )

Slide 56

Slide 56 text

Reflection class FakeDate(BadDate): def to_iso8801(self): return '-'.join( self.year, self.month, self.day ) def from_iso8801(self, date_string): date_bits = date_string.split('-') self.year = date_bits[0] self.month = date_bits[1] self.day = date_bits[2]

Slide 57

Slide 57 text

Broad Familiarity If it’s a similar task to something else they’ll know, mimic that something else.

Slide 58

Slide 58 text

Broad Familiarity def go_search(engine_url, query, limit=10 offset=0, just_values=False): # Hit the search engine... return results def crazy_raw_query(engine_url, raw_query): # Hope & pray the query is OK. return raw_results # Usage: kwargs = { 'limit': 20, 'offset': 20, } results = go_search('http://...', 'banana')

Slide 59

Slide 59 text

Broad Familiarity class SearchQuerySet(object): # Make it look like ``QuerySet``. def __getitem__(self, key): # Handle the slice here. def filter(self, **kwargs): # Apply filters. def raw(self, query): # Do a raw query. # Usage: sqs = SearchQuerySet().filter(text='banana') results = sqs[20:20]

Slide 60

Slide 60 text

Narrow Familiarity Call signatures matter.

Slide 61

Slide 61 text

Narrow Familiarity def go_search(engine_url, query, limit=10 offset=0, just_values=False): # Do stuff. return results def execute_raw_query(raw_query, offset, engine_url): # Do a raw query here. return { 'search_results': results, }

Slide 62

Slide 62 text

Narrow Familiarity def query(engine_url, query, limit=10 offset=0, just_values=False): # Do stuff. return { 'results': results, 'total': total_count, } def raw_query(engine_url, query, limit=10 offset=0, just_values=False): # Do a raw query here. return { 'results': results, 'total': total_count, }

Slide 63

Slide 63 text

Protocol Gently “suggest” things behave the same way. With a stick.

Slide 64

Slide 64 text

Protocol class CoolKidBackend(object): # Whatever, as if we care. pass class RedisBackend(CoolKidBackend): def get(self, key): return self.conn.hget(key) class RiakBackend(CoolKidBackend): def get_hash(self, bucket, key): return self._conn.get(bucket, key) # You can't transparently switch backends. # Welp, time to go rewrite everything.

Slide 65

Slide 65 text

Protocol class CoolKidBackend(object): def __init__(self, conn_string): # Parse the bucket out of # the conn_string, if present. def get(self, key): raise NotImplementedError('...') class RedisBackend(CoolKidBackend): def get(self, key): return self.conn.hget(key) class RiakBackend(CoolKidBackend): def get(self, key): return self._conn.get(...)

Slide 66

Slide 66 text

Assume The Worst™! Don’t code for just the easy case.

Slide 67

Slide 67 text

Assume The Worst™! def parse_name(name): # It'll be fine. Names are always # " " # and ASCII, right? return name.split(' ')

Slide 68

Slide 68 text

Assume The Worst™! def parse_name(name, separator=' ', middlename_present=False, allowed_suffixes=SUFFIXES): if ',' in name: maybe_last_name = name.split(',', 1) if maybe_last_name in allowed_suffixes: # ... return { 'original': name, 'confidence': confidence_level * 100, 'first_name': first, # ... }

Slide 69

Slide 69 text

Things You Should NOT Do

Slide 70

Slide 70 text

Stop At A Low Level “This Low Level API ought to be good enough.” Yeah, right. And 640Kb was totally enough for everyone.

Slide 71

Slide 71 text

Stop At A Low Level def get_page(socket, timeout): # Use the open socket to fetch # the web page. It's a open # socket to the correct place, # right?

Slide 72

Slide 72 text

Stop At A Low Level def get_raw_page(socket, timeout=60): # Do the actual fetch. return headers, content def get_page(url, timeout=None, fetcher=get_raw_page): # Validate the URL. # Open a socket. head, content = fetcher(my_socket) if head.status == 500: raise OhNoes('...') # ...

Slide 73

Slide 73 text

Wildly Different Return Values “Hm, does this return an integer or a dictionary?”

Slide 74

Slide 74 text

Wildly Different Return Values def query(engine_url, query, limit=10): # Make the user deal with the raw data. results = [res for res in results if res] return results, total def raw_query(engine_url, query, limit=10): # Make the user deal with the raw data. return { 'search_results': results, 'count': total, }

Slide 75

Slide 75 text

Wildly Different Return Values def query(engine_url, query, limit=10): # We're doing a search query. # Return sane, similar structures. return { 'results': results, 'total': total_count, } def raw_query(engine_url, query, limit=10): # We're doing a search query. # Return sane, similar structures. return { 'results': results, 'total': total_count, }

Slide 76

Slide 76 text

Useless “Implementation” Code If it can’t do anything for itself, it’s code without purpose.

Slide 77

Slide 77 text

Useless “Implementation” Code class BaseBackend(object): # Full implementation. def __init__(self, url): raise NotImplementedError('...') def save(self, filename, data): raise NotImplementedError('...') def load(self, filename): raise NotImplementedError('...')

Slide 78

Slide 78 text

Useless “Implementation” Code class FileBackend(object): # Does useful things. Subclasses can # override less. def __init__(self, url): self.url = url def save(self, filename, data): the_file = open(filename, 'w') return json.dump(the_file, data) def load(self, filename): data = open(filename) return json.load(data)

Slide 79

Slide 79 text

If It’s Difficult To Test... ...you screwed up. But maybe you can fix it.

Slide 80

Slide 80 text

If It’s Difficult To Test... def current_temperature(station): # Oh yeah, this totally works BTW. url = 'http://w1.weather.gov/obhistory/' + \ '%s.html' % station resp = requests.get(url) data = pq(resp.content) return float(data('table').eq(3)\ .find('tr').eq(4)\ .find('td').eq(6).text())

Slide 81

Slide 81 text

If It’s Difficult To Test... class Weather(object): def build_url(self, station): return 'http://w1.weather.gov/obhistory/' + \ '%s.html' % station def fetch_page(self, url): return requests.get(url).content def parse_page(self, content): return pq(resp.content) def last_conditions(self, data): return data('table').eq(3).find('tr').eq(4) def current_temperature(self, station): url = self.build_url(station) content = self.fetch_page(url) data = self.parse_page(content) return float(self.last_conditions(data)\ .find('td').eq(6).text())

Slide 82

Slide 82 text

django-Specific Topics

Slide 83

Slide 83 text

Pluggable Backend ALL THE THINGS

Slide 84

Slide 84 text

Internationalize ALL THE THINGS

Slide 85

Slide 85 text

Dynamically Loadable Classes/Code 60% more error-handling, every time.

Slide 86

Slide 86 text

Declarative Syntax Metaclasses: Call your doctor if headaches last more than 4 hours.

Slide 87

Slide 87 text

Don’t metaclass ALL THE THINGS

Slide 88

Slide 88 text

The ORM Love it or hate it, there’s some great learning opportunities there to be had.

Slide 89

Slide 89 text

Other Ideas

Slide 90

Slide 90 text

Very Little Global State # Only defaults or things that absolute # have to be module-level. BASE_URL = 'http://w1.weather.gov/obhistory/' TIMEOUT = 10 # Make them defaults but allow passing # different in their place. def fetch_page(self, url=BASE_URL, timeout=TIMEOUT): return requests.get( url, timeout=timeout ).content

Slide 91

Slide 91 text

Decrease Reliance On ``self`` class Weather(object): def build_url(self, station): # Make a URL. def fetch_page(self, url): # Get a page def parse_page(self, content): # Use the page content. def current_temperature(self, station): url = self.build_url(station) content = self.fetch_page(url) data = self.parse_page(content) # You could be using ``self.`` on # these, but by allowing data to be passed # in, you make it easier to test.

Slide 92

Slide 92 text

Resist The Urge To Use Magic Be explicit first, then add shortcuts. (which can be a little more magical)

Slide 93

Slide 93 text

So...

Slide 94

Slide 94 text

So... •Use the Golden Rule.

Slide 95

Slide 95 text

So... •Use the Golden Rule. •Consistency is key.

Slide 96

Slide 96 text

So... •Use the Golden Rule. •Consistency is key. •Plan for the worst & include sweet shortcuts for the best.

Slide 97

Slide 97 text

So... •Use the Golden Rule. •Consistency is key. •Plan for the worst & include sweet shortcuts for the best. •Write a thing you’d to use...

Slide 98

Slide 98 text

So... •Use the Golden Rule. •Consistency is key. •Plan for the worst & include sweet shortcuts for the best. •Write a thing you’d to use... •...then make it even better.

Slide 99

Slide 99 text

Thanks!

Slide 100

Slide 100 text

I’m Daniel Lindsley of Toast Driven @toastdriven http://toastdriven.com/