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

Maintaining a Library in a Swiftly Moving Ecosystem

Maintaining a Library in a Swiftly Moving Ecosystem

API evolution is tricky. You want to continually improve your library with new features and bug fixes, but at the same time, don't want to break your users' existing code. When your library is written in a language that moves as fast as Swift, the choices you need to make as a maintainer can be especially difficult. How do you decide when to drop support for an old language version? When can you begin incorporating exciting new features like the "some" keyword into your public API? In this talk, we will discuss best practices for API evolution, and how you can use Swift language features to gracefully change your library over time.

Kaitlin Mahar

November 01, 2019
Tweet

More Decks by Kaitlin Mahar

Other Decks in Programming

Transcript

  1. Maintaining a Library in a
    Swiftly Moving Ecosystem
    Kaitlin Mahar
    @k_ _mahar

    View Slide

  2. About me
    •Software engineer at MongoDB since 2017
    •Member of the Drivers team, which maintains the official client
    libraries for using MongoDB from many programming languages
    •Lead developer and maintainer of our Swift driver
    •Just pitched to SSWG! tinyurl.com/mongodb-pitch
    @k_ _mahar

    View Slide

  3. What does “maintaining” mean?
    •Adding features
    •Removing features
    •Modifying your APIs
    •Fixing bugs
    •Addressing security vulnerabilities
    •Refactoring/internal changes
    •Adapting to changes in the language/ecosystem

    View Slide

  4. Change is an inevitable part of library
    maintenance.
    How can we introduce changes without
    inflicting pain on our users?

    View Slide

  5. 1. Change your API
    gradually.

    View Slide

  6. public struct Cat {
    /// The cat's name.
    public let name: String
    /// Initialize a new cat.
    public init(name: String)
    /// Feed the cat one can of food.
    public func feed()
    }

    View Slide

  7. let chester = Cat(name: "Chester")
    chester.feed()
    let roscoe = Cat(name: "Roscoe")
    roscoe.feed()

    View Slide

  8. We demand more!

    View Slide

  9. How can we support
    giving cats snacks?

    View Slide

  10. public struct Cat {
    /// ...
    /// Feed the cat `cans` cans of food.
    public func feed(cans: Double)
    }
    chester.feed(cans: 0.25)

    View Slide

  11. But… this is a breaking change.
    let chester = Cat(name: "Chester")
    chester.feed()
    error: missing argument for parameter 'cans' in call
    c.feed()
    ^
    cans: <#Double#>

    View Slide

  12. Can we make this change in a
    more gradual manner?

    View Slide

  13. 1a. Use default values for new
    method parameters when possible.
    public struct Cat {
    /// ...
    /// Feed the cat `cans` cans of food.
    public func feed(cans: Double = 1.0)
    }

    View Slide

  14. ✅ Allows us to preserve existing behavior
    /// Feed the cat one can of food.
    public func feed()
    /// Feed the cat `cans` cans of food.
    public func feed(cans: Double = 1.0)
    before
    after
    1a. Use default values for new
    method parameters when possible.

    View Slide

  15. ✅ Users’ old code will still compile.
    chester.feed()
    1a. Use default values for new
    method parameters when possible.

    View Slide

  16. ✅ Users who want this feature can start using it when
    we release it!
    chester.feed(cans: 0.25)
    1a. Use default values for new
    method parameters when possible.

    View Slide

  17. User: “Not all cans of cat food
    are the same size!”

    View Slide

  18. public struct Cat {
    /// ...
    /// Feed the cat `ounces` ounces of food.
    public func feed(ounces: Double = 6)
    }
    chester.feed(ounces: 2)

    View Slide

  19. Still a breaking change!
    let chester = Cat(name: "Chester")
    chester.feed(cans: 0.25)
    error: incorrect argument label in call (have
    'cans:', expected 'ounces:')
    c.feed(cans: 0.25)
    ^~~~~
    ounces

    View Slide

  20. public struct Cat {
    /// ...
    /// Feed the cat `cans` cans of food.
    @available(*, deprecated, message: "Use feed(ounces:) instead.")
    public func feed(cans: Double = 1.0)
    /// Feed the cat `ounces` ounces of food.
    public func feed(ounces: Double = 6)
    }
    1b. Deprecate features you
    intend to remove later.

    View Slide

  21. 1b. Deprecate features you
    intend to remove later.
    warning: 'feed(cans:)' is deprecated: Use feed(ounces:) instead.
    chester.feed(cans: 1)
    ^
    Compiler:
    User:
    chester.feed(cans: 0.25)

    View Slide

  22. @available attribute
    See also: tinyurl.com/swift-attributes
    @available(swift, deprecated: 4.2, message: "")
    @available(macOS, introduced: 10.14)
    @available(*, unavailable, renamed: "newName")

    View Slide

  23. See also: tinyurl.com/swift-attributes
    Enables you to use the compiler to
    communicate information to your users.
    @available attribute

    View Slide

  24. When do you actually remove
    the deprecated feature?

    View Slide

  25. 2. Use Semantic Versioning.

    View Slide

  26. Semantic Versioning (“SemVer”)
    A versioning scheme where the differences
    between two version numbers convey
    information about what has changed about the
    library between the corresponding releases.

    View Slide

  27. x.y.z
    major minor patch

    View Slide

  28. 1.0.0
    2.0.0
    1.1.0
    1.0.1
    major
    minor
    patch

    View Slide

  29. type of change major minor patch
    backwards-compatible
    bug fix
    ✅ ✅ ✅
    new backwards-
    compatible functionality
    ✅ ✅
    deprecated
    functionality
    ✅ ✅
    substantial internal
    changes
    ✅ ✅
    backwards-
    incompatible changes

    View Slide

  30. Why use it?
    It serves as a contract between maintainers
    and users about what version numbers
    actually mean and what users can expect to
    change when they upgrade.
    See also: semver.org

    View Slide

  31. Pre-1.0: no rules! (well, sort of)
    •Major version zero (0.y.z) is for initial development
    •Tag 1.0 once your API has stablilized
    •If you are pre-1.0 for a while, consider minor
    version bumps for breaking changes

    View Slide

  32. Use with Swift Package Manager
    .package(url: "git-url-here", requirement)
    exactly 1.0.0
    1.0.0..<1.1.0
    1.0.0..<2.0.0
    Requirement Satisfied by
    .exact("1.0.0")
    .upToNextMinor(from: "1.0.0")
    .upToNextMajor(from: "1.0.0")

    View Slide

  33. When should you remove
    a deprecated feature?

    View Slide

  34. Removing a feature
    1. Deprecate it in a minor version release.*
    2. Remove it no sooner than your next major
    version release.
    *you may skip step 1, but if so consider
    marking unavailable in step 2

    View Slide

  35. 3. Add to your API
    conservatively.

    View Slide

  36. 3a. If it can already be done simply,
    don’t add another way to do it.
    public func feedCats(_ cats: [Cat])
    feedCats(myCats)
    myCats.forEach { $0.feed() }
    • Confusing for users. Which should they use?
    • Greater maintenance overhead

    View Slide

  37. 3b. Do add helpers to eliminate user errors and
    boilerplate on commonly-used code paths.
    MongoDB "insert" command
    try db.runCommand(
    [
    "insert": "cats",
    "documents": [
    ["name": "Chester"],
    ["name": "Roscoe"]
    ],
    "writeConcern": ["w": "majority"]
    ]
    )
    var opts = InsertManyOptions()
    opts.writeConcern =
    try WriteConcern(w: .majority)
    let cats = db.collection("cats")
    try cats.insertMany(
    [
    ["name": "Chester"],
    ["name": "Roscoe"]
    ],
    options: opts
    )

    View Slide

  38. – A. Jesse Jiryu Davis
    “Features are like children: conceived in a
    moment of passion, they must be
    supported for years to come.”
    tinyurl.com/jesses-talk

    View Slide

  39. 3c. When in doubt, leave it out.
    • By default, use internal / fileprivate / private.
    • Don't expose implementation details the user doesn't
    need access to.
    • It’s much easier to add it later than to remove it later.

    View Slide

  40. 4. Be clear about what you
    support, and what you don’t.

    View Slide

  41. Swift 5.0 Swift 5.1
    MacOS 10.15
    ✅ ✅
    Ubuntu 18.04
    ✅ ✅
    If you say you support it, you need to test it!

    View Slide

  42. What should you support?
    • SSWG graduation requirements
    • CI setup for two latest Swift.org recommended versions of Swift
    • CI setup for two latest versions of Swift.org recommended Linux distributions
    • MacOS and Linux tests
    • Support new GA versions of Swift within 30 days
    • Test early!
    • You may decide to support more or less, depending on you and your users’ needs

    View Slide

  43. 5. Help users with
    words, too.

    View Slide

  44. 5a. Publish release notes.
    • Describe what has changed in a release
    • When appropriate, provide context on why it changed
    • Highlight what users upgrading to this release should know
    • Include links to relevant GitHub issues, pull requests, JIRA tickets, etc.
    • Helpful for users investigating any new warnings or failures that they encounter
    after upgrading

    View Slide

  45. 5b. Write migration guides for
    significant API changes.
    •Explain rationale behind the changes
    •Include examples of how to accomplish common tasks
    “the new way”
    •If it’s possible to automate any of the upgrade process
    (e.g. using a script) include instructions

    View Slide

  46. 5c. Keep existing documentation
    and examples up-to-date.
    •Check as part of your release process that any
    sample repos/projects still compile

    View Slide

  47. In summary…
    • Add to your API conservatively
    • Introduce changes gradually
    • Use a combination of semantic versioning, Swift features, and
    good documentation to help users through the process

    View Slide

  48. Thanks!
    @k_ _mahar

    View Slide