10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Elm] CustomTypeを活用する

Last updated at Posted at 2019-09-02

背景

  • CustomTypeに引数を渡すことで状態管理がより適切にできるはず
  • CustomTypeを使わずに作ったものを、CustomTypeを使ってリファクタリングすることでそのメリットを学ぶ

作ってみたもの

基本的なTodoリスト

仕様

  • TODO追加ボタン押下で、TODO追加フォームが開く
  • 追加フォーム内の追加するボタン押下で、新規TODOが追加され追加フォームが閉じる
  • 登録済みTODOのテキストをクリックすると、TODO編集フォームが開く
  • 編集フォーム内の保存するボタン押下で、TODOが更新され編集フォームが閉じる
  • 追加/編集フォーム内のやめるボタン押下で、追加/編集を取りやめフォームが閉じる
  • 追加/編集フォームが開いている時、一度閉じてからでなくても連続して他のフォームを開くことができる
  • チェックボックス押下でチェックのON/OFFを変えることができる

リファクタ前のコード

Elm始めて1ヶ月くらいの時期に書いていたコードはこんな感じ

Model

type alias Model =
    { todos : List Todo
    , addFormS : FormS
    , addingTodo : Todo
    , editFormS : FormS
    , editingTodo : Todo
    }

init : ( Model, Cmd Msg )
init =
    ( { todos =
            [ { id = "xxx1"
              , text = "住みたい街を決める"
              , done = True
              }
            , { id = "xxx2"
              , text = "物件候補を探す"
              , done = False
              }
            , { id = "xxx3"
              , text = "内見の予約を取る"
              , done = False
              }
            ]
      , addFormS = Close
      , editFormS = Close
      }
    , Cmd.none
    )

Todo

type alias Todo =
    { id : Id   -- 可読性を上げるためにtype aliasで別名指定した普通のString
    , text : String
    , done : Bool
    }
    
init : Todo
init =
    { id = ""
    , text = ""
    , done = False
    }

FormS

  • uuid生成をjs側で行なっているので、一度Portを経由してjsからuuidを受け取るまでの間の状態をSendingとした。
type FormS
    = Open
    | Sending
    | Close

Msg

type Msg
    = OpenAddTodoForm
    | ChangeAddTodoText String
    | AddTodo
    | AddedTodo Todo
    | CancelAddTodo
    | OpenEditTodoForm Todo
    | ChangeEditTodoText String
    | UpdateTodoText
    | CancelUpdateTodoText
    | UpdateTodoDone Id

Update

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        OpenAddTodoForm ->
            ( { model | addFormS = Open, addingTodo = Todo.init }, Cmd.none )

        ChangeAddTodoText text ->
            let
                addingTodo =
                    model.addingTodo

                nextAddingTodo =
                    { addingTodo | text = text }
            in
            ( { model | addingTodo = nextAddingTodo }, Cmd.none )

        AddTodo ->
            ( { model | addFormS = Sending }, addTodo { text = model.addingTodo.text } )

        AddedTodo newTodo ->
            let
                nextTodos =
                    model.todos ++ [ newTodo ]
            in
            ( { model | todos = nextTodos, addFormS = Close, addingTodo = Todo.init }, Cmd.none )

        CancelAddTodo ->
            ( { model | addFormS = Close, addingTodo = Todo.init }, Cmd.none )

        OpenEditTodoForm todo ->
            ( { model | editFormS = Open, editingTodo = todo }, Cmd.none )

        ChangeEditTodoText text ->
            let
                editingTodo =
                    model.editingTodo

                nextEditingTodo =
                    { editingTodo | text = text }
            in
            ( { model | editingTodo = nextEditingTodo }, Cmd.none )

        UpdateTodoText ->
            let
                nextTodos =
                    List.map (\todo -> Todo.updateText model.editingTodo.id model.editingTodo.text todo) model.todos
            in
            ( { model | todos = nextTodos, editFormS = Close, editingTodo = Todo.init }, Cmd.none )

        CancelUpdateTodoText ->
            ( { model | editFormS = Close, editingTodo = Todo.init }, Cmd.none )

        UpdateTodoDone id ->
            let
                nextTodos =
                    List.map (\todo -> Todo.updateDone id todo) model.todos
            in
            ( { model | todos = nextTodos }, Cmd.none )

いけてないところ

追加中の新規TODOや編集中のTODOなど、「追加中」「編集中」という特定の状態のみでしか使われない変数を、一時的な値をmodelに直接持たせている

  • Update内でこれらを初期化する記述が頻出。初期化漏れによるバグの危険大
{ model | addFormS = Close, addingTodo = Todo.init }
{ model | editFormS = Close, editingTodo = Todo.init }
  • 以下のような意図しない更新ができてしまう
-- 編集中フォームを閉じたときに追加中TODOが初期化されるという期待しない処理が書けてしまう
{ model | editFormS = Close, addingTodo = Todo.init }
  • 一時的な値をmodelに持たせていることで、modelの視認性が下がっている

リファクタ後のコード

  • Elm3ヶ月くらいになった自分が過去の自分のコードをリファクタする
  • CustumTypeの引数を活用して、この問題を解決してみる

Model

  • めっちゃスッキリしてわかりやすいModelになった
type alias Model =
    { todos : List Todo
    , addFormS : AddFormS
    , editFormS : EditFormS
    }

FormS

  • AddとEditで分けてみた
  • リファクタ前はModelに直接追加中のTODOや編集中Todoの情報を持たせていたが、これらはフォームが開いている状態の時のみ持てばいい情報なので、OpenのCustomTypeの引数に持たせてみた
type AddFormS
    = AddFormOpen Todo
    | Sending
    | AddFormClose

type EditFormS
    = EditFormOpen Todo
    | EditFormClose
  • リファクタ前のコードでは、Formの状態と追加中(or編集中)のTODOの状態を別々の変数で持っていたため、Formの状態に関係なくTODOを書き換えたり初期化したりできてしまっていた。例えば追加中TODOのテキスト更新(ChangeAddTodoText)でeddingTodoの上書きができてしまうような実装になっていた。
  • CustumTypeの引数に追加中(or編集中)のTODOを持たせることで、フォームが開いている状態のみ追加中(or編集中)TODOの状態を扱えるようになり、よりわかりやすく仕様に合った実装になった。
OpenAddTodoForm ->
-    ( { model | addFormS = Open, addingTodo = Todo.init }, Cmd.none )
+    ( { model | addFormS = AddFormOpen Todo.init }, Cmd.none )

update

  • Modelはスッキリしたがupdateの記述量は増えた(CustumTypeの引数をcase文で取り出す部分でコード量が増えている)
  • 記述量は増えたが、updateで何をしようとしているのか、どんな状態更新をしようとしているのかがわかりやすくなった
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        OpenAddTodoForm ->
            ( { model | addFormS = AddFormOpen Todo.init }, Cmd.none )

        ChangeAddTodoText text ->
            case model.addFormS of
                AddFormOpen newTodo ->
                    ( { model | addFormS = AddFormOpen { newTodo | text = text } }, Cmd.none )

                _ ->
                    ( model, Cmd.none )

        AddTodo ->
            case model.addFormS of
                AddFormOpen newTodo ->
                    ( { model | addFormS = Sending }, addTodo newTodo )

                _ ->
                    ( model, Cmd.none )

        AddedTodo newTodo ->
            let
                nextTodos =
                    model.todos ++ [ newTodo ]
            in
            ( { model | todos = nextTodos, addFormS = AddFormClose }, Cmd.none )

        CancelAddTodo ->
            ( { model | addFormS = AddFormClose }, Cmd.none )

        OpenEditTodoForm todo ->
            ( { model | editFormS = EditFormOpen todo }, Cmd.none )

        ChangeEditTodoText text ->
            case model.editFormS of
                EditFormOpen newTodo ->
                    ( { model | editFormS = EditFormOpen { newTodo | text = text } }, Cmd.none )

                _ ->
                    ( model, Cmd.none )

        UpdateTodoText ->
            case model.editFormS of
                EditFormOpen newTodo ->
                    let
                        nextTodos =
                            List.map (\todo -> Todo.updateText newTodo.id newTodo.text todo) model.todos
                    in
                    ( { model | todos = nextTodos, editFormS = EditFormClose }, Cmd.none )

                _ ->
                    ( model, Cmd.none )

        CancelUpdateTodoText ->
            ( { model | editFormS = EditFormClose }, Cmd.none )

        UpdateTodoDone id ->
            let
                nextTodos =
                    List.map (\todo -> Todo.updateDone id todo) model.todos
            in
            ( { model | todos = nextTodos }, Cmd.none )

まとめ

  • 全部Modelに変数持たせようとすると煩雑になる
  • ある特定の状態でしか必要としない変数は、CustomTypeでその状態を定義し、その引数に持たせることで、ModelやUpdateがわかりやすくなる
  • 全体的に使用する変数はもちろん、Modelに持たせてよい
10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?