UX設計からはじめるTEA入門

2019/08/12
[Elm関西](https://connpass.com/event/141426/) 発表資料

---

## 自己紹介

* arowM
    * [Twitter](https://twitter.com/arowM_)
    * [GitHub](https://github.com/arowM/)

    ![avatar](https://avatars3.githubusercontent.com/u/1481749?s=400&u=f05f8574d3791cd4515e5d08972263e28195db96&v=4)

* おしごと: 広義のUX設計
    * ヤギコンサルタント/ヤギアイドルさくらちゃんのプロデュース
        * [さくらちゃん特大ワッペン](https://amzn.to/2YVCNBI)つくりました

        ![sakura](https://pbs.twimg.com/media/EBmZI_bUYAAaroz?format=jpg)

    * 気吹堂(出版) 代表
        * さくらちゃん写真集
    * ARoW(IT技術) 代表
        * 技術顧問
        * 技術補佐
        * 要件分析・UX設計
    * 株式会社UZUZ(人材紹介) CXO (ユーザー体験の責任者)
        * 組織制度の改善
        * マネジメント
        * 社員のモチベーションマネジメント
        * 業務フローの改善 (業務アプリの作成を含む)
        * 求職者向けアプリの設計・開発
        * 提携企業向けアプリの設計・開発
    * OSS活動
        * 公式ガイド Elm guide の[日本語翻訳プロジェクト](https://guide.elm-lang.jp/)
        * Form decoderの概念の提唱と[Elm版実装](https://github.com/arowM/elm-form-decoder)
        * Haskell製型安全なテンプレートエンジン[heterocephalus](https://github.com/arowM/heterocephalus)
        * プラグイン形式でさくさくっとバックエンドを作れちゃうフレームワーク[tonatona](https://github.com/tonatona-project/tonatona)

---

## 概要

Elmのアプリケーションフレームワーク The Elm Architecture (TEA) の入門として、すご〜くシンプルなゲームを作ってみましょう。
TEAはUX設計と親和性がめちゃんこ高いよ！

ただし、[ちゃんとTEAを身につけるならElm guideを読みましょう！](https://qiita.com/arowM/items/5ec5853298fc880353b7)

---

## シナリオ

「シナリオ」はUX設計をする上で最も大切なテクニックの1つです。
ユーザーの気持ちを妄想して、ユーザーの行動を未来予知して、事前に文章に列挙します。

「まずシナリオを書いてみましょう」と助言しても、「そんなの素人がやるやつじゃん」と、いきなり実装を初めてクソアプリを作る人たちがいっぱいでヤギさんは悲しいです。

**いい製品とは、粘り強く根気強く試行錯誤を何度も重ねたもの**
**じゃあ、先に妄想の中で試行錯誤した方が短期間でいい製品ができるじゃん**

では、実際に「さくらちゃん と じゃんけん」ゲームのシナリオを作っていきましょう！

[完成版はこちら](https://arowm.github.io/elm-kansai1-tea/index.html)

---

### シナリオ1 -- ヤギ思いのユーザー

* __このスタイルの文__: アプリ側の表示
* 地の文: ユーザーの思考・行動

1. __ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる__
2. 「じゃんけんゲームってことかな？」
3. とりあえず「ぐー」を押してみた
4. __「かち！」と表示され、ヤギが悔しがった顔をしながら「ヤギさんにも ようしゃが ないね！」と言ってくる__
5. 「そっか、ヤギさんだからチョキしか出せないのか。悪いことしちゃったな」
6. 「もういっかい」のボタンがあることに気づく
7. 「あ、もしかしてもう一回じゃんけんできる？ 今度は負けてあげよう」
8. 「もういっかい」のボタンをクリックする
9. __最初の画面が再び表示される__
10. 「あ、やっぱりもういっかいできるみたいだ」
11. 「ぱー」を押してみた
12. __「まけ！」と表示され、ヤギが調子に乗った顔しながら「ほもさぴえんすって よわい いきものだね！」と言ってくる__
13. 「なんかうざいけど、でも喜んでくれてよかった。」

---

### シナリオ2 -- 察しが悪いユーザー

UX設計は、「まさかそんなことするか？！」というところまで妄想するといいUXができる。

1. __ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる__
2. 「え、なによくわからん」
3. とりあえずヤギの写真をクリックしてみた
4. __荒ぶった感じのヤギが「やぎさんじゃなくて ぼたんをおしてね！」って言ってくる__
5. 「あ、ボタンか。なるほど」
6. とりあえず「ぐー」を押してみた
7. (以下略)

---

## 画面設計

シナリオをもとに、画面と状態を整理します。

### 基本画面構成

以下のエリアから構成されます。

* ヤギ: ヤギの写真が表示される
* メインメッセージ: ユーザーへのメインのメッセージを表示する
* サブメッセージ: ユーザーへの補助的なメッセージを表示する
* ボタンエリア: ボタンを表示する

---

### 初期状態

* [画面]
    * ヤギ: ノーマルやぎ
    * メインメッセージ: 「じゃ〜んけ〜ん」
    * サブメッセージ: 「すきな ぼたんを おしてね！」
    * ボタンエリア:
        * 「ぐー」
        * 「ちょき」
        * 「ぱー」
* [ユーザー] 「なにこれ、どれかボタン押せばいいの？」
    * [イベント1] ぐーを押した
    * [イベント2] ちょきを押した
    * [イベント3] ぱーを押した
    * [イベント4] ヤギを押した

---

### イベント1

* [画面]
    * ヤギ: 悔しそうなやぎ
    * メインメッセージ: 「かち！」
    * サブメッセージ: 「ヤギさんにも ようしゃが ないね！」
    * ボタンエリア
        * 「もういっかい」
* [ユーザー]
    * [イベント5] 「もういっかい」を押した

---

### イベント2

* [画面]
    * ヤギ: 絶妙な表情のヤギ
    * メインメッセージ: 「あいこ！」
    * サブメッセージ: 「きが あうね！」
    * ボタンエリア
        * 「もういっかい」
* [ユーザー]
    * [イベント5] 「もういっかい」を押した

---

### イベント3

* [画面]
    * ヤギ: 嬉しいヤギ
    * メインメッセージ: 「まけ！」
    * サブメッセージ: 「ほもさぴえんすって よわい いきものだね！」
    * ボタンエリア
        * 「もういっかい」
* [ユーザー]
    * [イベント5] 「もういっかい」を押した

---

### イベント4

* [画面]
    * ヤギ: 荒ぶるやぎ
    * メインメッセージ: 「じゃ〜んけ〜ん」
    * サブメッセージ: 「やぎさんじゃなくて ぼたんをおしてね！」
    * ボタンエリア:
        * 「ぐー」
        * 「ちょき」
        * 「ぱー」
* [ユーザー] 「なにこれ、どれかボタン押せばいいの？」
    * [イベント1] ぐーを押した
    * [イベント2] ちょきを押した
    * [イベント3] ぱーを押した

---

### イベント5

初期状態と同じ

## 実装する

TEAは画面設計と親和性が高いので簡単に実装できます。

ポイントとなる概念は4つ:

* モデル
* 初期状態
* メッセージ/アップデート
* ビュー

---

### モデル

アプリケーションの状態を、Elmの値で表現してみましょう。
これをTEAでは「モデル」と言います。

基本的には「画面設計」で列挙した各イベント後の状態をカスタム型で表現するだけです。

```elm
-- あとで拡張できるようにレコード型にしておくと便利！
type alias Model =
    { phase : Phase
    }


type Phase
    -- 初期状態/イベント5
    = Start
    -- イベント1
    | AfterWin
    -- イベント2
    | AfterDraw
    -- イベント3
    | AfterLose
    -- イベント4
    | AfterClickingGoat
```

---

### 初期状態

初期状態のモデルを定義しましょう。

```elm
init : Model
init =
    { phase = Start
    }
```

そのまんま

---

### メッセージ/アップデート

イベントハンドリングのことです。

シナリオのイベントに名前をつけて、それぞれどのようにモデルを更新するか定義しましょう。

```elm
-- メッセージ (イベント)
type Msg
    = PressGuu
    | PressChoki
    | PressPaa
    | ClickGoat
    | PressAgain


-- モデルの更新 (イベントが起きた時にどのようにモデルを更新するか)
update : Model -> Msg -> Model
update model msg =
    case msg of
        PressGuu ->
            { phase = AfterWin
            }
        PressChoki ->
        (中略)
        PressAgain ->
            { phase = Start
            }
```

---

### ビュー

モデルをもとに実際の画面をつくってみましょう！

[elm/htmlライブラリ](https://package.elm-lang.org/packages/elm/html/latest/)を使います。

```elm
import Html exposing (..)
import Html.Attributes exposing (..)


view : Model -> Html Msg
view model =
    {-
        <div>
            <img src="./img/goat/normal.jpg">
            ...
        </div>
    -}
    div
        []
        [ img
            [ src <|
                case model.phase of
                    Start ->
                        "./img/goat/normal.jpg"

                    (後略)
            ]
            []
        -- メインメッセージ
        , h1
            []
            [ text <|
                case model.phase of
                    Start ->
                        "じゃ〜んけ〜ん"

                    AfterWin ->
                        "かち！"

                    (中略)

                    AfterClickingGoat ->
                        "じゃ〜んけ〜ん"
            ]
        -- サブメッセージ
        , h2
            []
            [ text <|
                case model.phase of
                    Start ->
                        "すきな ぼたんを おしてね！"

                    (後略)
            ]
        -- ボタンエリア
        , if model.phase == Start || model.phase == AfterClickingGoat then
            div
                []
                {-
                    <button type="button">ぐー</button>
                -}
                [ button
                    [ type_ "button"
                    ]
                    [ text "ぐー"
                    ]
                , button
                    [ type_ "button"
                    ]
                    [ text "ちょき"
                    ]
                , button
                    [ type_ "button"
                    ]
                    [ text "ぱー"
                    ]
                ]

          else
            div
                []
                [ button
                    [ type_ "button"
                    ]
                    [ text "もういっかい"
                    ]
                ]
        ]
```

---

### ビューをイベントと紐付ける

これだけでは、ユーザーのどんな操作がどのイベント(`Msg`)を引き起こすのかがわかりません。
`Html.Events` の関数を使ってイベントの紐付けを行います。


```elm
import Html.Events as Events

div
    []
    [ button
        [ type_ "button"
        -- ボタンを押した時のイベントを紐付ける
        , Events.onClick PressGuu
        ]
        [ text "ぐー"
        ]
    , ...
    , ...
    ]
```


```elm
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events as Events


view : Model -> Html Msg
view model =
    div
        []
        [ img
            [ src <|
                case model.phase of
                    Start ->
                        "./img/goat/normal.jpg"

                    AfterWin ->
                        "./img/goat/loser.jpg"

                    AfterDraw ->
                        "./img/goat/draw.jpg"

                    AfterLose ->
                        "./img/goat/winner.jpg"

                    AfterClickingGoat ->
                        "./img/goat/wild.jpg"
            , if model.phase == Start then
                Events.onClick ClickGoat

              else
                -- 何も属性がないときのテクニック
                class ""
            ]
            []
        , h1
            []
            [ text <|
                case model.phase of
                    Start ->
                        "じゃ〜んけ〜ん"

                    AfterWin ->
                        "かち！"

                    AfterDraw ->
                        "あいこ！"

                    AfterLose ->
                        "まけ！"

                    AfterClickingGoat ->
                        "じゃ〜んけ〜ん"
            ]
        , h2
            []
            [ text <|
                case model.phase of
                    Start ->
                        "すきな ぼたんを おしてね！"

                    AfterWin ->
                        "ヤギさんにも ようしゃが ないね！"

                    AfterDraw ->
                        "きが あうね！"

                    AfterLose ->
                        "ほもさぴえんすって よわい いきものだね！"

                    AfterClickingGoat ->
                        "やぎさんじゃなくて ぼたんをおしてね！"
            ]
        , if model.phase == Start || model.phase == AfterClickingGoat then
            div
                []
                [ button
                    [ type_ "button"
                    , Events.onClick PressGuu
                    ]
                    [ text "ぐー"
                    ]
                , button
                    [ type_ "button"
                    , Events.onClick PressChoki
                    ]
                    [ text "ちょき"
                    ]
                , button
                    [ type_ "button"
                    , Events.onClick PressPaa
                    ]
                    [ text "ぱー"
                    ]
                ]

          else
            div
                []
                [ button
                    [ type_ "button"
                    , Events.onClick PressAgain
                    ]
                    [ text "もういっかい"
                    ]
                ]
        ]
```

---

### しあげ

[elm/browserライブラリ](https://package.elm-lang.org/packages/elm/browser/latest/)を使います。

```elm
import Browser

main =
  Browser.sandbox { init = init, update = update, view = view }
```

これをくわえるだけ！

[完成したソース](https://github.com/arowM/elm-kansai1-tea/blob/master/src/Main.elm)

---

## リファクタリング

Elmは強い静的型とかしこいコンパイラによって守られているので、リファクタリングしてもデグレしにくいぞ！

---

### 関数を分けてみよう

ビューが長すぎて読めなぁあい！
別の関数に分けてみましょう。

```elm
view : Model -> Html Msg
view model =
    div
        []
        [ img
            [ src <| goatSrc model.phase
            , if model.phase == Start then
                Events.onClick ClickGoat

              else
                -- 何も属性がないときのテクニック
                class ""
            ]
            []
        , h1
            []
            [ text <| mainMessage model.phase
            ]
        , h2
            []
            [ text <| subMessage model.phase
            ]
        , buttons model.phase
        ]
```

めちゃんこ短い！見やすい！

別の場所に定義するだけ。

```elm
{-| ヤギ画像
-}
goatSrc : Phase -> String
goatSrc phase =
    case phase of
        Start ->
            "./img/goat/normal.jpg"

        AfterWin ->
            "./img/goat/loser.jpg"

        AfterDraw ->
            "./img/goat/draw.jpg"

        AfterLose ->
            "./img/goat/winner.jpg"

        AfterClickingGoat ->
            "./img/goat/wild.jpg"


{-| ボタンエリア
-}
buttons : Phase -> Html Msg
buttons phase =
    case phase of
        Start ->
            guuChokiParrButtons

        AfterClickingGoat ->
            guuChokiParrButtons

        _ ->
            againButtons


guuChokiParrButtons : Html Msg
guuChokiParrButtons =
    div
        []
        [ button
            [ type_ "button"
            , Events.onClick PressGuu
            ]
            [ text "ぐー"
            ]
        , button
            [ type_ "button"
            , Events.onClick PressChoki
            ]
            [ text "ちょき"
            ]
        , button
            [ type_ "button"
            , Events.onClick PressPaa
            ]
            [ text "ぱー"
            ]
        ]
```

---

## シナリオの改善

### 失敗シナリオ

1. __ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる__
2. 「じゃんけんゲームってことかな？」
3. とりあえず「ぐー」を押してみた
4. __「かち！」と表示され、ヤギが悔しがった顔をしながら「ヤギさんにも ようしゃが ないね！」と言ってくる__
5. 「ん？？ ヤギが負けて悔しがってるっぽいのに『かち！』っておかしくない？バグ？」

誰の視点で「かち！」と言っているかわかりにくいのが問題。
「あなたの かち！」に変えるだけでもいいけど、せっかくなのでユーザーの名前を聞いてみよう！

---

### シナリオex1

1. __「あなたの名前を教えてください」という案内とともに入力欄が表示されている__
2. 「なにこれ、個人情報とられそうで怖い |> _ <|」
3. よく見たら「結果の表示に使います」と書いてある
4. 「なんだ、じゃあ仮名でもいいのか」
5. 「ウラジーミル・ヤギスキィ」と入力して「決定」ボタンを押した
7. __ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる__
8. 「じゃんけんゲームってことかな？」
9. とりあえず「ぐー」を押してみた
10. __「ウラジーミル・ヤギスキィさんの かち！」と表示され、ヤギが悔しがった顔をしながら「ヤギさんにも ようしゃが ないね！」と言ってくる__
11. (後略)

---

## 画面設計(改)

### 基本画面構成

#### 設定画面

* メインメッセージ: ユーザーへのメインのメッセージを表示する
* サブメッセージ: ユーザーへの補助的なメッセージを表示する
* 入力欄: 入力ボックスや送信ボタンを表示する

#### ゲーム画面

* ヤギ: ヤギの写真が表示される
* メインメッセージ: ユーザーへのメインのメッセージを表示する
* サブメッセージ: ユーザーへの補助的なメッセージを表示する
* ボタンエリア: ボタンを表示する

---

### 初期状態

* [画面] 設定画面
    * メインメッセージ: 「あなたの名前を教えてください」
    * サブメッセージ: 「結果の表示に使います」
    * 入力欄:
        * 名前入力欄
        * 「決定」ボタン
* [ユーザー] 「仮名にしよ」
    * [イベントex1] 名前(= `name`)を入力してフォーカスを外した

---

### イベントex1

* [画面] 設定画面
    * メインメッセージ: 「あなたの名前を教えてください」
    * サブメッセージ: 「結果の表示に使います」
    * 入力欄:
        * 名前入力欄
        * 「決定」ボタン
* [ユーザー] 「これでよし！」
    * [イベントex2] 「決定」ボタンを押した

---

### イベントex2

* [画面] ゲーム画面
    * ヤギ: ノーマルやぎ
    * メインメッセージ: 「じゃ〜んけ〜ん」
    * サブメッセージ: 「すきな ぼたんを おしてね！」
    * ボタンエリア:
        * 「ぐー」
        * 「ちょき」
        * 「ぱー」
* [ユーザー] 「なにこれ、どれかボタン押せばいいの？」
    * [イベント1] ぐーを押した
    * [イベント2] ちょきを押した
    * [イベント3] ぱーを押した
    * [イベント4] ヤギを押した

---

### イベント1'

* [画面]
    * ヤギ: 悔しそうなやぎ
    * メインメッセージ: 「`name`さんの かち！」
    * サブメッセージ: 「ヤギさんにも ようしゃが ないね！」
    * ボタンエリア
        * 「もういっかい」
* [ユーザー] 「ああ、ヤギさんだからチョキしかだせないのか」
* [ユーザー] 「今度は負けてあげよう」
    * [イベント5] 「もういっかい」を押した

---

## 実装する(改)

### モデル

フェーズを変更し、変数`name`も保持できるようにしましょう！

```elm
-- あとで拡張できるようにレコード型にしておいてよかった！
type alias Model =
    { phase : Phase
    , name : String
    }


type Phase
    -- 初期状態 / イベントex1
    = Setting
    -- イベント5 / イベントex2
    | Start
    -- イベント1'
    | AfterWin
    -- イベント2'
    | AfterDraw
    -- イベント3'
    | AfterLose
    -- イベント4
    | AfterClickingGoat
```

---

### 初期状態

```elm
init : Model
init =
    { phase = Setting
    , name = ""
    }
```

---

### メッセージ/アップデート

イベントが2つ追加されます。

```elm
type Msg
    = PressGuu
    | PressChoki
    | PressPaa
    | ClickGoat
    | PressAgain
    -- イベントex1
    | ChangeName String
    -- イベントex2
    | SubmitName


update : Msg -> Model -> Model
update msg model =
    case msg of
        -- モデルの関係ないフィールド(`name`)はそのままにしておきたいので
        -- レコードの更新構文が便利
        PressGuu ->
            { model
                | phase = AfterWin
            }

        PressChoki ->
            { model
                | phase = AfterDraw
            }

        PressPaa ->
            { model
                | phase = AfterLose
            }

        ClickGoat ->
            { model
                | phase = AfterClickingGoat
            }

        PressAgain ->
            { model
                | phase = Start
            }

        ChangeName str ->
            { model
                | name = str
            }

        SubmitName ->
            { model
                | phase = Start
            }
```

---

### ビュー


```elm
view : Model -> Html Msg
view model =
    case model.phase of
        Setting ->
            settingView model

        _ ->
            gameView model
```

入力欄のイベントには `elm-community/html-extra` の `Html.Events.Extra.onChange` が便利。

```elm
import Html.Events.Extra as Events


settingView : Model -> Html Msg
settingView model =
    div
        []
        [ h1
            []
            [ text "あなたの名前を教えてください"
            ]
        , h2
            []
            [ text "結果の表示に使います"
            ]
        , input
            [ type_ "text"
            , value model.name
            , Events.onChange ChangeName
            ]
            []
        , button
            [ type_ "button"
            , Events.onClick SubmitName
            ]
            [ text "決定" ]
        ]
```

[完成したソース](https://github.com/arowM/elm-kansai1-tea/blob/master/src/Main3.elm)
[完成版](https://arowm.github.io/elm-kansai1-tea/extra.html)

---

## さらに学ぶには

[公式ガイドの日本語訳](https://guide.elm-lang.jp/) がとっても便利！

---

[さくらちゃんにご飯をあげる](https://www.amazon.co.jp/gp/registry/wishlist/20ZV2VC57615B/ref=nav_wishlist_lists_1)
[さくらちゃんをもっと見る](https://twitter.com/hashtag/%E3%81%95%E3%81%8F%E3%82%89%E3%81%A1%E3%82%83%E3%82%93%E6%97%A5%E8%A8%98?src=hash)
[さくらちゃんグッズを見る](https://www.amazon.co.jp/s?me=A1DAP5YJL8PV4M&marketplaceID=A1VC38T7YXB528)
[他の記事を見る](https://qiita.com/arowM)
![さくらちゃん](https://pbs.twimg.com/media/D-dYIqzU4AAAbAN?format=jpg&name=4096x4096)