Как стать автором
Обновить

Java. Abstract Factory Pattern in Game Server

Время на прочтение8 мин
Количество просмотров3.2K

Абстрактная фабрика - это шаблон проектирования, который позволяет создавать семейства связанных объектов без указания их конкретных классов.

Проблема

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

Семейство сопутствующих игровых вещей, например: chest + unit + hero.

Несколько игровых событий. Например, продукты Chest + Unit + Hero доступны в следующих ивентах: Pirate, Space, Default.

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

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

Решение

Первое, что предлагает шаблон «Абстрактная фабрика» - это явное объявление интерфейсов для каждого отдельного игрового предмета из ивента (например, сундук,  юнит или герой). Затем вы можете заставить все варианты предметов следовать этим интерфейсам. Например, все варианты сундуков могут реализовывать интерфейс Chest; все варианты юнитов могут реализовывать интерфейс Unit и так далее.

Следующим шагом будет объявление абстрактной фабрики - интерфейса со списком методов создания для всех предметов, входящих в ивент (например, createChest, createUnit и createHero). Эти методы должны возвращать абстрактные типы продуктов, представленные интерфейсами, которые мы извлекли ранее: Chest, Unit, Hero и так далее.

А как насчет вариантов игровых предметов? Для каждого ивента мы создаем отдельный фабричный класс на основе интерфейса AbstractFactory. Фабрика - это класс, который возвращает предметы определенного типа. Например, PirateFactory может создавать только объекты PirateChest, PirateUnit и PirateHero.

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

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

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

Структура

Структура паттерна
Структура паттерна

Применимость

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

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

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

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

Как реализовать

  1. Составьте матрицу различных типов предметов в сравнении с вариантами этих предметов.

  2. Объявите абстрактные интерфейсы предметов для всех типов событий. Затем заставьте все конкретные классы игровых предметов реализовать эти интерфейсы.

  3. Объявите абстрактный интерфейс фабрики с набором методов создания для всех абстрактных предметов.

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

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

  6. Просмотрите код и найдите все прямые вызовы конструкторов игровых предметов. Замените их вызовами соответствующего метода создания объекта фабрики.

Шаблон для использования

Создадим интерфейсы для игровых предметов

public interface Chest {

    /*
     * Get URL where client can download skin, or
     * we can return skin config name.
     * It depends.
     */
    String getSkinURL();

    /*
     * For some events we want to change item names;
     */
    String getName();
}

public interface Hero {

    /*
     * Get URL where client can download skin, or
     * we can return skin config name.
     * It depends.
     */
    String getSkinURL();

    /*
     * For some events we want to change item names;
     */
    String getName();
}
public interface Unit {

    /*
     * Get URL where client can download skin, or
     * we can return skin config name.
     * It depends.
     */
    String getSkinURL();

    /*
     * For some events we want to change item names;
     */
    String getName();

}

   Создадим реализации для игровых предметов. Default реализация, когда у нас не проходит игровой ивент и мы хотим использовать базовые скины и названия. Pirate - это пиратский ивент “тысяча чертей”. И наконец Space - это космо ивент в нашей игре. 

Default Items

public class DefaultChest implements Chest {

    @Override
    public String getSkinURL() { return "default url"; }

    @Override
    public String getName() { return "Default chest"; }

}
public class DefaultHero implements Hero {

    @Override
    public String getSkinURL() { return "default hero url"; }

    @Override
    public String getName() { return "default hero"; }

}
public class DefaultUnit implements Unit {

    @Override
    public String getSkinURL() { return "default unit url"; }

    @Override
    public String getName() { return "default unit"; }

}

Pirate Items

public class PirateChest implements Chest {

    @Override
    public String getSkinURL() { return "Pirate url"; }

    @Override
    public String getName() { return "Pirate chest"; }

}

public class PirateHero implements Hero {

    @Override
    public String getSkinURL() { return "Pirate hero url"; }

    @Override
    public String getName() { return "Pirate hero"; }

}
public class PirateUnit implements Unit {

    @Override
    public String getSkinURL() { return "Pirate unit url"; }

    @Override
    public String getName() { return "Pirate unit"; }

}

Space Items

public class SpaceChest implements Chest {

    @Override
    public String getSkinURL() { return "Space url"; }

    @Override
    public String getName() { return "Space chest"; }

}
public class SpaceHero implements Hero {

    @Override
    public String getSkinURL() { return "Space hero url"; }

    @Override
    public String getName() { return "Space hero"; }

}
public class SpaceUnit implements Unit {

    @Override
    public String getSkinURL() { return "Space unit url"; }

    @Override
    public String getName() { return "Space unit"; }

}

Дальше нам потребуется EventFactory, которая будет возвращать (например) наши игровые предметы. 

public interface EventFactory {

    Chest getChest();

    Unit getUnit();

    Hero getHero();

}

И 3 реализации фабрики для каждого нашего игрового события:

public class DefaultFactory implements EventFactory {

    @Override
    public Chest getChest() { return new DefaultChest(); }

    @Override
    public Unit getUnit() { return new DefaultUnit(); }

    @Override
    public Hero getHero() { return new DefaultHero(); }

}
public class PirateFactory implements EventFactory {

    @Override
    public Chest getChest() { return new PirateChest(); }

    @Override
    public Unit getUnit() { return new PirateUnit(); }

    @Override
    public Hero getHero() { return new PirateHero(); }

}
public class SpaceFactory implements EventFactory {

    @Override
    public Chest getChest() { return new SpaceChest(); }

    @Override
    public Unit getUnit() { return new SpaceUnit(); }

    @Override
    public Hero getHero() { return new SpaceHero(); }

}

Создадим класс Application, принимающий на вход фабрику. Тип фабрики может выбираться при старте приложения или в зависимости от параметров, которые установили наши ГД.  При создании Application в конструкторе будут инициализироваться наши игровые предметы. И напишем вспомогательные методы, для возвращения списка URL и списка имен. 

public class Application {

    private Chest chest;
    private Unit unit;
    private Hero hero;

    Application(EventFactory factory){
        this.chest = factory.getChest();
        this.unit = factory.getUnit();
        this.hero = factory.getHero();
    }

    List<String> getSkinUrls(){
        return List.of(chest.getSkinURL(), unit.getSkinURL(), hero.getSkinURL());
    }

    Map<String, String> getNames(){
        Map<String, String> names = new HashMap<>();
        names.put("chest", chest.getName());
        names.put("unit", unit.getName());
        names.put("hero", hero.getName());
        return names;
    }
}

Дальше мы можем имитировать старт приложения и проверить результаты запроса URL  и имен в зависимости от текущего ивента.

public class Main {

    public static void main(String[] args) {
        Application application = new Application(new DefaultFactory());
        print(application);
        application = new Application(new PirateFactory());
        print(application);
        application = new Application(new SpaceFactory());
        print(application);
    }

    private static void print(Application application) {
        System.out.println(Arrays.toString(application.getSkinUrls().toArray(new String[0])));
        System.out.println(convertWithStream(application.getNames()));
    }

    private static String convertWithStream(Map<?, ?> map) {
        String mapAsString = map.keySet().stream()
                .map(key -> key + "=" + map.get(key))
                .collect(Collectors.joining(", ", "{", "}"));
        return mapAsString;
    }
}

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

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

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


Теги:
Хабы:
+2
Комментарии1

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн