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

So You're Writing a Framework...

Bas Broek
November 02, 2018

So You're Writing a Framework...

How to create better, nicer APIs.

Bas Broek

November 02, 2018
Tweet

More Decks by Bas Broek

Other Decks in Programming

Transcript

  1. So You're
    Writing a
    Framework...
    1 — @basthomas

    View Slide

  2. So You're
    Writing a
    Framework...
    2 — @basthomas

    View Slide

  3. So You're
    Writing a
    Feature...
    3 — @basthomas

    View Slide

  4. So You're
    Writing an
    API...
    4 — @basthomas

    View Slide

  5. So You're
    Writing a
    Library...
    5 — @basthomas

    View Slide

  6. So You're
    Writing a
    Code Base...
    6 — @basthomas

    View Slide

  7. So You're
    Writing an
    App...
    7 — @basthomas

    View Slide

  8. Who am I
    → iOS Developer @ XING
    → I curate Swift Weekly Brief
    → I do some open source things
    → I like continuous integration systems, too
    8 — @basthomas

    View Slide

  9. Have you ever tried...
    → using UIKit without documentation? (Xcode
    "helps" sometimes)
    → using someone's (undocumented) API without
    looking at the implementation?
    → using an undocumented API you wrote in the
    past, without looking at the
    implementation?
    9 — @basthomas

    View Slide

  10. Looking into a pull
    request that came up in
    my GitHub notifications...
    10 — @basthomas

    View Slide

  11. [WIP] Add lists class
    No description provided
    11 — @basthomas

    View Slide

  12. class Lists
    def initialize(array_1, array_2)
    @array_1 = array_1.clone
    @array_2 = array_2.clone
    end
    def combine_outputs
    concat_arrays
    end
    private
    def occurrences
    @array_1.count(@array_2.first)
    end
    def occurred_at_index(array)
    array.index(@array_2.first)
    end
    def occurrence_chunks
    chunks = []
    array = @array_1.clone
    occurrences.times do
    occurrence_index = occurred_at_index(array)
    chunks << array[occurrence_index..array.count]
    array.delete_at(occurrence_index)
    end
    chunks
    end
    # more of this
    end
    12 — @basthomas

    View Slide

  13. Does anyone have any
    idea what's going on?
    13 — @basthomas

    View Slide

  14. Me neither.
    14 — @basthomas

    View Slide

  15. 15 — @basthomas

    View Slide

  16. This pull request adds a possibility to
    concat arrays without removing the
    duplicates in the parts that are not
    overlap completely (in the meaning that
    we care only if the last x values of 1st
    array overlaps with the same x values of
    the 2nd, but we don't care if some
    duplicates are randomly exist in the body
    of array)
    16 — @basthomas

    View Slide

  17. This pull request adds a
    possibility to concat arrays
    without removing the
    duplicates in the parts that
    are not overlap completely
    17 — @basthomas

    View Slide

  18. # @param array_1 [Array] an array that contain all the strings collected till
    # this moment
    # @param array_2 [Array] an array that contain new elements that should be
    # added in array_1
    def initialize(array_1, array_2)
    @array_1 = array_1.clone
    @array_2 = array_2.clone
    end
    # Combines two arrays with partial removal of duplicates
    #
    # @return [Array] of complate @array_1 and partially(removing the first
    # occurances in both arrays) added @array_2
    def combine_outputs
    concat_arrays
    end
    18 — @basthomas

    View Slide

  19. RSpec.describe Lists do
    subject { described_class.new(array_1, array_2) }
    describe '#combine_outputs' do
    let(:array_1) do
    ['a', 'b', 'c']
    end
    let(:array_2) do
    ['c', 'd', 'e']
    end
    it 'concat the array in the right way' do
    expected_result = ['a', 'b', 'c', 'd', 'e']
    expect(subject.combine_outputs).to match(expected_result)
    end
    let(:array_3) do
    ['x', 'y', 'z']
    end
    it 'raises when cannot find similarities in array' do
    expect do
    described_class.new(array_1, array_3).combine_outputs
    end.to raise_exception('Cannot find comparable data')
    end
    end
    end
    19 — @basthomas

    View Slide

  20. class Lists
    class NoOverlapError < StandardError; end
    # Adds two arrays with removing the overlap between them
    #
    # @param existing [Array] the exsting elemets.
    # @param new_entries [Array] the elements that should be added to the front
    # @return [Array] returns the sum of both array with `new_entries` at the front
    def self.concat_without_overlap(existing, new_entries)
    occourences = new_entries.each_index.select { |i| new_entries.at(i) == existing.first }
    overlap_start = occourences.find { |index| array_starts_with?(existing, new_entries[index..-1]) }
    raise NoOverlapError, 'Cannot find comparable data' if overlap_start.nil?
    new_entries.first(overlap_start) + existing
    end
    # Checks if one array starts with an other one
    #
    # @param a1 [Array] the first array
    # @param a2 [Array] the second array
    # @return [Boolean] returns true if `a1` starts with `a2`
    def self.array_starts_with?(a1, a2)
    a1[0, a2.length] == a2
    end
    end
    20 — @basthomas

    View Slide

  21. Naming is
    hard.
    21 — @basthomas

    View Slide

  22. class Lists
    class NoOverlapError < StandardError; end
    # Adds two arrays with removing the overlap between them
    #
    # @param existing [Array] the exsting elemets.
    # @param new_entries [Array] the elements that should be
    # added to the front
    # @return [Array] returns the sum of both array with
    # `new_entries` at the front
    def self.concat_without_overlap(existing, new_entries)
    # implementation
    end
    end
    22 — @basthomas

    View Slide

  23. # Prepends an array to front of an existing one (like Array#unshift)
    # without adding the overlap twice.
    #
    # @example
    # existing = [4, 3, 2, 1]
    # new_entries = [6, 5, 4, 3]
    # Lists.concat_without_overlap(existing, new_entries) # => [6, 5, 4, 3, 2, 1]
    #
    # @param existing [Array] the existing array.
    # @param new_entries [Array] the array that should be added to the front
    # @return [Array] returns the new array
    23 — @basthomas

    View Slide

  24. class Lists
    class NoOverlapError < StandardError; end
    # Appends an array to end of an existing one
    # without adding the overlap twice.
    #
    # @example
    # existing_entries = [1, 2, 3, 4]
    # new_entries = [4, 5, 6, 7]
    # Lists.stitch(existing_entries, new_entries) # => [1, 2, 3, 4, 5, 6, 7]
    #
    # @note Raises an NoOverlapError if no overlap is found.
    #
    # @param existing_entries [Array] the existing array.
    # @param new_entries [Array] the array to stitch to the existing one.
    # @return [Array] returns the new array
    def self.stitch(existing_entries, new_entries)
    # get the index of all occurrences of the first element from the
    # new_entries array in existing_entries
    occurrences = existing_entries.each_index
    .select { |i| existing_entries.at(i) == new_entries.first }
    # check if one of the slices of existing_entries is prefix of
    # the new_entries array
    overlap_start = occurrences
    .find { |index| array_starts_with?(new_entries, existing_entries[index..-1]) }
    raise NoOverlapError, 'Cannot find comparable data' if overlap_start.nil?
    # add the new_entries without the overlap at the end of the existing array
    existing_entries.first(overlap_start) + new_entries
    end
    24 — @basthomas

    View Slide

  25. # Checks if one array starts with another one
    #
    # @example Array starts with another array
    # Lists.array_starts_with?([1, 2, 3], [1, 2]) # => true
    #
    # @example Array does not start with another array
    # Lists.array_starts_with?([4, 5, 6], [1, 2]) # => false
    #
    # @param a1 [Array] the first array
    # @param a2 [Array] the second array
    # @return [Boolean] returns true if `a1` starts with `a2`
    def self.array_starts_with?(a1, a2)
    a1[0, a2.length] == a2
    end
    end
    25 — @basthomas

    View Slide

  26. !!!
    26 — @basthomas

    View Slide

  27. Code
    27 — @basthomas

    View Slide

  28. Tests
    28 — @basthomas

    View Slide

  29. Documentation
    29 — @basthomas

    View Slide

  30. Code.
    30 — @basthomas

    View Slide

  31. Tests.
    31 — @basthomas

    View Slide

  32. Documentation.
    32 — @basthomas

    View Slide

  33. Code.
    Tests.
    Documentation.
    33 — @basthomas

    View Slide

  34. These are not three
    separate devices!
    — Steve Jobs, at some event
    34 — @basthomas

    View Slide

  35. They are like
    a Venn
    diagram
    35 — @basthomas

    View Slide


  36. Warning, potential unpopular opinion ahead
    36 — @basthomas

    View Slide

  37. The order
    does not
    matter*
    * But do figure out what works best for your team.
    There's a high chance starting with tests is a good idea.
    37 — @basthomas

    View Slide

  38. How to
    measure
    success?
    38 — @basthomas

    View Slide

  39. It is successful if a failure
    or success case that is
    discovered or added in
    the future can be solved via
    Test Driven Development
    — Me
    39 — @basthomas

    View Slide

  40. Communication is hard.
    40 — @basthomas

    View Slide

  41. Managing expectations.
    41 — @basthomas

    View Slide

  42. What more can we do?
    → Give examples
    → Write simple, non near-code documentation
    → "No raw loops"
    → Self-enforced best practice
    → Strongly typed APIs
    → Scope
    → Compassion
    → ..?
    42 — @basthomas

    View Slide

  43. OK, not yet...
    → Component System
    → Component (Text) Configuration
    → Strong typing
    → Objective-C interop...
    → (is a good thing?)
    43 — @basthomas

    View Slide

  44. @objc public class TextConfiguration: NSObject {
    @objc public init(
    color: TextColor,
    weight: TextWeight,
    size: TextSize,
    numberOfLines: Int
    ) {
    self._color = color
    self.weight = weight
    self.size = size
    self.numberOfLines = numberOfLines
    }
    }
    44 — @basthomas

    View Slide

  45. /// Creates a new `TextConfiguration` with the specified values.
    ///
    /// - Parameter color: the text color to use. Available colors are a subset
    /// of `XINGColor` and are safe to use for text.
    /// - Parameter weight: the text weight to use. This will be used for the text's font.
    /// - Parameter size: the text size to use. This will be used for the text's font.
    /// - Parameter numberOfLines: The number of lines of text to render.
    /// To remove any maximum limit, and use as many lines as needed,
    /// set the value of this property to 0.
    @objc public init(
    color: TextColor,
    weight: TextWeight,
    size: TextSize,
    numberOfLines: Int
    ) {
    self._color = color
    self.weight = weight
    self.size = size
    self.numberOfLines = numberOfLines
    }
    45 — @basthomas

    View Slide

  46. let headerConfiguration = TextConfiguration(
    color: .blue500,
    weight: .medium,
    size: .default12
    )
    // now what?
    46 — @basthomas

    View Slide

  47. /// - Paramter label: the label to generate a text configuration from.
    ///
    /// - Returns: a `TextConfiguration` based on the label, if it uses valid
    /// text configuration values. If not, it will assert and revert to default options.
    @objc public static func from(label: UILabel) -> TextConfiguration {
    func textColor(from color: UIColor) -> TextColor {
    guard let _textColor = TextColor.all.first(where: { $0.uiColor == label.textColor }) else {
    assertionFailure("Could not convert label's color to a valid TextColor. This will default to `.grey800` in production, which may be unexpected.")
    return .grey800
    }
    return _textColor
    }
    func textWeight(from font: UIFont) -> TextWeight {
    guard let _textStyle = TextWeight.from(font: font) else {
    assertionFailure("Could not convert font's weight to a valid TextWeight. This will default to `.regular` in production, which may be unexpected.")
    return .regular
    }
    return _textStyle
    }
    func textSize(from font: UIFont) -> TextSize {
    guard let _textSize = TextSize.from(font: font) else {
    assertionFailure("Could not convert font's size to a valid TextSize. This will default to `.default12` in production, which may be unexpected.")
    return .default12
    }
    return _textSize
    }
    return .init(
    color: textColor(from: label.textColor),
    weight: textWeight(from: label.font),
    size: textSize(from: label.font),
    numberOfLines: label.numberOfLines
    )
    }
    47 — @basthomas

    View Slide

  48. func textColor(from color: UIColor) -> TextColor {
    guard let _textColor = TextColor.all.first(where: { $0.uiColor == label.textColor }) else {
    assertionFailure("Could not convert label's color to a valid TextColor.")
    return .grey800
    }
    return _textColor
    }
    func textWeight(from font: UIFont) -> TextWeight {
    guard let _textStyle = TextWeight.from(font: font) else {
    assertionFailure("Could not convert font's weight to a valid TextWeight.")
    return .regular
    }
    return _textStyle
    }
    func textSize(from font: UIFont) -> TextSize {
    guard let _textSize = TextSize.from(font: font) else {
    assertionFailure("Could not convert font's size to a valid TextSize.")
    return .default12
    }
    return _textSize
    }
    48 — @basthomas

    View Slide

  49. @objc public static func from(label: UILabel) -> TextConfiguration {
    return .init(
    color: textColor(from: label.textColor),
    weight: textWeight(from: label.font),
    size: textSize(from: label.font),
    numberOfLines: label.numberOfLines
    )
    }
    49 — @basthomas

    View Slide

  50. /// Applies the current `TextConfiguration` to the supplied label.
    ///
    /// - Parameter label: the `UILabel` instance to apply the
    /// text configuration to.
    ///
    /// - Returns: a reference to the label that has the configuration
    /// aplied to it.
    @discardableResult
    @objc public func apply(to label: UILabel) -> UILabel {
    label.textColor = color
    label.font = font
    label.numberOfLines = numberOfLines
    return label
    }
    50 — @basthomas

    View Slide

  51. let headerConfiguration = TextConfiguration.from(label: headerLabel)
    let subtitleLabel = UILabel()
    headerConfiguration
    .with(color: .red500)
    .apply(to: subtitleLabel)
    51 — @basthomas

    View Slide

  52. Outcome
    → Scoped
    → Testable (and bugs and edge cases fixed /
    covered during development)
    → Objective-C interop
    → Chainable API
    → It's not perfect (and that is OK)
    → Architecture design is missing
    52 — @basthomas

    View Slide

  53. Outcome, part 2
    → The compiler is your friend
    → Don't be afraid to use types
    → The compiler is your enemy
    → We should explain the architectural design
    → Prevent "rest of the damn owl" syndrom
    → Use assertions!
    53 — @basthomas

    View Slide

  54. Things to keep in mind
    → I don't want to write the correct code. There is no
    correct code. There is no silver bullet.
    → I want to help the most correct code to be written.
    → I want to prevent incorrect code to be written.
    → I want to fail early and often.
    → (but for that, I need it to be testable)
    → Iterate, iterate, iterate
    54 — @basthomas

    View Slide

  55. Takeaways
    → Optimize code for reading, not writing
    → Work together and bounce off ideas
    → Documentation is helpful for yourself when writing
    code, too
    → Documentation helps you with naming, scope,
    testability and readability
    → Having testable code feels awesome
    → Snapshot tests are the root of all evil
    55 — @basthomas

    View Slide

  56. Thanks!
    @basthomas
    @_contravariance podcast
    @swiftlybrief newsletter
    56 — @basthomas

    View Slide

  57. Further reading
    "Combinators" by Daniel Steinberg
    https://vimeo.com/290272240
    "Denotational Design" by Conal Elliot
    https://github.com/conal/talk-2014-lambdajam-denotational-design
    "... But That Should Work?" by me
    https://basthomas.github.io/but-that-should-work
    Point-Free by Brandon Williams & Stephen Celis
    https://www.pointfree.co
    57 — @basthomas

    View Slide