Slide 1

Slide 1 text

ADAPTING TO CHANGE DESIGNING FOR MODULARITY AND MAINTAINABILITY IN SWIFT Jesse Squires jessesquires.com • @jesse_squires

Slide 2

Slide 2 text

Where I work Construction productivity so!ware It's like GitHub and Xcode for construction

Slide 3

Slide 3 text

CONSTRUCTION ! AND SOFTWARE ! HAVE MANY THINGS IN COMMON

Slide 4

Slide 4 text

CONNECTING MODEL AND UI LAYERS

Slide 5

Slide 5 text

FORGETTING TO IMPLEMENT ACCESSIBILITY FEATURES

Slide 6

Slide 6 text

CLIENT-SEVER COMMUNICATION MANY DIFFERENT TEAMS WORKING TOGETHER

Slide 7

Slide 7 text

WHEN A NEW FEATURE HAS USER PRIVACY OR USABILITY ISSUES

Slide 8

Slide 8 text

WHEN BAD ARCHITECTURE AND DESIGN DECISIONS CREATE BUGS

Slide 9

Slide 9 text

DESIGNING BUILDING PROTOTYPING VERIFYING

Slide 10

Slide 10 text

WE ARE ALWAYS ADAPTING TO CHANGE (and paying off technical debt)

Slide 11

Slide 11 text

So, you're writing some sweet codes !

Slide 12

Slide 12 text

And everything is going great !

Slide 13

Slide 13 text

Then you hit a wall (but, not because the Swift compiler is slow)

Slide 14

Slide 14 text

YOU ARE STUCK ! You need a huge hack or a huge refactor

Slide 15

Slide 15 text

How can we prevent* this? We can't! LOLOLOLOLOLOLOL ! But, we can try. * Make it slightly less terrible

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

All of these architectures have the same goals SEPARATATION OF CONCERNS AND CLEAR DATA FLOW

Slide 18

Slide 18 text

S O L I D DESIGN PRINCIPLES Applicable to object-oriented and functional programming!

Slide 19

Slide 19 text

SINGLE RESPONSIBILITY OPEN / CLOSED LISKOV SUBSTITUION INTERFACE SEGREGATION DEPENDENCY INVERSION

Slide 20

Slide 20 text

Single responsibility Any type you create should only have one reason to change

Slide 21

Slide 21 text

Single responsibility ❌ class ImageDownloader { let cache: [URL: UIImage] let fileManager: FileManager let session: URLSession func getOrDownloadImage(url: URL, completion: @escaping (UIImage?) -> Void) }

Slide 22

Slide 22 text

Single responsibility ✅ class Downloader { func downloadItem(url: URL, completion: @escaping (Result) -> Void) } class Cache { func store(item: T, key: String, completion: @escaping (Bool) -> Void) func retrieveItem(key: String, completion: @escaping (Result) -> Void) } class DataProvider { let downloader: Downloader let cache: Cache func getItem(url: URL, completion: @escaping (Result) -> Void) }

Slide 23

Slide 23 text

Single responsibility ❌ extension UIImage { func cropFilterTransformAndExportPNGData( size: CGSize?, filter: CIFilter?, transform: CGAffineTransform?) -> Data } let png = img.cropFilterTransformAndExportPNGData(size: s, filter: f, transform: t)

Slide 24

Slide 24 text

Single responsibility ✅ Easy to reason about each small function individually extension UIImage { func crop(to size: CGSize) -> UIImage func transform(_ t: CGAffineTransform) -> UIImage func filter(_ f: CIFilter) -> UIImage func asPNGData() -> Data } let pngData = img.crop(to: size).transform(t).filter(f).asPNGData()

Slide 25

Slide 25 text

Open for extension closed for modification How can we change behavior without modifying a type? — Subclass ! — Inject dependency - protocols - closures

Slide 26

Slide 26 text

Open / closed Using enums as configuration ❌ enum MessagesTimestampPolicy { case all case alternating case everyThree case everyFive } class MessagesViewController: UIViewController { var timestampPolicy: MessagesTimestampPolicy }

Slide 27

Slide 27 text

Need to implement switch statements everywhere ! Every time you add a new case you have to update all switch statements class MessagesViewController: UIViewController { func timestampFor(indexPath: IndexPath) -> Date? { switch self.timestampPolicy { case .all: // ... case .alternating: // ... case .everyThree: // ... case .everyFive: // ... } } // ... }

Slide 28

Slide 28 text

Open / closed A simpler, more flexible design: configuration objects struct MessagesTimestampConfig { let timestamp: (IndexPath) -> Date? let textColor: UIColor let font: UIFont } func applyTimestampConfig(_ config: MessagesTimestampConfig)

Slide 29

Slide 29 text

Liskov substitution Types should be replaceable with instances of their subtypes without altering correctness

Slide 30

Slide 30 text

Liskov substitution ! Adding a new message type makes program incorrect class Message { } class TextMessage: Message { } class PhotoMessage: Message { } if model is TextMessage { // do text things } else if model is PhotoMessage { // do photo things } else { fatalError("Unexpected type \(model)") }

Slide 31

Slide 31 text

Liskov substitution enum MessageContent { /* ... */ } protocol Message { var content: MessageContent { get } } class TextMessage: Message { var content: MessageContent { return self.text } } class PhotoMessage: Message { var content: MessageContent { return self.photo } } let content = message.content // use content...

Slide 32

Slide 32 text

Interface segregation Use many specific interfaces, rather than one general purpose interface protocol UITableViewDataSource { func tableView(_, cellForRowAt: ) -> UITableViewCell func numberOfSections(in: ) -> Int func tableView(_, numberOfRowsInSection: ) -> Int } protocol UITableViewDataSourceEditing { func tableView(_, commit: , forRowAt: ) func tableView(_, canEditRowAt: ) -> Bool }

Slide 33

Slide 33 text

Dependency inversion Write against interfaces, not concrete types Decouple via protocols and dependency injection class MyViewController: UIViewController { let sessionManager: SessionManagerProtcool init(sessionManager: SessionManagerProtcool = SessionManager.shared) { self.sessionManager = sessionManager } // ... }

Slide 34

Slide 34 text

INTERTWINED COMPONENTS EXPONENTIALLY INCREASE COGNITIVE LOAD AND INCREASE THE MAINTENANCE BURDEN

Slide 35

Slide 35 text

FINALLY LET'S TALK ABOUT CLARITY "It was hard to write, it should be hard to read."

Slide 36

Slide 36 text

Example: drawing line graphs ! let p1 = Point(x1, y1) let p2 = Point(x2, y2) let slope = p1.slopeTo(p2) Need to check if the slope is: — undefined (vertical line) — zero (horizontal line) — positive — negative

Slide 37

Slide 37 text

We could write code like this if slope == 0 { // slope is a horizontal line // do something... } else if slope.isNaN { // slope is undefined, a vertical line // handle this case... } else if slope > 0 { // positive slope } else if slope < 0 { // negative slope }

Slide 38

Slide 38 text

Or, we could add extensions for our specific domain extension FloatingPoint { var isUndefined: Bool { return isNaN } var isHorizontal: Bool { return isZero } } extension SignedNumeric where Self: Comparable { var isPositive: Bool { return self > 0 } var isNegative: Bool { return self < 0 } }

Slide 39

Slide 39 text

And then remove the comments. if slope.isHorizontal { } else if slope.isUndefined { } else if slope.isPositive { } else if slope.isNegative { } This code reads like a sentence.

Slide 40

Slide 40 text

Another example: dynamic collection layout func getBehaviors(for attributes: [UICollectionViewLayoutAttributes]) -> [UIAttachmentBehavior] { // get the attachment behaviors return self.animator.behaviors.flatMap { $0 as? UIAttachmentBehavior }.filter { // remove non-layout attribute items guard let item = $0.items.first as? UICollectionViewLayoutAttributes else { return false } // get attributes index paths // see if item index path is included return !attributes.map { $0.indexPath }.contains(item.indexPath) } }

Slide 41

Slide 41 text

extension UIAttachmentBehavior { var attributes: UICollectionViewLayoutAttributes? { /* ... */ } } extension UIDynamicAnimator { var attachmentBehaviors: [UIAttachmentBehavior] { /* ... */ } } func getBehaviors(for attributes: [UICollectionViewLayoutAttributes]) -> [UIAttachmentBehavior] { let attributesIndexPaths = attributes.map { $0.indexPath } let attachmentBehaviors = self.animator.attachmentBehaviors let filteredBehaviors = attachmentBehaviors.filter { guard let attributes = $0.attributes else { return false } return !attributesIndexPaths.contains(attributes.indexPath) } return filteredBehaviors }

Slide 42

Slide 42 text

WHEN YOU START TO WRITE A COMMENT SEE IF YOU CAN WRITE CODE INSTEAD

Slide 43

Slide 43 text

COMMENTS CAN GET STALE BUT CODE DOESN'T LIE // show prompt if user has permissions if !user.hasPermissions { displayPrompt() } !

Slide 44

Slide 44 text

Only add comments to truly exceptional or non-obvious code // Intentionally not a struct! // Using immutable reference types for performance reasons final class Message { let uid: UUID let timestamp: Date let sender: User let text: String }

Slide 45

Slide 45 text

Keep it small and simple Write code, not comments Separate components Inject dependencies Avoid over-abstraction Avoid unnecessary complexity

Slide 46

Slide 46 text

When programmers get bored, they find ways to make easy things more complicated. !

Slide 47

Slide 47 text

Thanks! Jesse Squires @jesse_squires • jessesquires.com Swift Unwrapped @swi!_unwrapped Swift Weekly Brief @swi!lybrief • swi!weekly.github.io