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

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

Шаг 1. Берем статическую таблицу в БД (state), где хранятся разные параметры состояния сервера, например игровая дата, и добавляем поле server_type

Шаг 2. Делаем статически методы, которыми будем определять тип сервера

public static boolean isRoundServerType() {
        return serverType == SERVER_TYPE_ROUND || serverType == SERVER_TYPE_EVOLUTION;
    }

    public static boolean isEvolutionServerType() {
        return serverType == SERVER_TYPE_EVOLUTION;
    }

Шаг 3. Готовим методы для веб-интерфейса, чтобы определять тип сервера:

    public boolean getRoundServer() {
        return State.isRoundServerType();
    }
    
    public boolean getEvolutionServer() {
        return State.isEvolutionServerType();
    }

Шаг 4. Во все места бизнес-логики вставляем блоки типа:

    public String[] getProcessTimes() {
        if (State.isEvolutionServerType()) {
            String[] times = { "09.00", "11.00", "13.00", "15.00", "17.00", "19.00", "21.00" };
            return times;
        } else if (State.isRoundServerType()) {
            String[] times = { "08.00", "10.00", "12.00", "14.00", "16.00", "18.00", "20.00" };
            return times;
        } else {
            String[] times = { "09.30", "11.30", "13.30", "15.30", "17.30", "19.30", "22.30" };
            return times;
        }
    }

Шаг 5. Во все места веб-интерфейса вставляем проверку на доступность элемента интерфейса.

<item name="corporationtotal" caption="Влияние корпораций" href="/corporation/controltotal/" hide="game.evolutionServer"/>
       <item name="citytotal" caption="Влияние корпораций" href="/corporation/citycontrol/" show="game.evolutionServer"/>

Тут надо сделать поправку на то, что мы используем самописный фреймворк, который позволяет скрывать элементы по двум видам правил:show/hide — где непосредственно вычисляется значение какого поля, allow/deny — тогда у пользователя, который залогинен запрашиваются права на какие-то действия. Последнее правило дает возможность вести разработку прямо в основной ветке репозитория, просто скрывая новый функционал правами например:

<button icon="adddoc" caption="Добавить новый город" action="javascript:dialog('/intercitynew/')" allow="admin:gamemanager"/>

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

                
      if (RetailFormula.isEvolutionRetail(currentCity.getId().toInteger())) {
          house = new House4(row, date);
      }
      else {
          house = new House2(row, date);
      }

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

Код получается немного пухленький, зато имеет минимальные затраты на поддержку всей системы, т.к. фактически 1 флажок в БД менять логику работы сервера. Опыт показал, что особых проблем такое сожительство версий не вызывает, а перспективные разработки — заметно ускоряются.
Поделиться с друзьями
-->

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


  1. ApeCoder
    28.01.2017 22:51
    +4

    Ура, вы изобрели feature toggle!


    1. qdreadknight
      29.01.2017 19:08
      +1

      Я догадывался, что не открыл америку. А оказывается у этого есть задокументированое название.
      Статья в целом про то, что это работает и это удобно в продакшене. Давным давно мы пробовали ветки в репозитории, но это был какой-то кромешный ад по факту.


  1. sergey1borisov
    29.01.2017 16:17

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


    1. qdreadknight
      29.01.2017 19:06

      Мой проект состоит из такого количества классов, что шаблон «стратегия» мне кажется здесь абсолютно неуместным на глобальном уровне. Применить его конечно можно, но тогда сразу вешаться. Концов не найдешь потом. А вот варианты алгоритмов спроса реализуют разные классы, так что локально это работает.


  1. elegorod
    02.02.2017 15:38

    Когда-то делали похожим образом, но тут есть проблемы:
    1. При рефакторинге большой шанс сломать старый код. Например, переделываете вы механику осад, в объекте осад были поля String startTime, String endTime, Date date. Переходите на Джаву 8, решили сделать через правильные Instant: Instant start, Instant end. Старый код работает со старыми полями (ожидая, что они not null), новый — с новыми. Большая вероятность, что тут что-то сломается, особенно во время первода часов.
    2. Трудно делать миграции базы данных. Тот же пример с осадами. Старый код вставляет в базу 3 старых поля, новый — 2 новых. При этом падает с ошибками, что не указаны значения для not null полей.
    3. Не всё можно спрятать за правами доступа. Особенно, если это переделка уже работающих механик.