Pull to refresh

Comments 99

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

Итог: я просил Вас «аргументировать» Вашу точку зрения, но аргументов в Вашем ответе я не увидел.
Эмм… если вам нужно «сокрыть данные» — объявляйте поля приватными.

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


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

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


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

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


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

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


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

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


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

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

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

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


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

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

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

Интересно каким образом вы отделите объекты к которым пол не применим, если они хранятся в одной таблице и имеют одинаковые свойства с теми у которых пол применим? Создадите отдельную таблицу или класс под них?
Определенно, объекты, к которым применимо понятие «пол», будут реализовывать интерфейс HasSex, например. А как это будет в базе — другой вопрос, зависящий от модели данных бд, принятый подход к выбору стратегии отображения наследования, итд. В данном случае, если учесть что нужно сохранить совместимость(и обратные миграции не используются), в базе это можно отобразить как нулевое поле.
Предположим, что функция hasSex отвечает у нас за применимо или не применимо понятие «пол». А сам пол мы отобразим в базе как нулевое поле. Через какое то время у нас появляется новое понятие, пол «не известен». Итого у нас есть «пол не применим», «пол не определен», «пол не известен», «другой». Нулевое значение у нас уже занято. Функция hasSex задействована. Каким образом вы будете решать появление нового значения «не известен»? Разумеется разделять эти понятия не имеет смысла, так как у нас объект не может иметь одновременно и «мужской пол» и статус «не определен» или «женский пол» и статус «пол не применим».

Я просто хочу вам сказать, что вы можете ошибаться, когда думаете что знаете точное количество значений, на примере поля sex. В процессе разработки, например компьютерной игры, могут появляться новые значения, которые на этапе планирования не заложили. А соответственно тогда и начнутся те самые проблемы, которые описал автор в своей статье.
Для этого есть специальный термин, называется overengineering. Всё предугадать невозможно, и в данном случае использование enum будет вполне оправдано.
«Не определен» это не пол. Если есть такое поле, значит есть и другие поля, которые «не определены», и нужно вводить тип для отображения этого состояния на другие объекты. Возможно, «не определен» будет возраст. Что тогда — не будем использовать int для хранения возраста?
Можно сделать тип Optional/Maybe a, и отображать пустое значение в базе как null.
null для пола не занят, как Вы ошибочно предположили. Для сущности HasSex null можно трактовать как пустое значение NotPresent/Nothing, а для сущности не HasSex его вообще не надо никак трактовать.
> «Не определен» это не пол.

Это не пол, но значение вполне может храниться в поле sex.

> Если есть такое поле, значит есть и другие поля, которые «не определены»

Абсолютно не значит. Других полей может не быть, которые не определены. Если у вашего объекта всего два поля «имя» и «пол». Имя известно, пол не определен. Допустим вы добавите отдельное поле, например, булево отражающее определен пол или не определен, тогда вы можете столкнуться с ситуацией когда ваш объект будет иметь пол «мужской» и тип «пол не определен».
Чем Вас не устраивает решение с Optional? Даже для одного поля, это будет работать. Не нужно захламлять Enum лишними сущностями, подобную логику возможно использовать и для других полей.
Устраивает. Но optional мы сможем применить только один раз по отношению к полю. А значений у нас может быть много: не определен, не известен, не применим и т.д. И это разные по смыслу и назначению вещи.

И все они применимы не только к данному enum, а значит нужно более общее решение

Ну допустим. А если нас появится пол «унисекс», когда персонаж может иметь мужской и женский пол, например, для того чтобы в sims 4 можно было одевать разную одежду. И это может быть «другое» как у автора статьи на скриншоте. И тогда мы опять приходим к тем проблемам, которые обозначил автор статьи.
То, что перечислил автор — не проблемы. Обновить клиент? Это проблема?
Ошибка при десериализации нового значения в базе на старом клиенте? Это нормально, так и должно быть.
Тип это в любом случае ограничение, на которое можно опираться в дальнейшем. Если это становится невозможным, выход один — рефакторинг, пускай он иногда и бывает сложным.
Опять же, «унисекс» это не пол. И если предусматривать возможность отвязки одежды от пола, это не означает что нужно убирать пол у персонажа. Как в ПАБГ, где мужчина может надеть платье, но выглядеть оно будет как платье на мужике. Таким образом типы делают своё дело, ограничивая нас от необдуманной логики «унисекс», которую можно не реализовывать.

Вы можете называть это как угодно. Сути не меняет.

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

> А просто не использовать поле не по назначению.
Если вы считаете, что поле sex может содержать значения только «мужской» и «женский», а всё остальное это «не по назначению», то вы ошибаетесь. Если взять стандарт ISO 5218 то поле sex может иметь не 2, а 4 значения.

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

Простите, что вмешиваюсь, но не вижу проблемы в вашем описании. Енам — это не бит, он легко кастуется в инт, и раз уж мы в треде с тегом C#…

    [Flags]
    public enum MySexType
    {

        UNKNOWN = 0,
        MASCULINE = 1,
        FEMININE = 2
    }

    MySexType guy = MySexType.MASCULINE;
    MySexType girl = MySexType.FEMININE;
    MySexType someoneInTheShadow = MySexType.UNKNOWN;
    MySexType unisex = MySexType.FEMININE | MySexType.MASCULINE;


И да, я считаю, что в енамах значение по умолчанию всегда должно быть неопределённым. Иначе потом неопределённым может стать уже поведение программы.
> И да, я считаю, что в енамах значение по умолчанию всегда должно быть неопределённым.
Неопределенным или неизвестным? Это разные вещи. Оба варианта могут быть присущи полю типа enum.

Зависит от ситуации и куда мы его употребляем :).


Смотрим внезапно...https://developer.android.com/about/versions/14/features и https://developer.android.com/reference/android/app/GrammaticalInflectionManager
Там 4 варианта (речь правда про grammatical gender) в GrammaticalInflectionManager (еще добавлены не указанный и нейтральный) а в описании ICUшного API ( https://developer.android.com/reference/android/icu/text/SelectFormat#using-selectformat-for-gender-agreement ) все еще немного сложнее


Если имелось ввиду что полов два — стоило наверно так и говорить. И при этом для игры это будет неправильно.
Ну например — какого пола (и гендера если в даннном случае — ответ другой) и почему...


  • Назара и прочие Жнецы? (тоже массэффект)(если что — там размножение совсем другим способом, не половым)
  • Р. Дэниэл Оливо (Азимов, скажем что мужского потому что выглядит так?), хорошо а Гея из того же цикла произведений?
  • Гея (да собственно и почти все разумные с SI(Singularity Index):2 и вообще все разумные с SI:3 или выше)(из Orion's Arm Universe Project, там правда как раз в документации даже табличка есть какие гендеры в сеттинге есть и как правильно использовать, шесть чтоли вариантов, и нет — всяких там "боевых вертолетов апач" нет даже близко)
  • HAL900
  • Андромеда из одноименного сериала
  • Все Старшие Демиурги из одноименного цикла Евгения Лотоша (СИ/AT)(в принципе важное на эту тему — в подцикле про "современный" временной период Текиры + повесть-приквел цикла). Аналогичный вопрос про Младших (и да — там чуть другая ситуация, нет дело не в том что тела построены по другому принципу, как раз тела то у Младших такие же)
  • Ирис и Агата в дилогии "Великая Империя" Плотникова
В enum надо помещать действительно незыблемые и фундаментальные вещи, которые не меняются годами
До недавнего времени и GENDER и SEX были примерно синонимами, и это незыблемо и фундаментально не менялось тысячелетиями. Так что, уже ни в чём нельзя быть на 100% уверенным.
Cлово «гендер» было придумано в 1955 году сексологом Джоном Мани (по крайней мере, так написано в Википедии), поэтому ни о каких тысячелетиях речи идти не может.
И кроме того, это просто разные понятия. Пол — это понятие из биологии, представляющее из себя набор характеристик организма, принадлежащего к конкретному виду. Гендер — это изначально грамматический род (лат. genus), и даже в русском языке есть средний род («оно»).
А если на сервере для гендера использовать enum, то всё опять развалится, только уже после того, как выяснится, что с новым кодом что-то не то и надо немедленно откатить всё до предыдущей версии, а новые гендеры в базу данных уже попали.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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


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

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

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

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


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


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

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

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

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

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


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

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

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

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

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

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

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


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

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

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


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

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

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


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

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

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

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

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

А новая конфигурация откуда возьмется? При обновлении ресурсов можно и клиента обновить. Если клиента не обновили, то и конфигурация старая.
Так это же наоборот хорошо [что всё упадёт] .

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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


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

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

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

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

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

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


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

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


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

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


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

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


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

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


Плюсы:


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

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

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

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

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


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

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

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

Для энума тесты с невхождением затронут только десериализатор. Проверяем что он дает ожидаемую ошибку и все. Остальной код не затрагивается такими тестами вообще.
Со строками надо весь код тестировать. Мало ли как кто где эту строку заиспользет. Надо убедится что инжектов нигде нет, что вся бизнес логика корректно кидает ошибки. И при изменении бизнеслогики эти тесты надо обновлять. Итого и тесты сложнее и бизнес логика сложнее.
Энум гарантирует набор инвариантов. Строка, причем строка полученная из интернета, в бизнес логике дает кучу боли на пустом месте.

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

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

Мне только всякие ужасы с кодогенерацией в голову приходят.

Код с енумом


MyEnum value = MyEnum.valueOf(stringValue);

Код без енума


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

или


MyClass value = MyClass.getInstanceByName(stringValue);

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


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

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


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

Нужно придерживаться простого правила. Никакие значения не должны протекать в код. Если встречаете в коде


if (value.getName().equals("someSpecialValue")) {
    doSomeSpecificFeature();
}

или


if (value == MyEnum.SOME_SPECIAL_VALUE) {
    doSomeSpecificFeature();
}

то это ошибка. Так делать нельзя ни с енумами, ни без енумов. Вместо этого код должен выглядеть так:


if (value.needSomeSpecificFeature()) {
    doSomeSpecificFeature();
}

Связь needFeature -> doFeature никогда не изменится, поэтому данный код никогда не сломается. А признак needFeature в классе MyClass может быть присвоен любому экземпляру этого класса. Логика этой привязки может отличаться, но она должна быть инкапсулирована в коде MyClass или его фабрике. Это может быть что угодно, поле базы данных, настройка в файле, запрос внешнего источника или хардкодинг. Конкретная реализация не имеет значения, так как на остальной код она не влияет никак.


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


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

У вас NPE в первом же примере.

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

Читаемость ухудшилась? Ну и ладно, зато все по правилам. Читаемость ухудшится совершенно точно. Лишний слой абстракции через который надо продираться. Лет через 5 разработки точно найдется место где имя функции не совпадает со значением из энума.

Стримы с таким подходом тоже использовать не выйдет. .map(Class::getVal) запрещен. Вся вот эта функциональща без сайд эффектов никому же не нужна.

Ну и рефакоринг ради рефакторинга. Это прямо звезда на вершине. Вычищать что-то из большого проекта. Причем вычищать по формальным правилам. Зачем? Кому от этого станет лучше? False positive срабатываний у таких правил будет слишком много.

У меня уже давно нет проблемы с NPE. Не знаю, где вы тут его увидели.


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


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


Зато я знаю, что бывает, если делать так, как предлагаете вы, на периоде 5+ лет. Код будет выглядеть так:


18+
if (value == MyEnum.V1 || value == MyEnum.V2
        || value == MyEnum.V3) {
    // кусочек кода фичи1
    // кусочек кода фичи2
    // кусочек кода фичи3
    // кусочек кода фичи1
    if (value != MyEnum.V3) {
        // кусочек кода фичи2
    }
}

if (value == MyEnum.V1 || value == MyEnum.V3
        || value == MyEnum.V2 || value == MyEnum.V5) {
    // кусочек кода фичи4
    // кусочек кода фичи2
    // кусочек кода фичи3
    // кусочек кода фичи1
    if (value == MyEnum.V5) {
        // кусочек кода фичи5
    }
}

Попробуйте добавить в MyEnum значение V6 с фичами 1, 2, 4 и 6, при этом новая фича 6 должна выполняться так же и для V4, а для V3 пока нет, но известно, что через полгода фича 6 и для V3 тоже понадобится.

Я правильно понял что у вас там что-то вроде
interface IMegaData { String val; get;}
class MegaDataVal1 implements IMegaData {val = "MegaDataVal1";}
class MegaDataVal2 implements IMegaData {val = "MegaDataVal2";}

В таком случае простейшая задача перебора всех возможных значений уже становится нерешаемой без костылей.
Код становится писать сложно. Хочется поставить точку и увидеть варианты. Автокомплит вот это вот все.
Хочется набор всех возможных значений видеть рядом в одном файле.
Вместо obj1.getVal() приходится писать obj1.getMegaval().getVal() лямбды от этого пухнут и становятся хуже читаемыми.
Ну и на закуску все это при неаккуратном использовании память будет жрать просто не в себя.

Да и зачем вообще это? Куча ничего не делающего кода. Чистый оверинжиниринг.

Ваш код легко рефактрорится на
if(obj.isMegaFeature1()) {код для мегафичи 1}

Даже если все остальное не трогать становится просто и понятно.
Пример
public class MyClass {
    private static final Map<String, MyClass> instances =
            new HashMap<>();
    private static final Logger logger =
            LoggerFactory.getLogger(MyClass.class);
    private static final MyClass FALLBACK;
    static {
        // Способы инициализации могут быть разными
        // в зависимости от сложности алгоритма.
        // Можно выкачать из базы,
        // можно rest-сервис вызвать и т. п.
        // В первой версии почти всегда сгодится хардкод.
        // Можно комбинировать: часть значений
        // захардкодить, часть откуда-нибудь закачать.
        instances.put("V1", new MyClass("V1", ...));
        instances.put("V2", new MyClass("V2", ...));
        instances.put("V3", new MyClass("V3", ...));
        instances.put("V4", new MyClass("V4", ...));
        instances.put("V5", new MyClass("V5", ...));
        FALLBACK = instances.get("V1"); // или что угодно
    }

    public static MyClass getInstance(String name) {
        // Возможно, здесь будет ленивая инициализация
        MyClass result = instances.get(name);
        if (result != null) {
            return result;
        }
        logger.warn("Получено неподдерживаемое имя {}",
                    name);
        return FALLBACK; // или что-то посложнее
    }

    private MyClass(String name, ...) {
        this.name = name;
        // инициализация фич и других свойств
    }

    private final String name;

    // Использовать имя в условиях if нельзя —
    // только для отладки
    // или в качестве ключа поиска.
    // Это контролируется через ревью или хуки.
    public String getName() {
        return name;
    }

    private final boolean feature1;
    // фичи и другие свойства с геттерами
}

Есть еще вариант с ConcurrentHashMap и putIfAbsent на лету для неизвестных значений, но это только для внутренних сервисов, которые не могут прислать пользовательские данные, иначе OOM можно словить.

Признаю вы умеете в энтерпрайз. Нагородить столько кода вместо 10 строчного энума это сильно.

И использование великолепное получается.
var myVal = MyClass.instances.get("V1");

вместо простого
var myVal = MyEnum.val1;

Самое хорошее это ошибки только в рантайме. Опечатку в V1 компилятор проигнорирует. Константы (V1,V2,...) рядышком завести хочется. Что бы хотя бы опечатки ловились.

Не меньше радует отсутсвие гарантий.
MyClass.instances.get(«V1») всегда может вернуть не то что мы ожидаем. И компилятор опять таки нам это никак не покажет. И тестами это не покрыть. Если данные идут извне гарантий нет.

И все ради чего? Чтобы вписать в конфиг набор значений и строго настрого запретить кому либо их трогать? Если тронуть все развалится в непредсказуемом месте.

Итого кода больше. Гораздо больше. Бойлерплейт-код размножается по всей бизнеслогике.
Гарантий меньше.
Компилятор перестает нам помогать.
Обычный switch в прекрасном и недостижимом будующем.
Лямбды резко становятся неудобными. Размер кода решает.

Это код на языке Java. Вы не можете написать MyClass.instances.get, потому что instances — приватное поле. В енуме оно тоже есть, и при отладке вы его даже посмотреть сможете.


Насчет switch практически все, что я о нем думаю, я написал в своей статье https://habr.com/en/post/312792/.

Ладно MyClass.getInstance(«V1») ничем не лучше. Писать немного поменьше. Это хорошо но недостаточно.

Все остальные проблемы остаются. Никакой помощи от IDE и компилятора. Точку нажать и увидеть подсказку. Ошибки в рантайме. Гарантии. Бойлерплейт код по всей бизнес логике.

Борьба с энумами идет уже третий год? Это печально. В той статье пример очень синтетический. Подменять полноценный класс с полями и свойствами энумом с кучей свитчей и показывать что это плохо нечестно. Энум это что-то простое. У него нет ни внутреннего состояния, ни кучи аттрибутов. Это просто одно свойство чего-то большого. С ограниченным набором возможных значений. И все.

Если при проектировании ошиблись и начали появляться свойтва просто рефакторим код. Это нормально.

Нажимаете точку и получаете все методы, определенные в классе MyClass. Какой еще помощи вы ожидаете от IDE или от компилятора?

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

Пользователь задонатил и купил смену гендера на «элитная нагибайка». Надо сделать update… set gender=«элитная нагибайка». И не забыть проверить что если там «элитная нагибайка» то оплата должна пройти, а если «просто мужик» то можно и без оплаты.

Без getInstance(«нагибайка») написать код наверно можно, но опять костыли.

Основная фишка заключается в том, что вам не придется нигде писать


MyClass.getInstance("V1")

Потому что нигде в коде, кроме тестов и внутренних инициализирущих методов класса MyClass, использование значений констант типа "V1" не допускается. Этот код не пройдет ревью.

if(obj.isMegaFeature1()) {код для мегафичи 1}

Это самый правильный код. Потому что его невозможно сломать.

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

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

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

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

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

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

Articles