背景
- CustomTypeに引数を渡すことで状態管理がより適切にできるはず
- CustomTypeを使わずに作ったものを、CustomTypeを使ってリファクタリングすることでそのメリットを学ぶ
作ってみたもの
仕様
- 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に持たせてよい