Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Design of Everyday APIs

The Design of Everyday APIs

What makes a good API for a library? Or more importantly, what makes it bad? This talk discusses the principles of what goes into user-centered design, and how best to apply those principles when writing a Python library for fellow developers.

8c5e76dca74a59822dbf7f0286177ddd?s=128

Lynn Root

June 02, 2022
Tweet

More Decks by Lynn Root

Other Decks in Technology

Transcript

  1. The Design of Everyday APIs Lynn Root @roguelynn Staff Engineer

    @ Spotify
  2. $ whoami▮ $ whoami

  3. None
  4. None
  5. – Don Norman, The Design of Everyday Things Design is

    concerned with how things work, how they are controlled, and the nature of the interaction between people and technology.
  6. – Don Norman, The Design of Everyday Things Design is

    concerned with how things work, how they are controlled, and the nature of the interaction between people and technology.
  7. – Don Norman, The Design of Everyday Things Two of

    the most important characteristics of good design are discoverability and understanding.
  8. 5 Key Elements of Discoverability

  9. 5 Key Elements of Discoverability ► Affordances

  10. 5 Key Elements of Discoverability ► Affordances ► Signi fi

    ers
  11. 5 Key Elements of Discoverability ► Affordances ► Signi fi

    ers ► Constraints
  12. 5 Key Elements of Discoverability ► Affordances ► Signi fi

    ers ► Constraints ► Mappings
  13. 5 Key Elements of Discoverability ► Affordances ► Signi fi

    ers ► Constraints ► Mappings ► Feedback
  14. Understanding

  15. Understanding System / Product

  16. Understanding Conceptual Model System / Product User

  17. Understanding Conceptual Model Conceptual Model System / Product Designer User

  18. Understanding Conceptual Model Conceptual Model System / Product User Designer

  19. Understanding User Designer Conceptual Model Conceptual Model System / Product

  20. None
  21. $ ffmpeg -filters wat wtf is all this no... plz

    stahp...
  22. None
  23. PLAY►

  24. Good API Design

  25. Good API Design LYNN’S 3 TENETS TO

  26. None
  27. 1 of 2 class Message: def __init__(self, id: str, data:

    str, published_at: str) -> None: self.id = id self.data = data self.published_at = published_at Library
  28. class Message: def __init__(self, id: str, data: str, published_at: str)

    -> None: class PubSubClient: def __init__(self, topic: str, subscription: str) -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library 2 of 2
  29. 1 of 2 client = chaos_queue.PubSubClient(TOPIC, SUBSCRIPTION) try: client.create_topic() except

    chaos_queue.TopicExists: pass try: client.create_subscription() except chaos_queue.SubscriptionExists: pass User
  30. 2 of 2 for i in range(5): message = chaos_queue.Message(

    id=str(uuid.uuid4()), data=f"hello {i}", published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User
  31. for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}",

    published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User 2 of 2
  32. for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}",

    published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User 2 of 2
  33. for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}",

    published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User 2 of 2
  34. intuitive intuitiv

  35. Use Domain Nomenclature intuitive

  36. before class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library
  37. before class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library
  38. after class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None: Library
  39. after class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None: Library
  40. Clumsy Naming Hints at Clumsy Abstractions intuitive

  41. before Library class PubSubClient: def __init__(self, topic: str, subscription: str)

    -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None:
  42. before Library class PubSubClient: def __init__(self, topic: str, subscription: str)

    -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None:
  43. after class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: Library
  44. Provide Symmetry intuitive

  45. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  46. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  47. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: after Library
  48. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: after Library
  49. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Intuitive Tenet 2… Tenet 3…
  50. flexible

  51. Provide Sane Defaults flexible

  52. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  53. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  54. after class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int = 30, retries: int = 0, ) -> None: def close(self) -> None: class SubClient: ... Library
  55. after class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int = 30, retries: int = 0, request_id: RequestIdType | bytes | str | None = None, debug: DebugLevelType | int | bool | None = None, ) -> None: def close(self) -> None: class SubClient: ... Library
  56. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, **kwargs) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  57. Minimize Repetition flexible

  58. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, **kwargs) -> None: def close(self) -> None: before Library
  59. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, **kwargs) -> None: def close(self) -> None: before Library
  60. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: after Library
  61. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: after Library
  62. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: ... for message in msgs: self._queue.put(message, timeout=timeout) def close(self) -> None: after Library
  63. Be Predictable and Precise flexible

  64. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  65. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  66. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  67. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  68. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: ... try: return self._queue.get(timeout=timeout) except queue.Empty: raise ChaosEmptyError("Subscription queue is empty!") def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  69. Let Users Be Lazy flexible

  70. class Message: def __init__(self, id: str, data: str, published_at: str)

    -> None: self.id = id self.data = data self.published_at = published_at before Library
  71. OptStr = str | None class Message: def __init__( self,

    data: str, id: OptStr = None, published_at: OptStr = None ): self.data = data self.id = id or str(uuid.uuid4()) self.published_at = published_at or datetime.datetime.now().isoformat() after Library
  72. OptStr = str | None class Message: def __init__( self,

    data: str, id: OptStr = None, published_at: OptStr = None ): self.data = data self.id = id or str(uuid.uuid4()) self.published_at = published_at or datetime.datetime.now().isoformat() def __repr__(self) -> str: return f"Message(id={self.id}, published_at={self.published_at})" after Library
  73. from dataclasses import dataclass, field @dataclass class Message: data: str

    = field(repr=False) id: str = field(default=None) published_at: str = field(default=None) def __post_init__(self): self.id = self.id or str(uuid.uuid4()) self.published_at = self.published_at or datetime.datetime.now().isoformat option 2 after Library
  74. from attrs import define, field @define class Message: data: str

    = field(repr=False) id: str = field() published_at: str = field() @id.default def set_id_default(self): return str(uuid.uuid4()) @published_at.default def set_published_at_default(self): return datetime.datetime.now().isoformat() after Library option 3
  75. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Provide sane defaults Minimize repetition Be predictable and precise Let users be lazy Flexible Intuitive Tenet 3…
  76. simple

  77. Provide Composable Functions simple

  78. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  79. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  80. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  81. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  82. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  83. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  84. Leverage Language Idioms simple

  85. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  86. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  87. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def iter(self, **kwargs) -> Iterator[Message]: ... while not self._queue.empty(): message = self._queue.get(timeout=timeout) yield message def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  88. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def iter(self, **kwargs) -> Iterator[Message]: ... while not self._queue.empty(): message = self._queue.get(timeout=timeout) yield message def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  89. Leverage Language Idioms (again) simple

  90. before class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: Library
  91. before class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: Library
  92. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: def __enter__(self) -> Self: return self def __exit__(self, *args) -> None: self.close() after Library
  93. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: def __enter__(self) -> Self: return self def __exit__(self, *args) -> None: self.close() after Library
  94. Provide Convenience simple

  95. README.md ## Installation $ pip install chaos-queue

  96. ## Installation $ pip install chaos-queue ## Get Started import

    chaos_queue messages = [chaos_queue.Message(str(i)) for i in range(5)] pub_client = chaos_queue.PubClient(TOPIC) with pub_client.create(): pub_client.publish(*messages, timeout=1, retry=False) README.md
  97. ## Installation $ pip install chaos-queue ## Get Started import

    chaos_queue messages = [chaos_queue.Message(str(i)) for i in range(5)] pub_client = chaos_queue.PubClient(TOPIC) with pub_client.create(): pub_client.publish(*messages, timeout=1, retry=False) ## Learn more Just go to chaos-queue.readthedocs.io for documentation and more examples! README.md
  98. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Provide sane defaults Minimize repetition Be predictable and precise Let users be lazy Provide composable functions Leverage language idioms Provide convenience Flexible Intuitive Simple
  99. ◄◄ REWIND

  100. before class Message: def __init__(self, id: str, data: str, published_at:

    str) -> None: self.id = id self.data = data self.published_at = published_at class PubSubClient: def __init__(self, topic: str, subscription: str) -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library
  101. after @define class Message: data: str = field(repr=False) id: str

    = field() published_at: str = field() @id.default def set_id_default(self): return str(uuid.uuid4()) @published_at.default def set_published_at_default(self): return datetime.datetime.now().isoformat() class PubClient: def __init__(self, topic: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: def __enter__(self) -> Self: def __exit__(self, *args) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def iter(self, **kwargs) -> Iterable[Message]: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: def __enter__(self) -> Self: def __exit__(self, *args) -> None: Library
  102. before client = chaos_queue.PubSubClient(TOPIC, SUBSCRIPTION) try: client.create_topic() except chaos_queue.TopicExists: pass

    try: client.create_subscription() except chaos_queue.SubscriptionExists: pass for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}", published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, False) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, True) if not message: break print(message) process_data(message.data) client.mark_message_done(message, None, False) client.close_client() User
  103. after messages = [chaos_queue.Message(data=f"hello {i}") for i in range(5)] with

    chaos_queue.PubClient(TOPIC) as pub_client: pub_client.create() try: pub_client.publish(messages, timeout=1) except chaos_queue.TimeoutError: pass with chaos_queue.SubClient(SUBSCRIPTION) as sub_client: sub_client.create() for message in sub_client.iter(): print(message) process_data(message.data) sub_client.ack(message) User
  104. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Provide sane defaults Minimize repetition Be predictable and precise Let users be lazy Provide composable functions Leverage language idioms Provide convenience Flexible Intuitive Simple
  105. – Don Norman, The Design of Everyday Things If all

    else fails, standardize.
  106. rogue.ly/apis Thank you! Lynn Root @roguelynn