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

Swift Eye for the Stringly Typed API

Andyy Hope
September 02, 2016

Swift Eye for the Stringly Typed API

This was the talk I gave at try! Swift NYC on September 2nd, 2016.

Andyy Hope

September 02, 2016
Tweet

More Decks by Andyy Hope

Other Decks in Programming

Transcript

  1. SWIFT EYE
    FOR THE STRINGLY TYPED API
    Medium: Andyy Hope
    Twitter: @andyyhope

    View Slide

  2. Stringly Typed?
    "A riff on "strongly typed". Used to describe an implementation that
    needlessly relies on strings when programmer & refactor friendly options
    are available."
    — Internet

    View Slide

  3. Stringly Typed?
    "A riff on "strongly typed". Used to describe an implementation that
    needlessly relies on strings when programmer & refactor friendly options
    are available."
    — Internet
    // Stringly typed
    let image = UIImage(named: "PalletTown")
    // Strongly typed
    let tableView = UITableView(style: .Plain)

    View Slide

  4. Are there downsides to
    stringly typed APIs?

    View Slide

  5. Are there downsides to
    stringly typed APIs
    Yep

    View Slide

  6. Stringly typed
    • Typos
    let notification = "userAlertNotificaton"
    • Side effects
    let image = UIImage(named: "Charmandr")
    • Collisions
    let keyA = "isUserLoggedIn"
    let keyB = "isUserLoggedIn"
    keyA == keyB

    View Slide

  7. Stringly typed
    • Runtime crashes
    tableView.registerClass(UITableViewCell.self,
    forCellReuseIdentifier: "CellIdentifer")
    ...
    let cell: UITableViewCell = tableView
    .dequeueReusableCellWithIdentifier("CelIdentifer")

    View Slide

  8. Stringly typed
    • Unicode artifacts
    "Account.!isUserLoggedIn"

    View Slide

  9. Stringly typed
    • Disorderly
    • Title cased
    "IsUserLoggedIn"
    • Camel cased
    "isUserLoggedIn"

    View Slide

  10. Stringly typed
    • Disorderly
    • Prefixes
    "isUserLoggedIn"
    "Account.isUserLoggedIn"
    "com.andyyhope.Account.isUserLoggedIn"

    View Slide

  11. Stringly typed
    • Disorderly
    • Naming
    "isUserBlocked"
    "userBlocked"
    "hasUserBeenBlocked"

    View Slide

  12. They're
    everywhere...
    UIImage
    UITableView
    UICollectionView
    UIStoryboard
    UISegue
    URL
    DateFormatter
    NotificationCenter
    UserDefaults
    ...

    View Slide

  13. UserDefaults

    View Slide

  14. UserDefaults
    • Used to store small persistent pieces of information:
    • Settings
    • Arrays
    • Booleans

    View Slide

  15. View Slide

  16. UserDefaults
    • Used to store small persistent pieces of
    information:
    • Settings
    • Arrays
    • Booleans
    • (AKA) Diet CoreData

    View Slide

  17. UserDefaults
    • Used to store small persistent pieces of
    information:
    • Settings
    • Arrays
    • Booleans
    • (AKA) Diet CoreData
    • Stringly typed APIs

    View Slide

  18. View Slide

  19. Swift Eye for
    UserDefaults API

    View Slide

  20. Swift 3
    UserDefaults Recap
    Xcode 8 Beta 6. August 15, 2016

    View Slide

  21. Swift 3: Rename
    Old
    NSUserDefaults
    New
    UserDefaults

    View Slide

  22. Swift 3: Instance
    Old
    NSUserDefaults.standardUserDefaults()
    New
    UserDefaults.standard

    View Slide

  23. Swift 3: Set API
    Old
    .setBool(true, forKey: "isUserLoggedIn")
    .setInt(1, forKey: "pokeballCount")
    .setObject(pikachu, forKey: "pikachu")
    New
    .set(true, forKey: "isUserLoggedIn")
    .set(1, forKey: "pokeballCount")
    .set(pikachu, forKey: "pikachu")

    View Slide

  24. Swift 3: Get API
    Old
    .boolForKey("isUserLoggedIn")
    .integerForKey("pokeballCount")
    .objectForKey("pikachu")
    New
    .bool(forKey: "isUserLoggedIn")
    .integer(forKey: "pokeballCount")
    .object(forKey: "pikachu")

    View Slide

  25. Swift 3: Synchronize
    Old
    NSUserDefaults.standardUserDefaults().synchronize()
    New
    // deprecated !

    View Slide

  26. UserDefaults
    Stringly typed API
    // Setter
    UserDefaults.standard.set(true, forKey: "isUserLoggedIn")
    // Getter
    UserDefaults.standard.bool(forKey: "isUserLoggedIn")

    View Slide

  27. Fix: Constants
    UserDefaults.standard.set(true, forKey: "isUserLoggedIn")
    UserDefaults.standard.bool(forKey: "isUserLoggedIn")

    View Slide

  28. Fix: Constants
    // UserDefaults.standard.set(true, forKey: "isUserLoggedIn")
    // UserDefaults.standard.bool(forKey: "isUserLoggedIn")
    ...
    let isUserLoggedInKey = "isUserLoggedIn"
    UserDefaults.standard.set(true, forKey: isUserLoggedInKey)
    UserDefaults.standard.bool(forKey: isUserLoggedInKey)

    View Slide

  29. Fix: Unify
    struct Constants {
    struct Keys {
    // Account
    static let isUserLoggedIn = "isUserLoggedIn"
    // Onboarding
    ...
    }
    }

    View Slide

  30. // Set
    UserDefaults.standard
    .set(true, forKey: Constants.Keys.isUserLoggedIn)
    // Get
    UserDefaults.standard
    .bool(forKey: Constants.Keys.isUserLoggedIn)

    View Slide

  31. Fix: Unify with context
    struct Constants {
    struct Keys {
    struct Account {
    static let isUserLoggedIn = "isUserLoggedIn"
    }
    }
    }

    View Slide

  32. // Set
    UserDefaults.standard
    .set(true, forKey: Constants.Keys.Account.isUserLoggedIn)
    // Get
    UserDefaults.standard
    .bool(forKey: Constants.Keys.Account.isUserLoggedIn)

    View Slide

  33. Fix: Unify
    Constants.swift
    struct Account {
    static let isUserLoggedIn = "isUserLoggedIn"
    }

    View Slide

  34. Fix: Unify with enums
    Constants.swift
    enum Account : String {
    case isUserLoggedIn
    }

    View Slide

  35. struct Constants {
    struct Keys {
    enum Account : String {
    case isUserLoggedIn
    }
    }
    }

    View Slide

  36. // Set
    UserDefaults.standard
    .set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
    // Get
    UserDefaults.standard
    .bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)

    View Slide

  37. Recap
    // Strings
    let key = "isUserLoggedIn"
    // Constant & Grouped
    let key = Constants.isUserLoggedIn
    // Context
    let key = Constants.Keys.Account.isUserLoggedIn
    // Safety
    let key = Constants.Keys.Account.isUserLoggedIn.rawValue

    View Slide

  38. // Set
    UserDefaults.standard
    .set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
    // Get
    UserDefaults.standard
    .bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)

    View Slide

  39. Hurts
    our brains

    View Slide

  40. Extend
    UserDefaults

    View Slide

  41. View Slide

  42. View Slide

  43. View Slide

  44. Extend
    UserDefaults

    View Slide

  45. tl;dw
    // Set
    UserDefaults.standard
    .set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
    // Get
    UserDefaults.standard
    .bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)

    View Slide

  46. tl;dw
    // Set
    // UserDefaults.standard
    // .set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
    UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
    // Get
    // UserDefaults.standard
    // .bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
    UserDefaults.standard.bool(forKey: .isUserLoggedIn)

    View Slide

  47. Protocols

    View Slide

  48. Protocol: BoolDefaultSettable
    protocol BoolDefaultSettable {
    associatedtype BoolKey : RawRepresentable
    }

    View Slide

  49. View Slide

  50. "For every protocol, there is an
    equal and corresponding protocol
    extension"
    — Crusty's Third Law

    View Slide

  51. Protocol: BoolDefaultSettable
    protocol BoolDefaultSettable {
    associatedtype BoolKey : RawRepresentable
    }
    extension BoolDefaultSettable where BoolKey.RawValue == String {
    }

    View Slide

  52. Protocol: BoolDefaultSettable
    extension BoolDefaultSettable where BoolKey.RawValue == String {
    // Setter
    func set(_ value: Bool, forKey key: BoolKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
    }
    }

    View Slide

  53. Protocol: BoolDefaultSettable
    extension BoolDefaultSettable where BoolKey.RawValue == String {
    // Getter
    func bool(forKey key: BoolKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
    }
    }

    View Slide

  54. protocol BoolDefaultSettable {
    associatedtype BoolKey : RawRepresentable
    }
    extension BoolDefaultSettable where BoolKey.RawValue == String {
    func set(_ value: Bool, forKey key: BoolKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
    }
    func bool(forKey key: BoolKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
    }
    }

    View Slide

  55. Protocol: DefaultSettable Family
    protocol IntegerDefaultSettable { ... }
    protocol DoubleDefaultSettable { ... }
    protocol FloatDefaultSettable { ... }
    protocol ObjectDefaultSettable { ... }
    protocol URLDefaultSettable { ... }

    View Slide

  56. UserDefaults extension
    extension UserDefaults : BoolDefaultSettable {
    enum BoolKey : String {
    case isUserLoggedIn
    }
    }

    View Slide

  57. UserDefaults extension
    extension UserDefaults : BoolDefaultSettable {
    enum BoolKey : String {
    case isUserLoggedIn
    }
    }
    ...
    UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
    UserDefaults.standard.bool(forKey: .isUserLoggedIn)

    View Slide

  58. Account extension
    extension Account : BoolDefaultSettable {
    enum BoolKey : String {
    case isUserLoggedIn
    }
    }
    ...
    Account.set(true, forKey: .isUserLoggedIn)
    Account.bool(forKey: .isUserLoggedIn)

    View Slide

  59. Problem: Collisions
    Account.BoolKey.isUserLoggedIn.rawValue
    // key: "isUserLoggedIn"
    UserDefaults.BoolKey.isUserLoggedIn.rawValue
    // key: "isUserLoggedIn"

    View Slide

  60. Protocol: KeyNamespaceable
    protocol KeyNamespaceable {
    func namespaced(_ key: T) -> String
    }

    View Slide

  61. Protocol: KeyNamespaceable
    protocol KeyNamespaceable {
    func namespaced(_ key: T) -> String
    }
    extension KeyNamespaceable {
    func namespaced(_ key: T) -> String {
    return "\(Self.self).\(key.rawValue)"
    }
    }

    View Slide

  62. protocol KeyNamespaceable {
    func namespaced(_ key: T) -> String
    }
    extension KeyNamespaceable {
    func namespaced(_ key: T) -> String {
    return "\(Self.self).\(key.rawValue)"
    }
    }
    // key: "UserDefaults.isUserLoggedIn"

    View Slide

  63. Protocol: BoolDefaultSettable
    protocol BoolDefaultSettable {
    associatedtype BoolKey : RawRepresentable
    }

    View Slide

  64. Protocol: BoolDefaultSettable
    protocol BoolDefaultSettable : KeyNamespaceable {
    associatedtype BoolKey : RawRepresentable
    }

    View Slide

  65. extension BoolDefaultSettable where BoolKey.RawValue == String {
    func set(_ value: Bool, forKey key: BoolKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
    }
    func bool(forKey key: BoolKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
    }
    }

    View Slide

  66. extension BoolDefaultSettable where BoolKey.RawValue == String {
    func set(_ value: Bool, forKey key: BoolKey) {
    let key = namespaced(key)
    UserDefaults.standard.set(value, forKey: key)
    }
    func bool(forKey key: BoolKey) -> Bool {
    let key = namespaced(key)
    return UserDefaults.standard.bool(forKey: key)
    }
    }

    View Slide

  67. Collision free
    UserDefaults.set(true, forKey: .isUserLoggedIn)
    // key: "UserDefaults.isUserLoggedIn"
    Account.set(true, forKey: .isUserLoggedIn)
    // key: "Account.isUserLoggedIn"

    View Slide

  68. What about
    Uniformity?

    View Slide

  69. Constants.swift
    extension Account : BoolDefaultSettable { ... }
    extension Onboarding : BoolDefaultSettable { ... }
    struct Constants { ... }

    View Slide

  70. Constants.swift
    extension Account : BoolDefaultSettable { ... }
    extension Onboarding : BoolDefaultSettable { ... }
    struct Constants { ... }
    ...
    Account.set(true, forKey: .isUserLoggedIn)
    Account.bool(forKey: .isUserLoggedIn)

    View Slide

  71. What about
    Context?

    View Slide

  72. Defaults.swift
    struct Defaults {
    struct Account : BoolDefaultSettable { ... }
    struct Onboarding : BoolDefaultSettable { ... }
    }

    View Slide

  73. Defaults.swift
    struct Defaults {
    struct Account : BoolDefaultSettable { ... }
    struct Onboarding : BoolDefaultSettable { ... }
    }
    ...
    Defaults.Account.set(true, forKey: .isUserLoggedIn)
    Defaults.Onboarding.set(true, forKey: .isUserOnboarded)
    Defaults.Account.bool(forKey: .isUserLoggedIn)
    Defaults.Onboarding.bool(forKey: .isUserOnboarded)

    View Slide

  74. View Slide

  75. So what have we
    learned?

    View Slide

  76. So what have we learned?
    • Stringly typed API's are bad

    View Slide

  77. So what have we learned?
    • Stringly typed API's are bad
    • Plenty of room for improvement

    View Slide

  78. So what have we learned?
    • Stringly typed API's are bad
    • Plenty of room for improvement
    • Grouping constants can assist uniformity

    View Slide

  79. So what have we learned?
    • Stringly typed API's are bad
    • Plenty of room for improvement
    • Grouping constants can assist uniformity
    • Namespacing gives context

    View Slide

  80. So what have we learned?
    • Stringly typed API's are bad
    • Plenty of room for improvement
    • Grouping constants can assist uniformity
    • Namespacing gives context
    • Protocols are (so) hot right now

    View Slide

  81. So what have we learned?
    • Stringly typed API's are bad
    • Plenty of room for improvement
    • Grouping constants can assist uniformity
    • Namespacing gives context
    • Protocols are (so) hot right now
    • Rethink how we interact with APIs

    View Slide

  82. SWIFT EYE
    FOR THE STRINGLY TYPED API
    Thank You
    Medium: Andyy Hope
    Twitter: @andyyhope

    View Slide