So You're Writing a Framework...

79fe3c13c618a61329298bdd6a86ec42?s=47 Bas Broek
November 02, 2018

So You're Writing a Framework...

How to create better, nicer APIs.

79fe3c13c618a61329298bdd6a86ec42?s=128

Bas Broek

November 02, 2018
Tweet

Transcript

  1. 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
  2. 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
  3. 10.

    Looking into a pull request that came up in my

    GitHub notifications... 10 — @basthomas
  4. 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
  5. 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
  6. 17.

    This pull request adds a possibility to concat arrays without

    removing the duplicates in the parts that are not overlap completely 17 — @basthomas
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 43.

    OK, not yet... → Component System → Component (Text) Configuration

    → Strong typing → Objective-C interop... → (is a good thing?) 43 — @basthomas
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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