$30 off During Our Annual Pro Sale. View Details »

Adapting To Change: Designing For Modularity And Maintainability In Swift

Adapting To Change: Designing For Modularity And Maintainability In Swift

Well-factored code is about more than cleanliness. The code we write is ever-changing. The only thing that’s certain is that it will need to change again... and again and again. By adopting proven design patterns and principles, we can adapt to changes more easily and quickly. Refactoring turns into a trivial afternoon task instead of a month-long project. Swift’s expressiveness and flexibility can not only help us factor our code nicely from the start, but it allows us to approach design in exciting new ways.

Video:
https://youtu.be/N0GweXS9Ilw

Event:
https://www.tryswift.co/events/2017/bangalore/

Jesse Squires

November 19, 2017
Tweet

More Decks by Jesse Squires

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. CONSTRUCTION !
    AND SOFTWARE !
    HAVE MANY THINGS
    IN COMMON

    View Slide

  4. CONNECTING
    MODEL
    AND UI
    LAYERS

    View Slide

  5. FORGETTING
    TO IMPLEMENT
    ACCESSIBILITY
    FEATURES

    View Slide

  6. CLIENT-SEVER COMMUNICATION
    MANY DIFFERENT TEAMS WORKING TOGETHER

    View Slide

  7. WHEN A NEW
    FEATURE HAS
    USER PRIVACY
    OR
    USABILITY
    ISSUES

    View Slide

  8. WHEN BAD
    ARCHITECTURE
    AND DESIGN
    DECISIONS
    CREATE BUGS

    View Slide

  9. DESIGNING
    BUILDING
    PROTOTYPING
    VERIFYING

    View Slide

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

    View Slide

  11. So, you're writing some sweet codes !

    View Slide

  12. And everything is going great !

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. View Slide

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

    View Slide

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

    View Slide

  19. SINGLE RESPONSIBILITY
    OPEN / CLOSED
    LISKOV SUBSTITUION
    INTERFACE SEGREGATION
    DEPENDENCY INVERSION

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. 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()

    View Slide

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

    View Slide

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

    View Slide

  27. 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:
    // ...
    }
    }
    // ...
    }

    View Slide

  28. 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)

    View Slide

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

    View Slide

  30. 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)")
    }

    View Slide

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

    View Slide

  32. 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
    }

    View Slide

  33. 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
    }
    // ...
    }

    View Slide

  34. INTERTWINED COMPONENTS
    EXPONENTIALLY INCREASE
    COGNITIVE LOAD
    AND INCREASE THE
    MAINTENANCE BURDEN

    View Slide

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

    View Slide

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

    View Slide

  37. 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
    }

    View Slide

  38. 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 }
    }

    View Slide

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

    View Slide

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

    View Slide

  41. 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
    }

    View Slide

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

    View Slide

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

    View Slide

  44. 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
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide