Согласно 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-ы