Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

共通 ⇔ 多様

Slide 5

Slide 5 text

フィールドってなに?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

順調?

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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 } } 修正版の値を選択するフィールド

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

入力フォームってなに?

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

struct SignUpForm { let nameField: StringFormField let emailField: EmailFormField let prefectureFormField: SelectionFormField

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField let prefectureFormField: SelectionFormField SignUpRequest {...} }

Slide 39

Slide 39 text

struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField let prefectureFormField: SelectionFormField SignUpRequest { return SignUpRequest( name: try nameField.buildProduct(), email: try emailField.buildProduct(), prefectureID: try prefectureFormField.buildProdu } }

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

順調?

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

enum & switch 文による網羅性

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

struct SignUpForm: Form { enum FieldID { case name case email case prefecture } let nameField: StringFormField let emailField: EmailFormField let prefectureField: SelectionFormField

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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 { // 来 }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField let prefectureFormField: SelectionFormField Result

Slide 63

Slide 63 text

他の言語に聞いてみよう

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

struct SignUpForm: Form { let nameField: StringFormField let emailField: EmailFormField let prefectureFormField: SelectionFormField Result nameField.buildProduct() <*> emailField.buildProduct() <*> prefectureField.buildProduct() } }

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

まとめ

Slide 69

Slide 69 text

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