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

Sane in the Domain

Sane in the Domain

How Swift type system can help you model and test your application domain

Michał Ciuba

April 11, 2019
Tweet

More Decks by Michał Ciuba

Other Decks in Programming

Transcript

  1. Sane in the domain How Swift type system can help

    you model and test your application domain Michał Ciuba
  2. Agenda • What Domain means • Measurements • Phantom types

    • Domain-specific error types • Functions are types: unit tests example
  3. Domain / Entities layer “Business rules or procedures that (…)

    would make or save the business money, irrespective of whether they were implemented on a computer” - Robert C. Martin, “Clean Architecture”
  4. Domain examples • Entities: Driver, Customer, Booking • Business rule:

    Driver has a list of Bookings (rides booked by Customers)
  5. Not Domain examples • class Driver: NSManagedObject • data storage

    is an implementation detail, not a core business rule
  6. Domain types • use “plain” objects (not inheriting from any

    base class) • powerful Swift value types: structs, enum • compiler-generated Equatable, Codable, init • exhaustiveness checks for enums
  7. Why? • Software developed by two teams • Each team

    used different units
 (kilograms vs pounds)
  8. C (1970s) type system: primitive types // is it in

    kilograms? Or pounds? func calculateForce(forMass: Double) -> Double
  9. Measurement let orbiterMass = Measurement<UnitMass>(value: 0.42, unit: .kilograms) let stardustMass

    = Measurement<UnitMass>(value: 1E-5, unit: .pounds) let totalMass = orbiterMass + stardustMass // 0.42000453592 kg
  10. Measurement let orbiterLength = Measurement<UnitLength>(value: 0.84, unit: .meters) let stardustMass

    = Measurement<UnitMass>(value: 1E-5, unit: .pounds) let totalLength = orbiterLength + stardustMass //won't compile
  11. Measurement • Predefined 21 unit types - physical quantities (time,

    distance, speed…) • You can define your own units • locale-aware MeasurementFormatter
  12. protocol MediaUploading { func uploadPhoto(at url: URL) func uploadVideo(at url:

    URL) } protocol MediaLibraryPicking { func selectPhoto() -> URL func selectVideo() -> URL }
  13. Mistakes happen let url = mediaLibraryPicker.selectVideo() // a couple of

    method calls later mediaUploader.uploadPhoto(at: url) //
  14. Phantom types struct LocalFile<FileType> { let url: URL } struct

    Photo {} struct Video {} let photo = LocalFile<Photo>(url: someURL)
  15. Explicit interfaces protocol MediaUploading { func uploadPhoto(file: LocalFile<Photo>) func uploadVideo(file:

    LocalFile<Video>) } protocol MediaLibraryPicking { func selectPhoto() -> LocalFile<Photo> func selectVideo() -> LocalFile<Video> }
  16. Compile-time checked let file = mediaLibraryPicker.selectVideo() // a couple of

    method calls later mediaUploader.uploadPhoto(file: file) // won’t compile!
  17. “Too early” error HTTP 400 Bad Request { "error": {

    "message": "You are starting the job too early", "code": "JOB_STARTED_TOO_EARLY" } }
  18. func handle(error: Error, errorCode: String?) { switch (error) { case

    AFError.responseValidationFailed where errorCode == "JOB_STARTED_TOO_EARLY": showTooEarlyErrorMessage() case is CLError: showLocationErrorMessage() default: showDefaultErrorMessage() } }
  19. Wild error appears! HTTP 400 Bad Request { "error": {

    “message": "This booking is assigned to a different vehicle from the one currently in use. Use a correct vehicle to continue. “, “code” :"INVALID_VEHICLE" } }
  20. func handle(error: Error, errorCode: String?) { switch (error) { case

    AFError.responseValidationFailed where errorCode == "JOB_STARTED_TOO_EARLY": showTooEarlyErrorMessage() case is CLError: showLocationErrorMessage() default: showDefaultErrorMessage() } }
  21. Errors as Domain types enum ApplicationError: String { case tooEarly

    = "JOB_STARTED_TOO_EARLY" case invalidVehicle = "INVALID_VEHICLE" }
  22. Errors as Domain types enum ApplicationError: String { case tooEarly

    = "JOB_STARTED_TOO_EARLY" case invalidVehicle = "INVALID_VEHICLE" } enum StartJobError: Swift.Error { case locationTimeout case applicationError(ApplicationError) case serviceUnavailable }
  23. func handle(error: StartJobError) { switch(error) { case .locationTimeout: showLocationErrorMessage() case

    .applicationError(.tooEarly): showTooEarlyErrorMessage() case .applicationError(.wrongVehicle): showSelectVehicleAlert() } } // checked for exhaustiveness // (all ApplicationError values have to be handled)
  24. func handle(error: StartJobError) { switch(error) { case .locationTimeout: showLocationErrorMessage() case

    .applicationError(.tooEarly): showTooEarlyErrorMessage() case .applicationError(.wrongVehicle): showSelectVehicleAlert() case .serviceUnavailable: showDefaultErrorMessage() } } No “default” case
  25. Error conversion func convertToDomain(error: Swift.Error, withCode code: String) -> StartJobError

    { if case AFError.responseValidationFailed = error, let appError = ApplicationError(rawValue: code) { return .applicationError(appError) } if error is CLError { return .locationTimeout } // and so on... return .serviceUnavailable }
  26. Example: async calculation class AsyncCalculator { let serialQueue = DispatchQueue(label:

    "serial") func calculate(completion: @escaping (Int) ->()) { serialQueue.async { self.doComplexCalculations() completion(42) } } }
  27. Async tests - Expectations func testCalculationHasCorrectResult() { // Arrange let

    expect = expectation(description: name) // Act calculator.calculate { result in XCTAssertEqual(result, 42) expect.fulfill() } // Assert waitForExpectations(timeout: 1) // }
  28. Dependency injection? class AsyncCalculator { let serialQueue = DispatchQueue(label: "serial")

    func calculate(completion: @escaping (Int) ->()) { serialQueue.async { self.doComplexCalculations() completion(42) } } }
  29. Dependency injection? class AsyncCalculator { var dispatcher = ? let

    serialQueue = DispatchQueue(label: "serial") func calculate(completion: @escaping (Int) ->()) { dispatcher { self.doComplexCalculations() completion(42) } } }
  30. Inject a function! typealias DispatchedWork = () -> Void typealias

    Dispatcher = (@escaping DispatchedWork) -> Void class AsyncCalculator { internal lazy var dispatcher: Dispatcher }
  31. Inject a function! typealias DispatchedWork = () -> Void typealias

    Dispatcher = (@escaping DispatchedWork) -> Void class AsyncCalculator { internal lazy var dispatcher: Dispatcher = self.serialQueue.async(execute:) let serialQueue = DispatchQueue(label: "serial") }
  32. Inject a function! typealias DispatchedWork = () -> Void typealias

    Dispatcher = (@escaping DispatchedWork) -> Void class AsyncCalculator { internal lazy var dispatcher: Dispatcher = self.serialQueue.async(execute:) let serialQueue = DispatchQueue(label: "serial") func calculate(completion: @escaping (Int) -> Void) { dispatcher { self.doComplexCalculations() completion(42) } } } Inject a function!
  33. Inject a function - test case private func instantDispatcher(_ closure:

    @escaping () -> Void ) { closure() } func testCalculationHasCorrectResult() { // Arrange calculator.dispatcher = instantDispatcher }
  34. Synchronous tests private func instantDispatcher(_ closure: @escaping () -> Void

    ) { closure() } func testCalculationHasCorrectResult() { // Arrange calculator.dispatcher = instantDispatcher var result: Int? //Act calculator.calculate { result = $0 } //Assert XCTAssertEqual(result, 42) }
  35. Wrap up • Use plain Swift objects (value types when

    possible) to model your Domain • Don’t pass around primitive types (like Double, URL…) • Error types are part of your Domain • Functions are types