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

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

Yosuke Ishikawa
February 24, 2017
3.8k

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

Yosuke Ishikawa

February 24, 2017
Tweet

Transcript

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

    View full-size slide

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

    View full-size slide

  3. 共通 ⇔ 多様

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  38. 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 full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

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

    View full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

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

    View full-size slide

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

    View full-size slide