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. Designing APIs
    for internal and external use

    View Slide

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

    View Slide

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

    View Slide

  4. Who am I to talk about that?
    • By no means an expert
    • Some experience on API design
    • PSPDFKit
    • ViewDeck
    • Internal frameworks

    View Slide

  5. 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

    View Slide

  6. What is an API?
    UIWindow
    UIView UIViewController
    UIWindow

    View Slide

  7. Structure of a good API
    • Structured into parts
    • Single purpose per part
    • Clear purpose
    UIView UIViewController
    UIWindow

    View Slide

  8. 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

    View Slide

  9. Designing APIs

    View Slide

  10. General Considerations

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. Consider your audience

    View Slide

  14. 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?

    View Slide

  15. 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…

    View Slide

  16. 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

    View Slide

  17. Methods and Classes

    View Slide

  18. 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

    View Slide

  19. Methods and Classes

    View Slide

  20. Methods and Classes
    PSPDFRenderRequest PSPDFRenderTask PSPDFRenderQueue
    PSPDF
    Cache
    Example: Rendering in PSPDFKit

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. 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

    View Slide

  24. 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

    View Slide

  25. Methods and Classes
    • MyObject.h
    • MyObject+Internal.h
    • MyObject+Protected.h
    • MyObject+Private.h
    • Use the appropriate visibility attributes in Swift

    View Slide

  26. 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

    View Slide

  27. Nullability

    View Slide

  28. 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.

    View Slide

  29. Enforce your API contracts

    View Slide

  30. Enforce your API contracts
    • Assert, assert, assert
    • Use NSAssert, NSParameterAssert
    • nullability, classes, container counts

    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

  33. Immutability

    View Slide

  34. Immutability
    • Prefer immutability whenever possible
    • Thread safe due to its nature
    • Explicit information flow
    • Less complexity

    View Slide

  35. 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

    View Slide

  36. Immutability
    @interface MyObject : NSObject
    @property (readonly) NSArray *myArray;
    @end
    Example: Internally mutable

    View Slide

  37. Immutability
    @interface MyObject : NSObject
    @property (readonly) NSArray *myArray;
    @end
    Example: Internally mutable

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

  42. Immutability
    @interface MyObject : NSObject


    @property (copy) NSArray *myArray;

    - (instancetype)initWithMyArray:(NSArray *)myArray;


    @end
    Example: Copy properties

    View Slide

  43. Immutability
    @interface MyObject : NSObject


    @property (copy) NSArray *myArray;

    - (instancetype)initWithMyArray:(NSArray *)myArray;


    @end
    Example: Copy properties

    View Slide

  44. Immutability
    @implementation MyObject


    - (instancetype)initWithMyArray:(NSArray *)myArray {

    self = [super init];

    if (self) {

    _myArray = myArray.copy;

    }

    return self;

    }


    @end
    Example: Copy properties

    View Slide

  45. Immutability
    @implementation MyObject


    - (instancetype)initWithMyArray:(NSArray *)myArray {

    self = [super init];

    if (self) {

    _myArray = myArray.copy;

    }

    return self;

    }


    @end
    Example: Copy properties

    View Slide

  46. 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

    View Slide

  47. 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

    View Slide

  48. Immutability
    @interface MyObject : NSObject 

    @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

    View Slide

  49. Immutability
    @interface MyObject : NSObject 

    @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

    View Slide

  50. Immutability
    @interface MyObject : NSObject 

    @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

    View Slide

  51. Immutability
    @interface MyObject : NSObject 

    @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

    View Slide

  52. 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

    View Slide

  53. 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

    View Slide

  54. 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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

  58. 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

    View Slide

  59. 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

    View Slide

  60. 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

    View Slide

  61. 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

    View Slide

  62. 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

    View Slide

  63. 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

    View Slide

  64. Immutability
    • In Swift consider using structs.
    • var and let does exactly what you want
    Example: Readonly properties

    View Slide

  65. Thread Safety

    View Slide

  66. Thread Safety
    • Make APIs that are accessed from various places thread safe
    • Model objects
    • Data provider
    • Business Logic
    • Services
    • …

    View Slide

  67. Thread Safety
    • Use immutable objects where possible
    • Don’t pass around mutable objects
    • Public methods should be callable on any queue

    View Slide

  68. 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

    View Slide

  69. 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;

    View Slide

  70. 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”

    View Slide

  71. Documenting APIs

    View Slide

  72. 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

    View Slide

  73. 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

    View Slide

  74. Conclusion

    View Slide

  75. 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

    View Slide

  76. –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.

    View Slide

  77. Thank you
    Michael Ochs
    @_mochs
    https://pspdfkit.com/blog/

    View Slide