Swift Eye for the Stringly Typed API

Ca5ae8aa8c34a42aacdd0ff5bc2592de?s=47 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.

Ca5ae8aa8c34a42aacdd0ff5bc2592de?s=128

Andyy Hope

September 02, 2016
Tweet

Transcript

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

    Twitter: @andyyhope
  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
  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)
  4. Are there downsides to stringly typed APIs?

  5. Are there downsides to stringly typed APIs Yep

  6. Stringly typed • Typos let notification = "userAlertNotificaton" • Side

    effects let image = UIImage(named: "Charmandr") • Collisions let keyA = "isUserLoggedIn" let keyB = "isUserLoggedIn" keyA == keyB
  7. Stringly typed • Runtime crashes tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "CellIdentifer") ... let

    cell: UITableViewCell = tableView .dequeueReusableCellWithIdentifier("CelIdentifer")
  8. Stringly typed • Unicode artifacts "Account.!isUserLoggedIn"

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

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

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

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

    UserDefaults ...
  13. UserDefaults

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

    • Settings • Arrays • Booleans
  15. None
  16. UserDefaults • Used to store small persistent pieces of information:

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

    • Settings • Arrays • Booleans • (AKA) Diet CoreData • Stringly typed APIs
  18. None
  19. Swift Eye for UserDefaults API

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

    2016
  21. Swift 3: Rename Old NSUserDefaults New UserDefaults

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

  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")
  24. Swift 3: Get API Old .boolForKey("isUserLoggedIn") .integerForKey("pokeballCount") .objectForKey("pikachu") New .bool(forKey:

    "isUserLoggedIn") .integer(forKey: "pokeballCount") .object(forKey: "pikachu")
  25. Swift 3: Synchronize Old NSUserDefaults.standardUserDefaults().synchronize() New // deprecated !

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

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

  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)
  29. Fix: Unify struct Constants { struct Keys { // Account

    static let isUserLoggedIn = "isUserLoggedIn" // Onboarding ... } }
  30. // Set UserDefaults.standard .set(true, forKey: Constants.Keys.isUserLoggedIn) // Get UserDefaults.standard .bool(forKey:

    Constants.Keys.isUserLoggedIn)
  31. Fix: Unify with context struct Constants { struct Keys {

    struct Account { static let isUserLoggedIn = "isUserLoggedIn" } } }
  32. // Set UserDefaults.standard .set(true, forKey: Constants.Keys.Account.isUserLoggedIn) // Get UserDefaults.standard .bool(forKey:

    Constants.Keys.Account.isUserLoggedIn)
  33. Fix: Unify Constants.swift struct Account { static let isUserLoggedIn =

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

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

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

    Constants.Keys.Account.isUserLoggedIn.rawValue)
  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
  38. // Set UserDefaults.standard .set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue) // Get UserDefaults.standard .bool(forKey:

    Constants.Keys.Account.isUserLoggedIn.rawValue)
  39. Hurts our brains

  40. Extend UserDefaults

  41. None
  42. None
  43. None
  44. Extend UserDefaults

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

    .bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
  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)
  47. Protocols

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

  49. None
  50. "For every protocol, there is an equal and corresponding protocol

    extension" — Crusty's Third Law
  51. Protocol: BoolDefaultSettable protocol BoolDefaultSettable { associatedtype BoolKey : RawRepresentable }

    extension BoolDefaultSettable where BoolKey.RawValue == String { }
  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) } }
  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) } }
  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) } }
  55. Protocol: DefaultSettable Family protocol IntegerDefaultSettable { ... } protocol DoubleDefaultSettable

    { ... } protocol FloatDefaultSettable { ... } protocol ObjectDefaultSettable { ... } protocol URLDefaultSettable { ... }
  56. UserDefaults extension extension UserDefaults : BoolDefaultSettable { enum BoolKey :

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

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

    String { case isUserLoggedIn } } ... Account.set(true, forKey: .isUserLoggedIn) Account.bool(forKey: .isUserLoggedIn)
  59. Problem: Collisions Account.BoolKey.isUserLoggedIn.rawValue // key: "isUserLoggedIn" UserDefaults.BoolKey.isUserLoggedIn.rawValue // key: "isUserLoggedIn"

  60. Protocol: KeyNamespaceable protocol KeyNamespaceable { func namespaced<T: RawRepresentable>(_ key: T)

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

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

    } extension KeyNamespaceable { func namespaced<T: RawRepresentable>(_ key: T) -> String { return "\(Self.self).\(key.rawValue)" } } // key: "UserDefaults.isUserLoggedIn"
  63. Protocol: BoolDefaultSettable protocol BoolDefaultSettable { associatedtype BoolKey : RawRepresentable }

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

    RawRepresentable }
  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) } }
  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) } }
  67. Collision free UserDefaults.set(true, forKey: .isUserLoggedIn) // key: "UserDefaults.isUserLoggedIn" Account.set(true, forKey:

    .isUserLoggedIn) // key: "Account.isUserLoggedIn"
  68. What about Uniformity?

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

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

    : BoolDefaultSettable { ... } struct Constants { ... } ... Account.set(true, forKey: .isUserLoggedIn) Account.bool(forKey: .isUserLoggedIn)
  71. What about Context?

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

    } struct Onboarding : BoolDefaultSettable { ... } }
  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)
  74. None
  75. So what have we learned?

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

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

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

    bad • Plenty of room for improvement • Grouping constants can assist uniformity
  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
  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
  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
  82. SWIFT EYE FOR THE STRINGLY TYPED API Thank You Medium:

    Andyy Hope Twitter: @andyyhope