Согласно Elm Architecture, вся логика приложения сконцентрирована в одном месте. Это довольно простой и удобный подход, но с ростом приложения можно увидеть функцию update
длиной 700 строк, Msg
с сотней конструкторов и Model
, не умещающуюся в экран.
Такой код довольно тяжело изучать и, зачастую, поддерживать. Я бы хотел продемонстрировать очень простой прием, который улучшит уровень абстракций в вашем приложении.
Давайте разберем простой пример.
Для начала создадим маленькое приложение с одним лишь текстовым полем. Полный код может быть найден здесь.
type alias Model =
{ name : String
}
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Name", value model.name, onInput ChangeName ] []
]
type Msg
= ChangeName String
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeName newName ->
{ model | name = newName }
Приложение растет, мы добавляем фамилию, "о себе" и кнопку "Сохранить". Коммит тут.
type alias Model =
{ name : String
, surname : String
, bio : String
}
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Name", value model.name, onInput ChangeName ] []
, br [] []
, input [ placeholder "Surname", value model.surname, onInput ChangeSurname ] []
, br [] []
, textarea [ placeholder "Bio", onInput ChangeBio, value model.bio ] []
, br [] []
, button [ onClick Save ] [ text "Save" ]
]
type Msg
= ChangeName String
| ChangeSurname String
| ChangeBio String
| Save
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeName newName ->
{ model | name = newName }
ChangeSurname newSurname ->
{ model | surname = newSurname }
ChangeBio newBio ->
{ model | bio = newBio }
Save ->
...
Ничего примечательного, все хорошо.
Но сложность резко возрастает, когда мы решаем добавить на нашу страницу еще один компонент, который совсем не связан с существующим — форма для собаки. Коммит.
type Msg
= ChangeName String
| ChangeSurname String
| ChangeBio String
| Save
| ChangeDogName String
| ChangeBreed String
| ChangeDogBio String
| SaveDog
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeName newName ->
{ model | name = newName }
ChangeSurname newSurname ->
{ model | surname = newSurname }
ChangeBio newBio ->
{ model | bio = newBio }
Save ->
...
ChangeDogName newName ->
{ model | dogName = newName }
ChangeBreed newBreed ->
{ model | breed = newBreed }
ChangeDogBio newBio ->
{ model | dogBio = newBio }
SaveDog ->
...
Уже на данном этапе можно заметить, что Msg
содержит в себе две "группы" сообщений. Мое "программистское чутье" подсказывает, что такие вещи нужно абстрагировать. Что вот случится, когда появится еще 5 компонентов? А подкомпоненты? Ориентироваться в этом коде будет почти невозможно.
Можем ли мы ввести этот дополнительный уровень абстракции? Конечно!
type Msg
= HoomanEvent HoomanMsg
| DoggoEvent DoggoMsg
type HoomanMsg
= ChangeHoomanName String
| ChangeHoomanSurname String
| ChangeHoomanBio String
| SaveHooman
type DoggoMsg
= ChangeDogName String
| ChangeDogBreed String
| ChangeDogBio String
| SaveDog
update : Msg -> Model -> Model
update msg model =
case msg of
HoomanEvent hoomanMsg ->
updateHooman hoomanMsg model
DoggoEvent doggoMsg ->
updateDoggo doggoMsg model
updateHooman : HoomanMsg -> Model -> Model
updateHooman msg model =
case msg of
ChangeHoomanName newName ->
{ model | name = newName }
-- Code skipped --
updateDoggo : DoggoMsg -> Model -> Model
-- Code skipped --
view : Model -> Html Msg
view model =
div []
[ h3 [] [ text "Hooman" ]
, input [ placeholder "Name", value model.name, onInput (HoomanEvent << ChangeHoomanName) ] []
, -- Code skipped --
, button [ onClick (HoomanEvent SaveHooman) ] [ text "Save" ]
, h3 [] [ text "Doggo" ]
, input [ placeholder "Name", value model.dogName, onInput (DoggoEvent << ChangeDogName) ] []
, -- Code skipped --
]
Утилизируя систему типов Elm мы разделили наши сообщения на два типа: человеческие и собачьи. Теперь порог вхождения в этот код станет гораздо проще. Как только какому-нибудь разработчику понадобится что-нибудь изменить в одном из компонентов, он сможет сразу по структуре типов определить, какие части кода ему нужны. Нужно добавить логику в сохранение собачьей информации? Погляди сообщения и запусти поиск по ним.
Представьте, что ваш код — это огромный справочник. Как вы будете искать интересующую вас информацию? По оглавлению (Msg и Model). Будет ли вам легко сориентироваться по оглавлению без деления на разделы и подразделы? Вряд ли.
Заключение
Это крайне простой прием, который можно использовать где угодно и довольно легко внедрить в существующий код. Рефакторинг существующего приложения будет совершенно безболезненный, благодаря статической типизации и нашему любимому elm-компилятору.
Потратив всего лишь час вашего времени (у нас на проекте я тратил меньше 20 минут на каждое приложение) вы можете значительно улучшить читаемость вашего кода и задать стандарт того, как нужно его писать в будущем. Хорош не тот код, в котором легко исправлять ошибки, а тот, который ошибки запрещает и задает пример того, как код должен писаться.
Точно такой же прием можно применить и к Model
, выделяя нужную информацию в типы. Например, в нашем примере можно модель разделить всего на два типа: Hooman
и Doggo
, сократив количество полей в модели до двух.
Боже, храни систему типов Elm.
P.S. репозиторий с кодом можно найти здесь, если вы хотите посмотреть diff-ы
Neftedollar
Почему это вообще должно быть в рамках одного компонента?
Почему это не один рутовый компонент который отображает два дочерних?
Nondv Автор
Что конкретно Вы имеете в виду?
Neftedollar
Есть подозрение, что не уловил правильно суть статьи, но обычно (в Fable-Elmish по крайней мере) происходит построение дерева компонентов.
как-то ну в разных файлах. в разных изолированных модулях. Это основная мысль
Nondv Автор
Да, такой подход имеет смысл, но реализовать его гораздо сложнее.
Разделение на файлы исключительно ради разделения на файлы ничего не приносит за исключением ненужного усложнения системы.
Если некоторую логику можно безболезненно выделить в отдельный модуль и при этом она совершенно полностью изолирована и, желательно, использована в нескольких местах — это определенно стоит сделать.
Но в целом хранение приложения в едином модуле не является плохой практикой в Elm. Во всяком случае, насколько мне известно. Достаточно придерживаться здравого смысла.
В посте же описан простой прием для улучшения разделения на уровни абстракции :)
P.S. рекомендую посмотреть доклад Эвана Чаплики — Life of a file (надеюсь, правильно название помню). Лично я со многим не согласен там, но, думаю, важно знать свои опции и идеи, стоящие за дизайном языка/фреймворка/etc