Есть у нас, допустим, компьютерная игра с разнополыми персонажами. Пол персонажа будет храниться в поле gender. Можно сделать это поле целым числом или строкой, но хороший программист стремится сделать некорректные состояния объектов невыразимыми, и поэтому, скорее всего, заведёт для пола enum. Теперь сделать персонажа с неправильным полом просто невозможно!


А ещё в игре есть база данных, в которой хранятся все игроки, и туда тоже нужно записывать, какого они пола. Было бы неплохо сделать так, чтобы персонажа с некорректным полом в базу данных добавить было нельзя, потому что с ней будет работать не только код, в котором для полов ничего, кроме enum, не предусмотрено, но ещё и живые люди. А живые люди, как известно, так и норовят вписать в базу данных то, что туда вписывать не надо.


К счастью, специально для таких случаев во многих СУБД также есть возможность делать колонки типа enum, тем самым ограничивая диапазон значений, которые могут иметь данные в такой колонке. Чем и воспользуется опытный разработчик, который не желает, чтобы в его проекте что-то было не по фэншую.


На клиенте программист тоже не дурак и тоже знает, что некорректные состояния должны быть невыразимы, и что для этого пол персонажа должен быть enum.


Идеально!


Однако если присмотреться, то можно обнаружить один небольшой изъян, который в итоге приведёт к проблемам. Поле, в котором хранится пол, называется не sex, а gender, а значит, к нему уже подбирается гейм-дизайнер. Рано или поздно он узнает, что гендеров, в отличии от полов, существенно больше двух, и захочет добавить гендер GENERIC_TEEN. Тут-то всем и поплохеет.


Клиента надо постоянно обновлять


Первыми под удар попадут ребята, которые писали клиентский код, потому что некорректные значения параметра гендер начнут приходить с сервера по каждому чиху. А когда с сервера придёт персонаж с гендером о существовании которого клиент не знает, то игра, как и хотел программист на клиенте, просто не запустится. Теперь при появлении новых гендеров необходимо релизить новые версии клиентов и запускать принудительные обновления, которые мы все так сильно любим.


На сервере enum — добро, потому что если там обнаружено какое-то неизвестное значение, то это явно ошибка, которую надо найти и исправить, а вот на клиенте появление неизвестного значения — штатная ситуация, которая будет регулярно случаться на устаревших версиях клиента, и поэтому там enum — зло, которое надо искоренить и уничтожить как можно скорее.


Новый код не сможет работать со старой базой


Кроме того, согласно рекомендациям лучших собаководов, новый код должен уметь работать со старой схемой данных, а тут получается, что вся система может развалиться только из-за того, что в код добавили новый гендер.


Поэтому, в базе данных enum нам мешает, и в данном случае надо хранить в поле строки, или инты, или что угодно, во что при необходимости можно записать значение, не предусмотренное заранее.


И кто-то скажет: ладно, ладно, на клиенте enum вредит, и в базе данных он больше мешает, чем помогает, но в серверном коде-то enum приносит большую пользу. Он же не будет предлагать убрать enum из кода на сервере? Будет!


Старый код не сможет работать с новой базой


Нынче программисту рекомендуется не только писать код так, чтобы он нормально работал со старой базой данных, но и развивать схему данных так, чтобы немного устаревший код тоже мог нормально с ней работать.


А если на сервере для гендера использовать enum, то всё опять развалится, только уже после того, как выяснится, что с новым кодом что-то не то и надо немедленно откатить всё до предыдущей версии, а новые гендеры в базу данных уже попали.


И поэтому enum из серверного кода мерным солдатским шагом идут куда-то в направлении ближайшего лесного массива, чтобы никогда оттуда не вернуться, ибо всё, что может развалить систему в неподходящий момент — зло, а злу не место в нашем уютном коде.


Так может, забить на enum и всегда использовать строки?


Короче, как сказал бы Винни-Пух – это какие-то неправильные енамы, и это они провоцируют программиста писать неправильный код! В enum надо помещать действительно незыблемые и фундаментальные вещи, которые не меняются годами, а не гендеры, которые добавляются и исчезают каждый релиз! Каждой задаче – свой инструмент, и enum – это не тот инструмент, который нужно использовать для хранения значений, возможный спектр которых постоянно расширяется.


Если переменная может принимать только какие-то заранее известные значения и всё, что не входит в эти значения, должно приводить к ошибке, то использовать enum – хорошая идея, но если это не так, если непредусмотренные значения регулярно будут попадать в переменную, то использовать enum равно выстрелить себе в ногу, и лучше этого не делать, если вы, конечно не мазохист.


Эта конкретная ситуация интересна тем, что ничто не предвещало добавления в enum новых значений, не говоря уже о том, что новые значения будут появляться регулярно. Откуда нам вообще было знать, что полов будет больше двух?


Поэтому, согласно принципу KISS, enum на первом этапе – хороший и правильный выбор. Опытный программист сделает enum и зафиксирует, что при появлении дополнительного значения нужно немедленно запустить мероприятия, необходимые для того, чтобы enum из кода исчез.


К сожалению, практика показывает, что на самом деле добавлением нового гендера будет заниматься кто-то другой, и он, недолго думая, добавит в enum новое значение, a на этом успокоится. Так что если есть какие-то сомнения, лучше следовать простому правилу: хранится где-то, кроме кода – значит, не enum, и на этом точка.


Получается, enum вообще не должно быть в коде, связанном с гендерами?


Несмотря на то, что я сейчас наговорил, enum в коде всё равно могут быть очень полезными, даже если в них находятся значения, которые попадают в базу данных. Не надо только использовать их для того, чтобы хранить значения, которые вытаскиваются из БД, надо сравнивать то, что пришло из БД с желаемым значением. Например, когда программист хочет написать, что если гендер персонажа – GENERIC_TEEN, то ему нельзя класть в инвентарь спиртные напитки, можно завести enum Gender с методом value, возвращающим строку, и написать код который будет проверять, равенство поля gender у персонажа и Gender.GENERIC_TEEN.value() и это будет хорошо, потому что не даст программисту сделать ошибку в значении статуса.


Код должен эволюционировать


Решения, хорошо служившие нам в какой-то момент, могут стать неудобными по прошествии времени. И нужно следить за тем, чтобы код отражал текущее состояние проекта, а не был набором костылей, с помощью которых решение, созданное на коленке пару лет назад, натягивается на “здесь и сейчас”. В противном случае он будет неисчерпаемым источником факапов и лулзов для всех, кто хоть как-то связан с проектом, будь то разработчик, тестировщик или непосредственный потребитель.

Комментарии (67)


  1. Griboks
    20.11.2019 12:21

    поэтому, скорее всего, заведёт для класса enum

    Для пола достаточно 1 бита или хотя бы булеана.

    А если говорить про enum, то всегда можно проверять его через сопоставление с образцом, тогда ошибка не возникнет (это ведь обёртка над типом). Т.е., если мы в {red=1,green=2} запишем значение 3, то ошибка в рантайме не возникнет, т.к. 3 принадлежит множеству значений int, однако компилятор может иногда ругаться.


    1. poxvuibr Автор
      20.11.2019 12:30

      Для пола достаточно 1 бита или хотя бы булеана.

      Теоретически бита хватит, но писать код для класса в котором для хранения пола будет использоваться один бит неудобно. Булеан в зависимости от платформы и языка программирования захочет кусок памяти от одного до четырёх байт, поэтому выигрыш от его использования сомнителен. Но если ситуация потребует, мы обязательно поправим код, у нас всё-таки ООП и доступ к свойствам осуществляется только через вызов методов.


      1. Griboks
        20.11.2019 12:34

        Теоретически бита хватит, но писать код для класса в котором для хранения пола будет использоваться один бит неудобно.

        Как вариант, возможно использовать битовые операции и хранить сразу вектор полов — в byte сразу 8 записей. Это не так сложно, и, думаю, даже даёт выигрыш производительности и памяти по сравнению с классическими типами.

        p.s. я подредачил коммент выше


        1. poxvuibr Автор
          20.11.2019 14:35

          Как вариант, возможно использовать битовые операции и хранить сразу вектор полов — в byte сразу 8 записей.

          Если у одного персонажа было бы много полов — возможно.


          Это не так сложно, и, думаю, даже даёт выигрыш производительности и памяти по сравнению с классическими типами.

          Если у одного персонажа один пол, то выигрыша не будет, если несколько, то наверное будет выигрыш по памяти, но не очень большой.


          1. Griboks
            20.11.2019 15:39

            А как вы смотрите на такую структуру?
            props:byte = sex*0x80 + age*0x7F //1 бит на пол + 7 бит на возраст = 1 байт

            Можно ведь много структур вставить. Впрочем, практически проще просто потратить немного больше оперативки.


            1. poxvuibr Автор
              20.11.2019 20:31

              Для уменьшения объёма трафика при передаче данных что-то такое удобно использовать, непосредственно в прикладном коде только когда по другому никак.


            1. KvanTTT
              21.11.2019 01:25

              Я думаю не так уж и долго осталось ждать человека, который переживет 127 лет.


      1. konraddd
        20.11.2019 15:29

        у нас всё-таки ООП и доступ к свойствам осуществляется только через вызов методов.

        Вы не представляете как у меня бомбануло от Ваших слов! Аргументируйте их, пожалуйста: почему, на Ваш взгляд, ООП против публичных полей..?


        1. poxvuibr Автор
          20.11.2019 15:34

          почему, на ваш взгляд, ООП против публичный полей..?

          Помимо того, что это противоречит принципу сокрытия данных, это ещё мешает писать код так, чтобы можно было менять реализацию без правки кода.


          Поэтому в ООП рекомендуется использовать интерфейсы в которых публичных полей вообще быть не может.


          1. Oxoron
            20.11.2019 19:06

            Как в вашу концепцию вписываются DTO?


            1. poxvuibr Автор
              20.11.2019 20:26

              Примерно так же, как примитивные типы. ДТО не являются объектами, что не мешает использовать их в ООП коде.


            1. ledocool
              21.11.2019 10:20

              Я слышал, что современный код, хоть и называется объектно-ориентированным, на практике является процедурным. А ООП — весьма специализированный инструмент, который еще надо знать где применять.

              В такую концепцию DTO прекрасно вписывается, на мой взгляд.


            1. VolCh
              22.11.2019 06:29

              Даже в DTO возникает необходимость соблюдать условия и инварианты, а потому DTO без публичных полей имеют право на жизнь. По крайней мере в языках где нет виртуальных свойств, которые непонятно куда отнести к полям или методам.


          1. VolCh
            22.11.2019 06:30

            Некоторые языки позволяют иметь публичные поля в интерфейсах.


            1. poxvuibr Автор
              22.11.2019 09:51

              А чем в таких языках интерфейс отличается от абстрактного класса?


          1. konraddd
            22.11.2019 17:14
            +1

            Эмм… если вам нужно «сокрыть данные» — объявляйте поля приватными.
            Насчёт «это ещё мешает писать код так, чтобы можно было менять реализацию без правки кода» — а как часто это нужно? Зачем пихать геттеры-сеттеры всюду, даже там где они не нужны?
            Из википедии:

            Объе?ктно-ориенти?рованное программи?рование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования
            Не вижу тут ни намёка на «сокрытие данных» и «возможность менять реализацию без правки кода».
            Основные принципы (из википедии):
            1) абстрагирование для выделения в моделируемом предмете важного для решения конкретной задачи по предмету, в конечном счёте — контекстное понимание предмета, формализуемое в виде класса;
            2) инкапсуляция для быстрой и безопасной организации собственно иерархической управляемости: чтобы было достаточно простой команды «что делать», без одновременного уточнения как именно делать, так как это уже другой уровень управления;
            3) наследование для быстрой и безопасной организации родственных понятий: чтобы было достаточно на каждом иерархическом шаге учитывать только изменения, не дублируя всё остальное, учтённое на предыдущих шагах;
            4) полиморфизм для определения точки, в которой единое управление лучше распараллелить или наоборот — собрать воедино.
            Опять ни намёка на Ваши тезисы.

            Итог: я просил Вас «аргументировать» Вашу точку зрения, но аргументов в Вашем ответе я не увидел.


            1. poxvuibr Автор
              22.11.2019 19:17

              Эмм… если вам нужно «сокрыть данные» — объявляйте поля приватными.

              А чтобы код был объектно ориентированным, открывать данные нельзя. Следовательно поля должны быть приватными.


              Насчёт «это ещё мешает писать код так, чтобы можно было менять реализацию без правки кода» — а как часто это нужно?

              Это нужно постоянно, чтобы можно писать тесты. Это если убрать случаи, когда это нужно для создания чистого кода.


              Зачем пихать геттеры-сеттеры всюду, даже там где они не нужны?

              Во-первых для того, чтобы можно было писать код, который принимает интерфейс, а не конкретную реализацию, во-вторых так принято. Объекты с геттерами и сеттерами для каждого поля с точки зрения ООП объектами являются. Это структуры.


              Не вижу тут ни намёка на «сокрытие данных»

              Ну вы почитайте википедию дальше, там сокрытие данных будет в инкапсуляции.


              и «возможность менять реализацию без правки кода».

              А тут можно почитать мой перевод статьи Роберта Мартина. Он хорошо изложил.


              я просил Вас «аргументировать» Вашу точку зрения, но аргументов в Вашем ответе я не увидел

              Странно, что не увидели, мои аргументы очень стандартны, их повторяют на каждом углу.


    1. Areldar
      20.11.2019 19:50

      Для пола достаточно 1 бита или хотя бы булеана.

      А потом вы решаете зарелизиться в какой нибудь стране из этого списка:
      en.wikipedia.org/wiki/Legal_recognition_of_non-binary_gender


      1. Griboks
        20.11.2019 20:15

        Я ведь написал про пол, а не гендер.


        1. juray
          23.11.2019 23:39
          +1

          С биологическим полом тоже не всё так просто.


  1. St_one
    20.11.2019 12:22
    -2

    Для гендеров как раз удобно использовать enum. Т.к. их всего два.


    1. Dim0v
      20.11.2019 13:29

      Во-первых, речь не про пол, а про гендер.


      Во-вторых, глупо приплетать свои убеждения к вполне себе техническим проблемам. И уж тем более отталкиваться от своих убеждений при решении таких проблем. Ведь будем честны, ваши слова про удобство использования enum-ов в этом случае вызваны совсем не объективными причинами, а вашими личными убеждениями. Если бы речь в посте шла не про гендеры, а, скажем, про рассы и геймдиз сказал бы добавить к негроидной, европеоидной и монголоидной расам еще австралоидную или даже рептилоидную — вы бы не стали писать комментарий "для рас как раз удобно использовать enum. Т.к. их всего три".
      Гейм-дизайнер из поста дал команду добавить поле не с целью глубоко оскорбить нежнейшие чувства St_one и силой вынудить его отказаться от своих безусловно правильных убеждений. Он это сделал по той простой причине, что это может быть полезно для игры и, как следствие, выгодно для компании.


      1. St_one
        20.11.2019 13:55

        Да, наверное вы правы. Разделил бы поля sex и gender, и для первого использовал enum =)


        1. Quber
          23.11.2019 22:17

          Предположим вы бы использовали enum для sex. Мужской и женский пол. Затем прошло какое то время, гейм дизайнер вводит новое понятие, унисекс (может быть и женского и мужского пола). Затем проходит время и добавляется еще одно значение, когда «пол не применим» к объекту. Потом через какое-то время еще одно значение, когда пол в силу каких то причин не может быть определен (юнит находится в тени). И так далее. Список может расти и для sex опять не выходит знать точное количество значений.


          1. St_one
            23.11.2019 22:35

            Если пол не применим к объекту, следует отделить объекты, к которым пол применим, для других enum просто не потребуется. Что вы имеете в виду под «унисекс»(ваше пояснение непонятно)? Объекты, находящиеся в тени так же не относятся к множеству тех, для которых применимо понятие пол.
            В конце концов можно добавить что-либо в перечисление, и если при откате на старую бд произойдет ошибка при работе с новым вариантом — это нормальная ситуация.


            1. Quber
              23.11.2019 22:51

              > Если пол не применим к объекту, следует отделить объекты, к которым пол применим, для других enum просто не потребуется.

              Интересно каким образом вы отделите объекты к которым пол не применим, если они хранятся в одной таблице и имеют одинаковые свойства с теми у которых пол применим? Создадите отдельную таблицу или класс под них?


              1. St_one
                23.11.2019 23:10
                +1

                Определенно, объекты, к которым применимо понятие «пол», будут реализовывать интерфейс HasSex, например. А как это будет в базе — другой вопрос, зависящий от модели данных бд, принятый подход к выбору стратегии отображения наследования, итд. В данном случае, если учесть что нужно сохранить совместимость(и обратные миграции не используются), в базе это можно отобразить как нулевое поле.


  1. adictive_max
    20.11.2019 13:03
    +1

    В enum надо помещать действительно незыблемые и фундаментальные вещи, которые не меняются годами
    До недавнего времени и GENDER и SEX были примерно синонимами, и это незыблемо и фундаментально не менялось тысячелетиями. Так что, уже ни в чём нельзя быть на 100% уверенным.


  1. ss-nopol
    20.11.2019 13:16
    +1

    А если на сервере для гендера использовать enum, то всё опять развалится, только уже после того, как выяснится, что с новым кодом что-то не то и надо немедленно откатить всё до предыдущей версии, а новые гендеры в базу данных уже попали.

    Вообще говоря, это немного сложнее. Если поставили старую версию на новую базу, то сломаться может много чего и много где. Что толку с того что старый код прочитает в строку гендер, если он всё равно не знает, что с ним делать и если не порушится, то при записи он опять этот гендер испортит.

    Здесь возможны несколько подходов.

    Подход №1 — проблема на стороне БД или утираем руки

    Для 100% корректного отката на старую версию нужно либо взять старую базу (из бэкапа, да-да) либо конвертировать новую базу в старую, либо комбинировать.

    Подоход №2 — просто и со вкусом, но только для enum

    В БД пишется строка или int, в коде — enum. Для совместимости (или для «прямой совместимости» — когда старая версия должна работать с новыми данными) код читающий из БД, сети и т.п., должен отлавливать некорректные значения и приводить их по возможности к корректным — в данном примере сделать из всех незнакомых, неправильных и отсутствующих значений знакомое, то есть если не male и не female, то присвоить male. Правда это сработает только для enum, далеко не для всех типов это возможно.

    Подоход №3 — проблема на стороне сервера или нудно, но совместимо и универсально

    При возникновении необходимости в расширении/изменении любого старого типа, вводится новое поле, например в данном случае gender2 или gender_extended. В котором будут сохраняться новые значения. Старый код будет читать/писать старый gender, новый код — оба поля.

    Со временем, когда старого кода не останется, можно будет старое поле удалить. При этом правильная реализация сериализации (как протобуф) заменит значение из отсутствующего поля значением по умолчанию, то есть даже после удаление поля из БД старый код будет работать, но все будут male.

    Этот подход будет работать и со всеми другими типами, причём любой сложности, а не только с enum, но требует соблюдения определённой дисциплины.

    Подоход №4 — просто, но не всегда приемлемо

    Для полноты добавлю — сервер пишет новую версию формата данных, а клиент выводит ошибку — обновите клиента, иначе работать не буду.

    Подведём итоги

    Таким образом, при соблюдении правильных подходов, можно и enum сохранить, как на сервере так и на клиенте, и соблюсти прямую и обратную совместимость, не теряя ни в производительности, ни в удобстве.

    Что из этих подходов выбирать — зависит от задачи и текущего момента, но enum тут вовсе не при чём.


    1. poxvuibr Автор
      20.11.2019 13:24

      Что толку с того что старый код прочитает в строку гендер, если он всё равно не знает, что с ним делать

      Я там писал, что нужно провести мероприятия для избавления от enum. В них в входит повление кода, который знает, что делать с незнакомыми значениями.


      Для 100% корректного отката на старую версию нужно либо взять старую базу (из бэкапа, да-да)

      И потерять изменения, которые сделаны в новой. Так делать можно только если у вас дата центр взорвали.


      код читающий из БД, сети и т.п., должен отлавливать некорректные значения и приводить их по возможности к корректным

      Зачастую сконвертировать это всё невозможно, нужно работать согласно существующим значениям, потому что они будут ключами в конфигах.


      При возникновении необходимости в расширении/изменении любого старого типа, вводится новое поле, например в данном случае gender2 или gender_extended.

      Это можно сделать, главное, чтобы gender_extended не объявили как enum


      Что из этих подходов выбирать — зависит от задачи и текущего момента, но enum тут вовсе не при чём.

      У нас кейс когда enum удобен превратился в кейс когда он неудобен, а вы говорите, что он тут не при чём ))


      1. ss-nopol
        20.11.2019 13:43

        Я там писал, что нужно провести мероприятия для избавления от enum. В них в входит повление кода, который знает, что делать с незнакомыми значениями.

        Но зачем избавляться от enum? И как написать код, «который знает, что делать с незнакомыми значениями»? Пойди туда, не знаю куда, принеси то, не знаю что? Код который предугадает появление новых гендеров в будущем? С этим даже полноценный ИИ не справится. Всё что можно сделать — прочитать и записать, ну вывести строку или номер. Если больше никакой функциональности не требуется, то возможно. Но, кто знает, надо просматривать весь код и проверять, как используется это поле. Кроме того, возможно в будущем появится код, который будет зависеть от конкретных значений.

        Максимум что можно сделать более-менее безопасно — привести к уже известным значениям. При этом можно добавить в enum кроме известных полов значение «unknown» или «undefined», к которым приводить все неизвестные. Но и тут кроется опасность, если, например, какие-то другие поля зависят от этого. То есть 100% совместимость невозможна, даже при избавлении от enum. Вопрос совместимости немного сложнее.

        Зачастую сконвертировать это всё невозможно

        Интересно, почему? В итоге всё равно enum сериализуется либо как int, либо как string. Так что как раз обычно это возможно.

        Это можно сделать, главное, чтобы gender_extended не объявили как enum

        В коде — не вижу никакой проблемы. А сериализовать лучше в строку или целое, да.


        1. poxvuibr Автор
          20.11.2019 14:32

          Но зачем избавляться от enum?

          Чтобы не терять значения когда они не попадают в enum.


          И как написать код, «который знает, что делать с незнакомыми значениями»?

          Незнакомые значения будут использоваться в качестве параметров поиска. Где-то в конфигурации будут заданы привязанные к гендеру параметры такие, например, как внешний вид. Ничего сверъестественного от кода в нашем кейсе не надо.


          Интересно, почему [нельзя нормально сконвертировать в другой enum] ?

          Потому что, как я уже сказал, будет потеряна информация. Нужно знать конкретный гендер.


          В коде — не вижу никакой проблемы. А сериализовать лучше в строку или целое, да.

          Если поле — enum то при десериализации при появлении неизвесиного значения либо будет ошибка, либо будет потеряна информация


          1. ss-nopol
            20.11.2019 15:40

            Если поле — enum то при десериализации при появлении неизвесиного значения либо будет ошибка, либо будет потеряна информация


            Вот это единственный случай, ради которого можно хранить исходную строку, из которой был сделан enum. То есть тот случай когда старый код десериализует, частично изменяет и затем сериализует данные сериализованные новым кодом и при этом потеря возможного значения енум критична. Кстати, енум при этом всё равно может быть полезен в коде (в Вашем — возможно нет).

            Но если присмотреться внимательно к этой ситуации, то несмотря на то что в данном конкретном случае в Вашей ситуации этот трюк сработал (строка используется только для поиска), в общем случае это не будет работать. Этот подход — не более чем трюк. В общем случае, если старый код вообще сможет десериализовать данные записанные новым кодом (это возможно, но для этого придётся приложить усилия, см. выше), то во-первых сериализовать он их будет точно по-старому, и если он не понимает новые данные, он их просто не запишет или исказит, а во-вторых даже со строковым енумом возможна ситуация, когда новые строковые значения приведут к непредсказуемой работе клиента или сделают эту работу невозможной.

            То есть как (а) заменять енум строкой всюду в общем случае не имеет смысла, так и (б) енум в виде строки это не панацея от всех возможных проблем совместимости. Это трюк, который может помочь в отдельном, очень конкретном и ограниченном случае. С такими оговорками принимается.


  1. HellMaster_HaiL
    20.11.2019 13:17

    Т.е. программист номер ноль, условно, выбрал не самое подходящее название для переменной (gender, a не sex), которое увидел геймдизайнер (какого хрена его вообще выпустили из экселей и дали ему доступ к БД?) и ему в голову прилетело, что это поле можно (и даже нужно) расширить согласно его больной фантазии. Геймдизайнер, ощущая себя самым важным павлином в курятнике, обходя архитектора, менеджера, тимлида и всех остальных, направляется к самому безответственному программисту номер «н» и ставит ему задачу «быстренько до обеда запилить вот маленькую фичу». Программист номер «н», не разбираясь в коде в силу своей лени или того фактора, что в проекте вообще не участвует, пилит костыль с заплаткой и деплоит все это дело, в лучшем случае, на стейдж. Но во всем виноваты энумы, да.


    1. poxvuibr Автор
      20.11.2019 13:31

      Ну нет, гейм-дизайнер молодец и плотно сотрудничает с разработчиками. Он вообще пол планировал использовать, чтобы к нему привязать внешний вид персонажа и возможный состав инвентаря, но не проартикулировал, что всего этого сделаем много. Ну а дальше уже сообщил разрабочикам, что надо добавить ещё один гендер, вот вчера.


      Тимлид одобрил хотфикс, архитектор одобрил тоже, но следующим таском поручили разработать нормальный механизм для работы с гендерами.


      А енумы вообще ни в чём не виноваты, виноват изменчивый мир.


  1. Virgo_Style
    20.11.2019 14:37

    Клиент получает идентификатор, э-э, типа существа и создает соответствующий класс, в котором уже зашито поведение существа. Причем если не знает, кто такой human.teen, создает просто human. Если вообще ничего не знает, создает заглушку default.
    Мне так видится.


    1. poxvuibr Автор
      20.11.2019 14:45

      Если с типом связана сложная логика, то лучше вообще разваливаться ))). Но тут достаточно загрузить параметры из конфига в зависимости от типа


      1. Virgo_Style
        20.11.2019 15:29

        В конфиге по условию задачи у нас еще пусто. Хотя загружать дефолтные значения можно и из конфига. Если никакой изощренной логики не ожидается, то класс действительно может быть излишним.
        С другой стороны, геймдизы могут быть так изобретательны…


        1. poxvuibr Автор
          20.11.2019 15:37

          В конфиге по условию задачи у нас еще пусто.

          Ну да, именно поэтому мы сначала сделали enum


          Если никакой изощренной логики не ожидается, то класс действительно может быть излишним.

          Я считаю, что класс-обёртка таки нужен. Хотя бы для того, чтобы сделать дедупликацию данных.


  1. Virgo_Style
    20.11.2019 15:58

    Я считаю, что класс-обёртка таки нужен.

    Да, лучше перестраховаться. Все всегда начинается невинно, есть мужчины и женщины, женщины носят платья. Потом появляются какие-нибудь русалки, которые женщины, но платьев не носят. Потом появляются собаки, которые вообще ничего не носят. Через неделю выясняется, что собак у собак есть породы, штук десять разных.
    С содроганием вспоминаю конфиг предметов в одной игре, над которой мне довелось работать.


    1. vassabi
      21.11.2019 14:25

      Все всегда начинается невинно, есть мужчины и женщины, женщины носят платья. Потом появляются какие-нибудь русалки, которые женщины, но платьев не носят. Потом появляются собаки, которые вообще ничего не носят. Через неделю выясняется, что собак у собак есть породы, штук десять разных.
      коллега по духу! жму руку!

      а мы вместо расширения существующих добавляли новые поля (с null как значения по умолчанию).


  1. Arb9i
    20.11.2019 16:35

    А почему не создать отдельную таблицу-каталог для гендеров(ид-название)?
    Появился новый гендер, добавили в таблицу.
    Для пользователя отображаем только название, так что он может выбрать только и списка.
    В таблице персонажей сохраняем ид гендера из каталога.


    1. poxvuibr Автор
      20.11.2019 16:37

      А почему не создать отдельную таблицу-каталог для гендеров(ид-название)?

      В самом начале не создать потому, что возиться не хочется. Полов 2 и будет 2 всегда )))
      А потом не создать потому что удобно брать гендеры из конфига. Гейм дизайнеру с ним легче, чем с таблицей БД. А так конечно подход хороший.


    1. Stawros
      20.11.2019 16:58

      Wargaming решали проблему неконсистентности значений у себя переходом на enum, я задавал им такой же вопрос. Для них enum проще, ALTER TYPE… ADD и новый тип добавлен.


  1. slowsort
    20.11.2019 17:00

    Теперь при появлении новых гендеров необходимо релизить новые версии клиентов и запускать принудительные обновления, которые мы все так сильно любим.


    А откуда без обновлений клиента взять всякие скины, анимации итд для нового типа GENERIC_TEEN?


    1. poxvuibr Автор
      20.11.2019 17:01

      А откуда без обновлений клиента взять всякие скины, анимации итд для нового типа GENERIC_TEEN?

      При загрузке сходит на сервер и спросит, какие гендеры есть и откуда качать скины, анимации и всё такое


  1. BugM
    20.11.2019 20:47

    А в чем проблема падения клиента при получении неизвестного гендера? Он все равно не знает даже как его показать пользователю. Перевода нет, картинки нет ничего нет. Упасть это нормально.


    Лучше пусть в тест или препрод сборке все сразу упадет, чем гадать заметит кто или нет. Fail fast.


    1. poxvuibr Автор
      20.11.2019 21:32

      А в чем проблема падения клиента при получении неизвестного гендера?

      В том, что всё упадёт.


      Он все равно не знает даже как его показать пользователю. Перевода нет, картинки нет ничего нет. Упасть это нормально.

      Он знает, это берётся из конфигурации. Падать тут нет никакого смысла.


      1. BugM
        21.11.2019 01:32

        В том, что всё упадёт.

        Так это же наоборот хорошо. Вместо кучи трудноуловимых багов в странных местах у нас будет один простой который ловится сразу. Красота.

        Он знает, это берётся из конфигурации. Падать тут нет никакого смысла.

        А новая конфигурация откуда возьмется? При обновлении ресурсов можно и клиента обновить. Если клиента не обновили, то и конфигурация старая.


        1. poxvuibr Автор
          21.11.2019 02:11

          Так это же наоборот хорошо [что всё упадёт] .

          Я же несколько раз открытым текстом написал, что это плохо ))


          Вместо кучи трудноуловимых багов в странных местах у нас будет один простой который ловится сразу.

          А можно сделать так, что и трудноуловимых багов не будет и простого тоже не возникнет и вообще не будет проблем при добавлении гендеров.


          А новая конфигурация откуда возьмется?

          С сервера конечно.


          При обновлении ресурсов можно и клиента обновить.

          Но лучше всё-таки не без необходимости не обновлять.


          Если клиента не обновили, то и конфигурация старая.

          Если обновлять клиента при каждом обновлении конфигурации. то никаких обновлений не напасешься.


          1. BugM
            21.11.2019 22:11

            Я же несколько раз открытым текстом написал, что это плохо ))

            Я так и не понимаю чем предсказуемое падение в предсказуемом месте хуже чем мерцающие баги по всему приложению? Тестирование такое падение найдет 100%. Да даже автотесты его найдут скорее всего. Программист не тратя лишнего времени сразу определит причину и скажет когда оно будет устранено. Одни плюсы.

            Но лучше всё-таки не без необходимости не обновлять. Если обновлять клиента при каждом обновлении конфигурации. то никаких обновлений не напасешься.

            С чего бы это? Трафик околобесплатный, каналы быстрые, торренты изобретены, магазины приложений работают. Девопс с минимальным релизным циклом уже везде. Обновлять это хорошо.

            С сервера конечно.

            То есть в реалтайме ну или при случайном запуске подкачивать данные это ок. А в заранее запланированное время неторопясь выкачать обновление не ок. Странно это.


            1. poxvuibr Автор
              21.11.2019 23:48

              Я так и не понимаю чем предсказуемое падение в предсказуемом месте хуже чем мерцающие баги по всему приложению?

              Предсказуемое падение в предсказуемом месте лучше, чем мерцающие баги. А ещё лучше, если и багов не будет и падать в предсказуемом месте тоже никогда не будет. Даже программиста не надо спрашивать, когда проблема будет устранена, потому что проблемы нет. Гейм дизайнер может даже не просить программиста поправить код, когда надо попробовать новый гендер. Одни плюсы ))


              1. BugM
                23.11.2019 14:02

                Код без ошибок. Вместо разработки дизайнер быстро меняет конфиг и все работает. И прочие мифы о разработке.

                Infrastructure as code уже везде. Fail fast — один из основополагающих принципов разработки. Строгую типизацию уже даже в Питон завозят. Складываем два и два.

                В конфигах в итоге остаются те вещи которые отличаются для разных сред или нод. И в конфигах ручки коротые можно крутить в рантайме. Желательно чтобы это были разные конфиги. Остальное меняем только обновлением. Обновления надо уметь выкатывать быстро и без проблем.


                1. poxvuibr Автор
                  23.11.2019 14:43
                  +1

                  Код без ошибок. Вместо разработки дизайнер быстро меняет конфиг и все работает.

                  Код без ошибок не бывает, а вот кейсы когда дизайнер просто меняет конфиг повсеместно распространены и работают у всех.


                  И прочие мифы о разработке.

                  Типа мифа о коде в котором что-то может сломаться только в одном месте? ))


                  Infrastructure as code уже везде.

                  Да, кто бы спорил. Конфиги надо комитить и их таки комитят


                  Fail fast — один из основополагающих принципов разработки.

                  Это принцип не рекомендует фейлиться там, где никаких причин для фейла нет.


                  В конфигах в итоге остаются те вещи которые отличаются для разных сред или нод.

                  Ну то есть вещи типа гендеров.


                  1. BugM
                    23.11.2019 19:28

                    Не убедили. Мое дизайн ревью этот подход не прошел бы.

                    Проблемы:
                    Усложнение разработки бизнес логики.
                    Увеличение шансов на ошибки в проде.
                    Усложнение тестирования.
                    Нарушение принципа «Не пиши тот код который сейчас не нужен».
                    Нарушение принципа «Fail fast»

                    Потенциальные плюсы:
                    Экономия трафика на обновлениях. Обходим через CDN или пиринг. Если это реально проблема.
                    Радость пользователей что не надо качать много и внезапно. Обходим выкладывая обновления клиента заранее и качаем в фоне.
                    Релиз сервера не зависит от релиза клиента. Налаживаем релизные процессы. Чтобы все были в курсе и заранее договаривались что когда и как релизится.
                    Нужны доработки клиента при релизах сервера. Это просто нормально и не является проблемой.


                    1. poxvuibr Автор
                      23.11.2019 22:49

                      Усложнение разработки бизнес логики.

                      Если предствить гендеры как enum, то разработка бизнес логики будет усложнена, потому что при добавлении нового гендера придётся обязательно править код.


                      Увеличение шансов на ошибки в проде.

                      За счёт чего увеличатся шансы на получение ошибки в проде?


                      Усложнение тестирования.

                      За счёт чего усложнится тестирование?


                      Нарушение принципа «Не пиши тот код который сейчас не нужен».

                      Этот код на момент написания уже нужен.


                      Нарушение принципа «Fail fast»

                      Как отказ от использования enum нарушает принцип Fail Fast?


                      Плюсы:


                      Нужны доработки клиента при релизах сервера. Это просто нормально и не является проблемой.

                      Релизы клиента нужны при правке конфигов, а не при релизах сервера. И это ни разу не нормально.


            1. sergey-b
              23.11.2019 13:34

              В чем проблема сделать код, работающий с любыми значениями? Никаких мерцающих багов быть просто не должно.


              1. BugM
                23.11.2019 14:04

                В том что такой код сложно писать и сложно тестировать?
                Любые значения это фаззинг тесты. Вы часто их пишете в своих проектах?


                1. poxvuibr Автор
                  23.11.2019 14:33

                  В том что такой код сложно писать и сложно тестировать?

                  И в чем сложность написать и оттестировать такой код? Чем написать такой код сложнее, чем код, который использует enum?


                  Любые значения это фаззинг тесты. Вы часто их пишете в своих проектах?

                  Тут нужно протестировать не для любых значений, а для значений, которые не входят в множество ожидаемых и наоборот.


                1. sergey-b
                  23.11.2019 15:02

                  Код с енумом


                  MyEnum value = MyEnum.valueOf(stringValue);

                  Код без енума


                  Optional<MyClass> value = MyClass.getInstanceByName(stringValue);

                  или


                  MyClass value = MyClass.getInstanceByName(stringValue);

                  Чего тут писать-то? Напишите тест, что на случайных значениях stringValue вы получаете допустимое значение value. В первом случае, это пустой Optional, во 2-м не null.


                  Разница только в том, что при использовании enum, вы вызвали стандартный метод valueOf, или за вас это сделал какой-нибудь jackson, а без него вам нужно чуточку подумать, о том как обрабатывать "некорректные" значения, и написать свой метод и тест к нему.


                  1. BugM
                    23.11.2019 19:23

                    Только вот потом это значение попадает в поле класса и на него завязывается бизнес логика. В случае с ограниченным набором у нас есть ограниченный набор инвариантов, которые легко покрываются тестами. В случае неограченного набора весь код использующий это значение должен об этом знать и корректно обрабатывать.


                    Сделать это реально, но зачем?


  1. vintage
    21.11.2019 06:25

    Надо всё же различать енум как тип-перечисление и энум как набор значений для которых у нас есть особая логика работы приложения. Первый сложно расширять. А второй не обязан быть исчерпывающим.


  1. sergey-b
    23.11.2019 12:49

    Енумы очень сильно дискредитированы сериализаторами. Приходит какой-нибудь json с массивом из 100 нормальных объектов, но вот 101-й объект содержит какой-нибудь новый код из енума. Этот json получит десериализатор, распарсит 100 объектов, а на 101-м выкинет ParseException, потому что ему в поле типа енум надо положить значение, которого он не знает. В итоге до приклада данные вообще не дойдут. И нельзя будет данную ситуацию ни залогировать нормально, ни обработать.


    1. poxvuibr Автор
      23.11.2019 14:31

      Особенно хорошо получается, когда объекты с енамими нужны были только для того, чтобы передать их соседнему микросервису, который этот новый емам знает


      1. sergey-b
        23.11.2019 15:06

        Именно так. Микросервис с енумом находится посередине между источником и потребителем. Ему самому это енум вообще ни для чего не нужен. Он берет данные из источника, обогащает их чем-то своим и возвращает потребителю. В результате любое изменение любого енума порождает каскадные доработки и обновления по всей инфраструктуре.


  1. sergey-b
    23.11.2019 13:31

    Один человек как-то раз снял деньги в банкомате в Белоруссии. После этого он не мог войти в интернет-банк, потому что там на главной странице показываются последние операции, а сервер, готовящий данные для главной страницы, содержит енум USD, EUR, RUB, GBP.
    Поддержка неделю думала, чем ему помочь, а потом предложила ему снять в банкомате российские рубли 10 раз. Тогда операция с белорусскими рублями не попадет в список последних операций, и главная страница интернет-банка наконец-то отрисуется.


  1. ekha
    24.11.2019 00:06

    Чот я не совсем понял. Есть enum для некоторого списка. Есть поле в БД, которое это хранит. Но при этом фиг знает, что из БД может прийти, мол, кто угодно туда всякое понапишет. Скорее всего, что-то пропустил, но правильно ли я понял, что в эту БД доступ напрямую имеют и некий «сервер» и некие «клиенты»? Вот просто иначе вообще бред получается. Таки enum в большинстве ЯПов — это чо-то производное от byte, int и т.д. (поправьте, плиз, учту) Т.е. вы без свои специальных проверок все равно можете внутри свойств получить что-то, чего нет в самом enum. И это вполне нормально, если не абстрагирующего слоя для доступа к БД.
    Второй момент — я ни разу не встречал кода (кроме логгера), который бы значение enum-а использовал напрямую — везде IF-ы и SWITCH-и и их аналоги. Т.е. каждое значение в каждой ситуации обабатывается индивидуально — на то они как бы созданы эти списки.