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

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

Yosuke Ishikawa
February 24, 2017
3.6k

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

Yosuke Ishikawa

February 24, 2017
Tweet

Transcript

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

    View Slide

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

    View Slide

  3. View Slide

  4. 共通 ⇔ 多様

    View Slide

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

    View Slide

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

    View Slide

  7. View Slide

  8. struct FormField {
    let name: String
    let value: String
    func validate() throws {...}
    }
    文字を入力するフィールド

    View Slide

  9. View Slide

  10. フィールドが持つ値はString
    とは限らない。

    View Slide

  11. struct FormField {
    let name: String
    let value: Value
    func validate() throws {...}
    }
    何を入力するフィールドにもなれる型

    View Slide

  12. View Slide

  13. バリデーションはフィールドの種類によって異なる

    View Slide

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

    View Slide

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

    View Slide

  16. protocol FormField {
    associatedtype Value
    var name: String { get }
    var value: Value { get }
    func validate() throws
    }

    View Slide

  17. フィールドの種類に応じたパラメーターも設定可能

    View Slide

  18. View Slide

  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)
    }
    }
    }
    文字を入力するフィールド

    View Slide

  20. View Slide

  21. struct SelectionFormField: FormField {
    let name: String
    let value: Value?
    func validate() throws {
    if value == nil {
    throw FormFieldError(
    title: "未選択 項目 ",
    message: "\(name) 選択 ")
    }
    }
    }
    値を選択するフィールド

    View Slide

  22. 順調?

    View Slide

  23. let prefecture = Prefecture(id: 13, name: "東京都")
    let selectionFormField = SelectionFormField(
    name: 都道府県,
    value: prefecture)
    try selectionFormField.validate()
    let product = selectionFormField.value!

    View Slide

  24. 1.
    バリデーションを行う
    2.
    値を取り出す
    操作の順序という暗黙のルールに依存

    View Slide

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

    View Slide

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

    View Slide

  27. let prefecture = Prefecture(id: 13, name: "東京都")
    let selectionFormField = SelectionFormField(
    name: 都道府県,
    value: prefecture)
    try selectionFormField.validate()
    let product = selectionFormField.value!
    バリデーションと値の取り出しが別々

    View Slide

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

    View Slide

  29. struct SelectionFormField: FormField {
    let name: String
    let value: Value?
    func buildProduct() throws -> Value {
    guard let value = value else {
    throw FormFieldError(
    title: "未選択 項目 ",
    message: "\(name) 選択 ")
    }
    return value
    }
    }
    修正版の値を選択するフィールド

    View Slide

  30. let prefecture = Prefecture(id: 13, name: "東京都")
    let selectionFormField = SelectionFormField(
    name: 都道府県,
    value: prefecture)
    let product = try selectionFormField.buildProduct()
    バリデーションと結果の取得が同時になった

    View Slide

  31. 入力フォームってなに?

    View Slide

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

    View Slide

  33. View Slide

  34. struct SignUpForm {
    let nameField: StringFormField
    let emailField: EmailFormField
    let prefectureFormField: SelectionFormFieldinit(name: String, email: String, prefecture: Prefecture
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. struct SignUpForm: Form {
    let nameField: StringFormField
    let emailField: EmailFormField
    let prefectureFormField: SelectionFormFieldinit(name: String, email: String, prefecture: Prefecture
    // 全
    // 組 立 同時 行
    func buildProduct() throws -> SignUpRequest {
    return SignUpRequest(
    name: try nameField.buildProduct(),
    email: try emailField.buildProduct(),
    prefectureID: try prefectureFormField.buildProdu
    }
    }

    View Slide

  40. // 入力値 渡
    let form = SignUpForm(
    name: nameTextField.text,
    email: emailTextField.text,
    prefecture: prefecturePickerView.selectedValue)
    do {
    sendRequest(try form.buildProduct())
    } catch {
    // 発生
    }

    View Slide

  41. 順調?

    View Slide

  42. let form = SignUpForm(
    name: nameTextField.text,
    email: emailTextField.text,
    prefecture: prefecturePickerView.selectedValue)
    do {
    sendRequest(try form.buildProduct())
    } catch {
    // 失敗 ?
    }

    View Slide

  43. View Slide

  44. フィールドの識別子が必要

    View Slide

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

    View Slide

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

    View Slide

  47. protocol Form {
    associatedtype FieldID
    associatedtype Product
    func buildProduct() throws -> Product
    }
    フォームにFieldID
    を導入

    View Slide

  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
    を導入

    View Slide

  49. struct FormFieldError {
    let fieldID: FieldID
    let title: String
    let message: String
    }
    フィールドエラーにもFieldID
    を導入

    View Slide

  50. struct SignUpForm: Form {
    enum FieldID {
    case name
    case email
    case prefecture
    }
    let nameField: StringFormField
    let emailField: EmailFormField
    let prefectureField: SelectionFormFieldinit(name: String, email: String, prefecture: Prefecture
    ...
    }
    }

    View Slide

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

    View Slide

  52. let form = SignUpForm(
    name: nameTextField.text,
    email: emailTextField.text,
    prefecture: prefecturePickerView.selectedValue)
    do {
    sendRequest(try form.buildProduct())
    } catch let error as FormFieldError {
    switch error.fieldID {
    case .name:
    nameTextField.becomeFirstResponder()
    case .email:
    emailTextField.becomeFirstResponder()
    case .prefecture:
    pushPrefectureViewController()
    }
    } catch {
    // 来
    }

    View Slide

  53. 網羅性は利用できたが、惜しい。

    View Slide

  54. let form = SignUpForm(
    name: nameTextField.text,
    email: emailTextField.text,
    prefecture: prefecturePickerView.selectedValue)
    do {
    sendRequest(try form.buildProduct())
    } catch let error as FormFieldError {
    // ↑ 型 存在 気 難
    } catch {
    // 何 ...
    }

    View Slide

  55. 1. というハンドルすべきエラーが発生する
    2.
    それ以外にハンドルすべきエラーは起きない
    という暗黙のルールが発生している。

    View Slide

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

    View Slide

  57. protocol Form {
    associatedtype FieldID
    associatedtype Product
    // ↓ 時点 型 失
    func buildProduct() throws -> Product
    }

    View Slide

  58. protocol Form {
    associatedtype FieldID
    associatedtype Product
    func buildProduct()
    -> Result>
    }

    View Slide

  59. protocol FormField {
    associatedtype FieldID
    associatedtype Value
    associatedtype Product
    var id: FieldID { get }
    var name: String { get }
    var value: Value { get }
    func buildProduct()
    -> Result>
    }

    View Slide

  60. は複数の組み合わせが難しい

    View Slide

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

    View Slide

  62. struct SignUpForm: Form {
    let nameField: StringFormField
    let emailField: EmailFormField
    let prefectureFormField: SelectionFormField...
    func buildProduct()
    -> Resultlet nameResult = nameField.buildProduct()
    let emailResult = emailField.buildProduct()
    let prefectureResult = prefectureFormField.buildProd
    // 3 楽 組 合 ...
    }
    }
    の場合

    View Slide

  63. 他の言語に聞いてみよう

    View Slide

  64. View Slide

  65. struct SignUpForm: Form {
    let nameField: StringFormField
    let emailField: EmailFormField
    let prefectureFormField: SelectionFormField...
    func buildProduct()
    -> Result// Curry, Runes 使 Haskell
    return curry(SignUpRequest.init)
    <^> nameField.buildProduct()
    <*> emailField.buildProduct()
    <*> prefectureField.buildProduct()
    }
    }

    View Slide

  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()
    }
    }

    View Slide

  67. 正しく使われていることを
    コンパイル時に保証できるようになった

    View Slide

  68. まとめ

    View Slide

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

    View Slide