Боремся со слишком большими Msg в Elm приложениях +4


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




К сожалению, не доступен сервер mySQL