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

Designing APIs for internal and external use

Designing APIs for internal and external use

I am talking about what I learned by designing APIs for various frameworks and internal modules and my experience in making APIs more robust and how to prevent errors before they ship to the user.

Michael Ochs

April 24, 2017
Tweet

More Decks by Michael Ochs

Other Decks in Programming

Transcript

  1. –Some developer somewhere “There are two hard things in computer

    science: cache invalidation, naming things, and off-by-one errors.”
  2. –Some developer somewhere “There are two hard things in computer

    science: cache invalidation, naming things, and off-by-one errors.”
  3. Who am I to talk about that? • By no

    means an expert • Some experience on API design • PSPDFKit • ViewDeck • Internal frameworks
  4. What is an API? • Method • Group of methods

    • Class • Group of classes • Modules • Everything you see from the outside Higher Level API Class Category Method Method Category Method Method Class Category Method Method Category Method Method
  5. Structure of a good API • Structured into parts •

    Single purpose per part • Clear purpose UIView UIViewController UIWindow
  6. What is an API good for? • Separation of concerns

    • Hides complex logic from consumers • Abstracts a problem and makes it understandable • Makes a solution reusable for different consumers
  7. General Considerations • API design is more important than having

    a straight forward implementation • Often these two are interconnected, but if not, choose the better API, not the better implementation • Design your API based on use cases and think about how an ideal API would look like for the use cases in mind • Don’t start with what you have and try to build a nice API wrapper around it
  8. General Considerations • Better copy from the best than having

    a bad API that is fully yours • Build a team environment where designing APIs is at least as important as writing code
  9. Consider your audience • There are different layers of APIs

    • These layers may have different consumers • Consider these groups when designing your APIs • A private API might not need to be as rock solid as a public API • It may be okay if some helper method, private to a class, has a horrible API to speed things up, but is it okay for a public method?
  10. Consider your audience • The average experience of a developer

    using an API might be different based on what the API is for • The newest animation hipster framework • An API to get a 3D model from image data in real time to analyze the traffic around an autonomous vehicle • If you think customer support is bad, you clearly never did any support on GitHub…
  11. Consider your audience /**
 Renders an image.
 @note This is

    a long running, blocking task.
 Only call from a background queue!
 !*/
 - (UIImage *)renderImage; vs /**
 Renders an image.
 !*/
 - (void)renderImage:(void(^)(UIImage *))completionHandler
  12. Methods and Classes • The less dependencies the better •

    The less features the better • Only provide what you really need • Keep the interface of classes small • Group methods together • Think hard about the naming
  13. Methods and Classes • Everything that starts with PSPDFRender… belongs

    together • Inside these classes, methods don’t have the render… prefix -[PSPDFRenderTask initWithRequest:] -[PSPDFRenderQueue scheduleTask:] Example: Rendering in PSPDFKit PSPDFRenderRequest PSPDFRenderTask PSPDFRenderQueue PSPDF Cache
  14. Methods and Classes • Every class has a very well

    defined lifetime and purpose • Request: Describes the image • Task: Controls the rendering • Queue: Manages task prioritization Example: Rendering in PSPDFKit PSPDFRenderRequest PSPDFRenderTask PSPDFRenderQueue PSPDF Cache
  15. Methods and Classes • The interface is very small •

    Request: 8 properties, 2 methods • Task: 6 properties, 3 methods • Queue: 2 properties, 3 methods Example: Rendering in PSPDFKit PSPDFRenderRequest PSPDFRenderTask PSPDFRenderQueue PSPDF Cache
  16. Methods and Classes !// UIGestureRecognizer.h
 @interface UIGestureRecognizer
 @property(nonatomic,readonly) UIGestureRecognizerState state;


    !!...
 @end
 
 !// UIGestureRecognizerSubclass.h
 @interface UIGestureRecognizer (UIGestureRecognizerProtected)
 @property(nonatomic,readwrite) UIGestureRecognizerState state;
 !!...
 @end Example: Hiding as much as possible
  17. Methods and Classes • MyObject.h • MyObject+Internal.h • MyObject+Protected.h •

    MyObject+Private.h • Use the appropriate visibility attributes in Swift
  18. Methods and Classes • Always prefix category methods – in

    Swift too! • -[NSString foo_myMethod] / -[NSString fooMyMethod] • If it is an Objective-C class under the hood, Swift will expose these things • If a framework’s class adds a method: • In your subclass it breaks your subclass • In a category it breaks every consumer of that method
  19. Nullability • NS_ASSUME_NONNULL all the things • Use nonnull as

    much as possible • e.g. collections almost never need to be nullable.
 An empty collection is just fine.
  20. Enforce your API contracts • Assert, assert, assert • Use

    NSAssert, NSParameterAssert • nullability, classes, container counts
  21. Enforce your API contracts - (void)setFileURL:(NSURL *)fileURL {
 NSParameterAssert([fileURL isKindOfClass:NSURL.class]

    !&& fileURL.isFileURL);
 _fileURL = fileURL;
 } • People will try to put a string or a remote URL in there! • Enforce things like this early to avoid trouble afterwards
  22. Enforce your API contracts • Apps that crash early don’t

    crash often • Apps that don’t crash early often do a lot of strange things
  23. Immutability • Prefer immutability whenever possible • Thread safe due

    to its nature • Explicit information flow • Less complexity
  24. Immutability • Never return mutable structures such as NSMutableArray •

    Make properties for potentially mutable classes copy their values • Make all properties on a class readonly • Consider creating mutable / immutable pairs if necessary
  25. Immutability @interface MyObject () {
 NSMutableArray *_myArray;
 }
 @end
 


    @implementation MyObject
 - (instancetype)init {
 self = [super init];
 if (self) {
 _myArray = [NSMutableArray new];
 }
 return self;
 }
 - (NSArray *)myArray {
 return _myArray.copy;
 }
 @end Example: Internally mutable
  26. Immutability @interface MyObject () {
 NSMutableArray *_myArray;
 }
 @end
 


    @implementation MyObject
 - (instancetype)init {
 self = [super init];
 if (self) {
 _myArray = [NSMutableArray new];
 }
 return self;
 }
 - (NSArray *)myArray {
 return _myArray.copy;
 }
 @end Example: Internally mutable
  27. Immutability @interface MyObject () {
 NSMutableArray *_myArray;
 }
 @end
 


    @implementation MyObject
 - (instancetype)init {
 self = [super init];
 if (self) {
 _myArray = [NSMutableArray new];
 }
 return self;
 }
 - (NSArray *)myArray {
 return _myArray.copy;
 }
 @end Example: Internally mutable
  28. Immutability @interface MyObject () {
 NSMutableArray *_myArray;
 }
 @end
 


    @implementation MyObject
 - (instancetype)init {
 self = [super init];
 if (self) {
 _myArray = [NSMutableArray new];
 }
 return self;
 }
 - (NSArray *)myArray {
 return _myArray.copy;
 }
 @end Example: Internally mutable
  29. Immutability @interface MyObject : NSObject
 
 @property (copy) NSArray *myArray;


    - (instancetype)initWithMyArray:(NSArray *)myArray;
 
 @end Example: Copy properties
  30. Immutability @interface MyObject : NSObject
 
 @property (copy) NSArray *myArray;


    - (instancetype)initWithMyArray:(NSArray *)myArray;
 
 @end Example: Copy properties
  31. Immutability @implementation MyObject
 
 - (instancetype)initWithMyArray:(NSArray *)myArray {
 self =

    [super init];
 if (self) {
 _myArray = myArray.copy;
 }
 return self;
 }
 
 @end Example: Copy properties
  32. Immutability @implementation MyObject
 
 - (instancetype)initWithMyArray:(NSArray *)myArray {
 self =

    [super init];
 if (self) {
 _myArray = myArray.copy;
 }
 return self;
 }
 
 @end Example: Copy properties
  33. Immutability @interface MyObject : NSObject
 
 @property (copy, readonly) NSArray

    *myArray;
 @property (copy, readonly) NSString *myString;
 @property (copy, readonly) NSData *myData;
 @property (copy, readonly) NSNumber *myNumber;
 
 - (instancetype)initWithMyArray:(NSArray *)myArray
 myString:(NSString *)myString
 myData:(NSData *)myData
 myNumber:(NSNumber *)myNumber;
 
 @end Example: Readonly properties
  34. Immutability @interface MyObject : NSObject
 
 @property (copy, readonly) NSArray

    *myArray;
 @property (copy, readonly) NSString *myString;
 @property (copy, readonly) NSData *myData;
 @property (copy, readonly) NSNumber *myNumber;
 
 - (instancetype)initWithMyArray:(NSArray *)myArray
 myString:(NSString *)myString
 myData:(NSData *)myData
 myNumber:(NSNumber *)myNumber;
 
 @end Example: Readonly properties
  35. Immutability @interface MyObject : NSObject <NSCopying, NSMutableCopying>
 @property (copy, readonly)

    NSArray *myArray;
 @property (copy, readonly) NSString *myString;
 @property (copy, readonly) NSData *myData;
 @property (copy, readonly) NSNumber *myNumber;
 @end
 
 @interface MyMutableObject : MyObject
 @property (copy, readwrite) NSArray *myArray;
 @property (copy, readwrite) NSString *myString;
 @property (copy, readwrite) NSData *myData;
 @property (copy, readwrite) NSNumber *myNumber;
 @end Example: Readonly properties
  36. Immutability @interface MyObject : NSObject <NSCopying, NSMutableCopying>
 @property (copy, readonly)

    NSArray *myArray;
 @property (copy, readonly) NSString *myString;
 @property (copy, readonly) NSData *myData;
 @property (copy, readonly) NSNumber *myNumber;
 @end
 
 @interface MyMutableObject : MyObject
 @property (copy, readwrite) NSArray *myArray;
 @property (copy, readwrite) NSString *myString;
 @property (copy, readwrite) NSData *myData;
 @property (copy, readwrite) NSNumber *myNumber;
 @end Example: Readonly properties
  37. Immutability @interface MyObject : NSObject <NSCopying, NSMutableCopying>
 @property (copy, readonly)

    NSArray *myArray;
 @property (copy, readonly) NSString *myString;
 @property (copy, readonly) NSData *myData;
 @property (copy, readonly) NSNumber *myNumber;
 @end
 
 @interface MyMutableObject : MyObject
 @property (copy, readwrite) NSArray *myArray;
 @property (copy, readwrite) NSString *myString;
 @property (copy, readwrite) NSData *myData;
 @property (copy, readwrite) NSNumber *myNumber;
 @end Example: Readonly properties
  38. Immutability @interface MyObject : NSObject <NSCopying, NSMutableCopying>
 @property (copy, readonly)

    NSArray *myArray;
 @property (copy, readonly) NSString *myString;
 @property (copy, readonly) NSData *myData;
 @property (copy, readonly) NSNumber *myNumber;
 @end
 
 @interface MyMutableObject : MyObject
 @property (copy, readwrite) NSArray *myArray;
 @property (copy, readwrite) NSString *myString;
 @property (copy, readwrite) NSData *myData;
 @property (copy, readwrite) NSNumber *myNumber;
 @end Example: Readonly properties
  39. Immutability @implementation MyObject {
 @protected
 NSArray *_myArray;
 NSString *_myString;
 NSData

    *_myData;
 NSNumber *_myNumber;
 }
 @synthesize myArray = _myArray, myString = _myString, myData = _myData,
 myNumber = _myNumber;
 @end
 
 @implementation MyMutableObject
 @dynamic myArray, myString, myData, myNumber;
 - (void)setMyArray:(NSArray *)myArray {
 _myArray = myArray.copy;
 }
 !// !!...
 @end Example: Readonly properties
  40. Immutability @implementation MyObject {
 @protected
 NSArray *_myArray;
 NSString *_myString;
 NSData

    *_myData;
 NSNumber *_myNumber;
 }
 @synthesize myArray = _myArray, myString = _myString, myData = _myData,
 myNumber = _myNumber;
 @end
 
 @implementation MyMutableObject
 @dynamic myArray, myString, myData, myNumber;
 - (void)setMyArray:(NSArray *)myArray {
 _myArray = myArray.copy;
 }
 !// !!...
 @end Example: Readonly properties
  41. Immutability @implementation MyObject {
 @protected
 NSArray *_myArray;
 NSString *_myString;
 NSData

    *_myData;
 NSNumber *_myNumber;
 }
 @synthesize myArray = _myArray, myString = _myString, myData = _myData,
 myNumber = _myNumber;
 @end
 
 @implementation MyMutableObject
 @dynamic myArray, myString, myData, myNumber;
 - (void)setMyArray:(NSArray *)myArray {
 _myArray = myArray.copy;
 }
 !// !!...
 @end Example: Readonly properties
  42. Immutability @implementation MyObject {
 @protected
 NSArray *_myArray;
 NSString *_myString;
 NSData

    *_myData;
 NSNumber *_myNumber;
 }
 @synthesize myArray = _myArray, myString = _myString, myData = _myData,
 myNumber = _myNumber;
 @end
 
 @implementation MyMutableObject
 @dynamic myArray, myString, myData, myNumber;
 - (void)setMyArray:(NSArray *)myArray {
 _myArray = myArray.copy;
 }
 !// !!...
 @end Example: Readonly properties
  43. Immutability @implementation MyObject {
 @protected
 NSArray *_myArray;
 NSString *_myString;
 NSData

    *_myData;
 NSNumber *_myNumber;
 }
 @synthesize myArray = _myArray, myString = _myString, myData = _myData,
 myNumber = _myNumber;
 @end
 
 @implementation MyMutableObject
 @dynamic myArray, myString, myData, myNumber;
 - (void)setMyArray:(NSArray *)myArray {
 _myArray = myArray.copy;
 }
 !// !!...
 @end Example: Readonly properties
  44. Immutability @implementation MyObject { !!... }
 
 - (instancetype)initWithMyObject:(MyObject *)myObject

    {
 self = [super init];
 if (self) {
 _myArray = myObject.myArray.copy;
 _myString = myObject.myString.copy;
 _myData = myObject.myData.copy;
 _myNumber = myObject.myNumber;
 return copy;
 }
 return self;
 }
 
 - (id)copyWithZone:(NSZone *)zone {
 if ([self isMemberOfClass:MyObject.class]) {
 return self;
 } else {
 return [[MyObject allocWithZone:zone] initWithMyObject:self];
 }
 }
 
 - (id)mutableCopyWithZone:(NSZone *)zone {
 return [[MyMutableObject allocWithZone:zone] initWithMyObject:self];
 }
 
 @end Example: Readonly properties
  45. Immutability @implementation MyObject { !!... }
 
 - (instancetype)initWithMyObject:(MyObject *)myObject

    {
 self = [super init];
 if (self) {
 _myArray = myObject.myArray.copy;
 _myString = myObject.myString.copy;
 _myData = myObject.myData.copy;
 _myNumber = myObject.myNumber;
 return copy;
 }
 return self;
 }
 
 - (id)copyWithZone:(NSZone *)zone {
 if ([self isMemberOfClass:MyObject.class]) {
 return self;
 } else {
 return [[MyObject allocWithZone:zone] initWithMyObject:self];
 }
 }
 
 - (id)mutableCopyWithZone:(NSZone *)zone {
 return [[MyMutableObject allocWithZone:zone] initWithMyObject:self];
 }
 
 @end Example: Readonly properties
  46. Immutability @implementation MyObject { !!... }
 
 - (instancetype)initWithMyObject:(MyObject *)myObject

    {
 self = [super init];
 if (self) {
 _myArray = myObject.myArray.copy;
 _myString = myObject.myString.copy;
 _myData = myObject.myData.copy;
 _myNumber = myObject.myNumber;
 return copy;
 }
 return self;
 }
 
 - (id)copyWithZone:(NSZone *)zone {
 if ([self isMemberOfClass:MyObject.class]) {
 return self;
 } else {
 return [[MyObject allocWithZone:zone] initWithMyObject:self];
 }
 }
 
 - (id)mutableCopyWithZone:(NSZone *)zone {
 return [[MyMutableObject allocWithZone:zone] initWithMyObject:self];
 }
 
 @end Example: Readonly properties
  47. Immutability @implementation MyObject { !!... }
 
 - (instancetype)initWithMyObject:(MyObject *)myObject

    {
 self = [super init];
 if (self) {
 _myArray = myObject.myArray.copy;
 _myString = myObject.myString.copy;
 _myData = myObject.myData.copy;
 _myNumber = myObject.myNumber;
 return copy;
 }
 return self;
 }
 
 - (id)copyWithZone:(NSZone *)zone {
 if ([self isMemberOfClass:MyObject.class]) {
 return self;
 } else {
 return [[MyObject allocWithZone:zone] initWithMyObject:self];
 }
 }
 
 - (id)mutableCopyWithZone:(NSZone *)zone {
 return [[MyMutableObject allocWithZone:zone] initWithMyObject:self];
 }
 
 @end Example: Readonly properties
  48. Immutability @implementation MyObject { !!... }
 
 - (instancetype)initWithMyObject:(MyObject *)myObject

    {
 self = [super init];
 if (self) {
 _myArray = myObject.myArray.copy;
 _myString = myObject.myString.copy;
 _myData = myObject.myData.copy;
 _myNumber = myObject.myNumber;
 return copy;
 }
 return self;
 }
 
 - (id)copyWithZone:(NSZone *)zone {
 if ([self isMemberOfClass:MyObject.class]) {
 return self;
 } else {
 return [[MyObject allocWithZone:zone] initWithMyObject:self];
 }
 }
 
 - (id)mutableCopyWithZone:(NSZone *)zone {
 return [[MyMutableObject allocWithZone:zone] initWithMyObject:self];
 }
 
 @end Example: Readonly properties
  49. Immutability @implementation MyObject { !!... }
 
 - (instancetype)initWithMyObject:(MyObject *)myObject

    {
 self = [super init];
 if (self) {
 _myArray = myObject.myArray.copy;
 _myString = myObject.myString.copy;
 _myData = myObject.myData.copy;
 _myNumber = myObject.myNumber;
 return copy;
 }
 return self;
 }
 
 - (id)copyWithZone:(NSZone *)zone {
 if ([self isMemberOfClass:MyObject.class]) {
 return self;
 } else {
 return [[MyObject allocWithZone:zone] initWithMyObject:self];
 }
 }
 
 - (id)mutableCopyWithZone:(NSZone *)zone {
 return [[MyMutableObject allocWithZone:zone] initWithMyObject:self];
 }
 
 @end Example: Readonly properties
  50. Immutability • When creating mutable / immutable pairs, remember to

    make all consumers copy the class! • Homework: Use the Objective-C runtime to automatically create the implementation of a mutable / immutable pair including support for NSCopying, NSMutableCopying, and NSCoding Example: Readonly properties
  51. Immutability • In Swift consider using structs. • var and

    let does exactly what you want Example: Readonly properties
  52. Thread Safety • Make APIs that are accessed from various

    places thread safe • Model objects • Data provider • Business Logic • Services • …
  53. Thread Safety • Use immutable objects where possible • Don’t

    pass around mutable objects • Public methods should be callable on any queue
  54. Thread Safety • An easy approach is: binding the internals

    of a class to a private queue • read concurrent, write serial
 dispatch_sync, dispatch_barrier_async • Consider prefixing internal methods: queue_ or barrier_
 Makes it almost a no brainer when refactoring
  55. Thread Safety • Make it very clear on what thread

    a callback arrives - (void)performRequest:(MyRequest *)request
 queue:(dispatch_queue_t)queue
 completionHandler:(void(^)(void))block;
  56. Thread Safety • If there is no explicit queue parameter

    and it is not absolutely clear on what queue a block is called: • As the maintainer of a public API: always call on the main queue • As the consumer of an API: always assume any queue • As the one writing the documentation: lie • “The block is called on an arbitrary queue”
  57. Documenting APIs • Header documentation is not about what the

    method does internally • Document only what you want to guarantee • What you want to guarantee and what you do might be distinct • Use the wording that Apple is using
  58. Documenting APIs • Guarantees in public APIs are hard to

    change • Try to prevent breaking changes whenever possible • But don’t cripple your API for that • Provider proper deprecation – most of the time it’s easy • Use semantic versioning Public APIs
  59. Conclusion • If you choose the right grouping and separation

    of concerns, the problems are by far not as big as they look like • Keeping your APIs small helps to keep the focus of this particular API • Naming things properly helps the consumer of the API and your future self when refactoring things • Immutability helps with designing a proper information flow and it automatically makes your code thread safe • Breaking things into pieces makes things like thread safety much easier
  60. –Me, now Great API design will always pay off. If

    your APIs are thread safe, immutable, small, simple, and separated you can refactor, optimize, change, iterate, test, and improve things very fast, easy and cost efficient because you don’t need to think – and thinking is where the shit hits the fan.