実践クライアントサイドSwift

8889da6a67db3667b0694d993c9a962c?s=47 Yosuke Ishikawa
February 24, 2017
3.1k

 実践クライアントサイドSwift

8889da6a67db3667b0694d993c9a962c?s=128

Yosuke Ishikawa

February 24, 2017
Tweet

Transcript

  1. 実践クライアントサイドSwi

  2. 実践的モデリング 入力フォームというエラーの塊を例に

  3. None
  4. 共通 ⇔ 多様

  5. フィールドってなに?

  6. 名前と値を持っていて、バリデーションができる。

  7. None
  8. struct FormField { let name: String let value: String func

    validate() throws {...} } 文字を入力するフィールド
  9. None
  10. フィールドが持つ値はString とは限らない。

  11. struct FormField<Value> { let name: String let value: Value func

    validate() throws {...} } 何を入力するフィールドにもなれる型
  12. None
  13. バリデーションはフィールドの種類によって異なる

  14. インターフェースと実装を分けた方がよい

  15. ジェネリック型 → プロトコル

  16. protocol FormField { associatedtype Value var name: String { get

    } var value: Value { get } func validate() throws }
  17. フィールドの種類に応じたパラメーターも設定可能

  18. None
  19. struct StringFormField: FormField { let name: String let value: String

    let maxCharactersCount: Int func validate() throws { if value.isEmpty { throw FormFieldError( title: "未入力 項目 ", message: "\(name) 入力 ") } if value.characters.count > maxCharactersCount { throw FormFieldError( title: "文字数 ", message: "\(name) \(maxCharactersCount) } } } 文字を入力するフィールド
  20. None
  21. struct SelectionFormField<Value>: FormField { let name: String let value: Value?

    func validate() throws { if value == nil { throw FormFieldError( title: "未選択 項目 ", message: "\(name) 選択 ") } } } 値を選択するフィールド
  22. 順調?

  23. let prefecture = Prefecture(id: 13, name: "東京都") let selectionFormField =

    SelectionFormField( name: 都道府県, value: prefecture) try selectionFormField.validate() let product = selectionFormField.value!
  24. 1. バリデーションを行う 2. 値を取り出す 操作の順序という暗黙のルールに依存

  25. 良い設計は誤った用法を コンパイルエラーにする

  26. 良い設計は誤った用法を コンパイルエラーにする ( なるべく)

  27. let prefecture = Prefecture(id: 13, name: "東京都") let selectionFormField =

    SelectionFormField( name: 都道府県, value: prefecture) try selectionFormField.validate() let product = selectionFormField.value! バリデーションと値の取り出しが別々
  28. protocol FormField { associatedtype Value associatedtype Product var name: String

    { get } var value: Value { get } func buildProduct() throws -> Product } バリデーションと結果の取得を同時に行う
  29. struct SelectionFormField<Value>: FormField { let name: String let value: Value?

    func buildProduct() throws -> Value { guard let value = value else { throw FormFieldError( title: "未選択 項目 ", message: "\(name) 選択 ") } return value } } 修正版の値を選択するフィールド
  30. let prefecture = Prefecture(id: 13, name: "東京都") let selectionFormField =

    SelectionFormField( name: 都道府県, value: prefecture) let product = try selectionFormField.buildProduct() バリデーションと結果の取得が同時になった
  31. 入力フォームってなに?

  32. フィールドを集めて、結果をまとめるもの。

  33. None
  34. struct SignUpForm { let nameField: StringFormField let emailField: EmailFormField let

    prefectureFormField: SelectionFormField<Prefecture init(name: String, email: String, prefecture: Prefecture }
  35. フィールドを集めて、結果をまとめるもの。

  36. フィールドのエラーやプロダクトをまとめるもの。

  37. protocol Form { associatedtype Product func buildProduct() throws -> Product

    }
  38. struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField

    let prefectureFormField: SelectionFormField<Prefecture init(name: String, email: String, prefecture: Prefecture // SignUpRequest API 表 型 func buildProduct() throws -> SignUpRequest {...} }
  39. struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField

    let prefectureFormField: SelectionFormField<Prefecture init(name: String, email: String, prefecture: Prefecture // 全 // 組 立 同時 行 func buildProduct() throws -> SignUpRequest { return SignUpRequest( name: try nameField.buildProduct(), email: try emailField.buildProduct(), prefectureID: try prefectureFormField.buildProdu } }
  40. // 入力値 渡 let form = SignUpForm( name: nameTextField.text, email:

    emailTextField.text, prefecture: prefecturePickerView.selectedValue) do { sendRequest(try form.buildProduct()) } catch { // 発生 }
  41. 順調?

  42. let form = SignUpForm( name: nameTextField.text, email: emailTextField.text, prefecture: prefecturePickerView.selectedValue)

    do { sendRequest(try form.buildProduct()) } catch { // 失敗 ? }
  43. None
  44. フィールドの識別子が必要

  45. enum & switch 文による網羅性

  46. どのフィールドに対するエラーハンドリングも コンパイル時にチェックされる... ?

  47. protocol Form { associatedtype FieldID associatedtype Product func buildProduct() throws

    -> Product } フォームにFieldID を導入
  48. protocol FormField { associatedtype FieldID associatedtype Value associatedtype Product var

    id: FieldID { get } var name: String { get } var value: Value { get } func buildProduct() throws -> Product } フィールドにFieldID を導入
  49. struct FormFieldError<FieldID> { let fieldID: FieldID let title: String let

    message: String } フィールドエラーにもFieldID を導入
  50. struct SignUpForm: Form { enum FieldID { case name case

    email case prefecture } let nameField: StringFormField<FieldID> let emailField: EmailFormField<FieldID> let prefectureField: SelectionFormField<FieldID, Prefect init(name: String, email: String, prefecture: Prefecture ... } }
  51. struct SignUpForm: Form { ... init(name: String, email: String, prefecture:

    Prefecture nameField = StringFormField( id: .name, name: " 名", value: name, maxCharactersCount: 20) emailField = EmailFormField( id: .email, name: " ", value: email) prefectureField = SelectionFormField( id: .prefecture, name: " 住 都道府県", value: prefecture) } }
  52. let form = SignUpForm( name: nameTextField.text, email: emailTextField.text, prefecture: prefecturePickerView.selectedValue)

    do { sendRequest(try form.buildProduct()) } catch let error as FormFieldError<SignUpForm.FieldID> { switch error.fieldID { case .name: nameTextField.becomeFirstResponder() case .email: emailTextField.becomeFirstResponder() case .prefecture: pushPrefectureViewController() } } catch { // 来 }
  53. 網羅性は利用できたが、惜しい。

  54. let form = SignUpForm( name: nameTextField.text, email: emailTextField.text, prefecture: prefecturePickerView.selectedValue)

    do { sendRequest(try form.buildProduct()) } catch let error as FormFieldError<SignUpForm.FieldID> { // ↑ 型 存在 気 難 } catch { // 何 ... }
  55. 1. というハンドルすべきエラーが発生する 2. それ以外にハンドルすべきエラーは起きない という暗黙のルールが発生している。

  56. 良い設計は誤った用法を コンパイルエラーにする

  57. protocol Form { associatedtype FieldID associatedtype Product // ↓ 時点

    型 失 func buildProduct() throws -> Product }
  58. protocol Form { associatedtype FieldID associatedtype Product func buildProduct() ->

    Result<Product, FormFieldError<FieldID>> }
  59. protocol FormField { associatedtype FieldID associatedtype Value associatedtype Product var

    id: FieldID { get } var name: String { get } var value: Value { get } func buildProduct() -> Result<Product, FormFieldError<FieldID>> }
  60. は複数の組み合わせが難しい

  61. struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField

    let prefectureFormField: SelectionFormField<Prefecture ... func buildProduct() throws -> SignUpRequest { return SignUpRequest( name: try nameField.buildProduct(), email: try emailField.buildProduct(), prefectureID: try prefectureFormField.buildProdu } } try の場合
  62. struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField

    let prefectureFormField: SelectionFormField<Prefecture ... func buildProduct() -> Result<SignUpRequest, FormFieldError<FieldID let nameResult = nameField.buildProduct() let emailResult = emailField.buildProduct() let prefectureResult = prefectureFormField.buildProd // 3 楽 組 合 ... } } の場合
  63. 他の言語に聞いてみよう

  64. None
  65. struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField

    let prefectureFormField: SelectionFormField<Prefecture ... func buildProduct() -> Result<SignUpRequest, FormFieldError<FieldID // Curry, Runes 使 Haskell return curry(SignUpRequest.init) <^> nameField.buildProduct() <*> emailField.buildProduct() <*> prefectureField.buildProduct() } }
  66. let form = SignUpForm( name: nameTextField.text, email: emailTextField.text, prefecture: prefecturePickerView.selectedValue)

    // case 消 switch form.buildProduct() { case .success(let request): sendRequest(request) case .failure(let error): switch error.fieldID { case .name: nameTextField.becomeFirstResponder() case .email: emailTextField.becomeFirstResponder() case .prefecture: pushPrefectureViewController() } }
  67. 正しく使われていることを コンパイル時に保証できるようになった

  68. まとめ

  69. 良い設計は誤った用法を コンパイルエラーにする