Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

1. Change your API gradually.

Slide 6

Slide 6 text

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() }

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

We demand more!

Slide 9

Slide 9 text

How can we support giving cats snacks?

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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#>

Slide 12

Slide 12 text

Can we make this change in a more gradual manner?

Slide 13

Slide 13 text

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) }

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

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)

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

When do you actually remove the deprecated feature?

Slide 25

Slide 25 text

2. Use Semantic Versioning.

Slide 26

Slide 26 text

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.

Slide 27

Slide 27 text

x.y.z major minor patch

Slide 28

Slide 28 text

1.0.0 2.0.0 1.1.0 1.0.1 major minor patch

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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")

Slide 33

Slide 33 text

When should you remove a deprecated feature?

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

3. Add to your API conservatively.

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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 )

Slide 38

Slide 38 text

– 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

Slide 39

Slide 39 text

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.

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

5. Help users with words, too.

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Thanks! @k_ _mahar