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