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. 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
  2. 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
  3. Change is an inevitable part of library maintenance. How can

    we introduce changes without inflicting pain on our users?
  4. 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() }
  5. public struct Cat { /// ... /// Feed the cat

    `cans` cans of food. public func feed(cans: Double) } chester.feed(cans: 0.25)
  6. 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#>
  7. 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) }
  8. ✅ 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.
  9. ✅ Users’ old code will still compile. chester.feed() 1a. Use

    default values for new method parameters when possible.
  10. ✅ 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.
  11. public struct Cat { /// ... /// Feed the cat

    `ounces` ounces of food. public func feed(ounces: Double = 6) } chester.feed(ounces: 2)
  12. 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
  13. 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.
  14. 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)
  15. @available attribute See also: tinyurl.com/swift-attributes @available(swift, deprecated: 4.2, message: "")

    @available(macOS, introduced: 10.14) @available(*, unavailable, renamed: "newName")
  16. See also: tinyurl.com/swift-attributes Enables you to use the compiler to

    communicate information to your users. @available attribute
  17. 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.
  18. type of change major minor patch backwards-compatible bug fix ✅

    ✅ ✅ new backwards- compatible functionality ✅ ✅ deprecated functionality ✅ ✅ substantial internal changes ✅ ✅ backwards- incompatible changes ✅
  19. 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
  20. 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
  21. 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")
  22. 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
  23. 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
  24. 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 )
  25. – 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
  26. 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.
  27. Swift 5.0 Swift 5.1 MacOS 10.15 ✅ ✅ Ubuntu 18.04

    ✅ ✅ If you say you support it, you need to test it!
  28. 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
  29. 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
  30. 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
  31. 5c. Keep existing documentation and examples up-to-date. •Check as part

    of your release process that any sample repos/projects still compile
  32. 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