В 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 флажок в БД менять логику работы сервера. Опыт показал, что особых проблем такое сожительство версий не вызывает, а перспективные разработки — заметно ускоряются.
Автор: qdreadknight