Знакомство с фабричным методом

Что такое фабричный метод?

Фабричный метод (англ. Factory Method или Factory Design Pattern)  —  это порождающий шаблон проектирования, предоставляющий подклассам интерфейс для инстанцирования нужных экземпляров некоторого класса. Иными словами, подклассы берут на себя обязанность по созданию экземпляров базового класса.

Преимущества фабричного метода

  1. Инкапсуляция: фабричный метод инкапсулирует создание объектов, позволяя отделить клиентский код от создаваемых им объектов. В итоге мы получаем возможность легко изменять и расширять процесс их создания, не влияя на клиентский код.
  2. Гибкость: этот шаблон упрощает внедрение в систему новых типов объектов без изменения существующего кода. Это особенно важно в ситуациях, когда необходимые типы объектов остаются неизвестны вплоть до среды выполнения.
  3. Переиспользуемость: за счет централизации создания объектов в фабричном классе, или интерфейсе, мы получаем возможность повторно использовать этот код для создания объектов в различных частях приложения. 
  4. Тестируемость: при использовании фабричного метода упрощается тестирование процесса создания объектов, поскольку его реализацию можно заменить макетом объекта для проведения такого тестирования.
  5. Обслуживание: применение фабричного метода упрощает дальнейшее обслуживание кода, так как изменения в реализации создания объектов можно будет вносить в одном месте, а не по всей базе кода.

Разбор фабричного метода на примере

Предположим, что создаем игру, в которой есть разные типы персонажей  —  Warrior (воин), Wizard (маг) и Archer (лучник). Каждый из этих персонажей обладает своим набором способностей и видов атаки.

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

public class Warrior{
@Override
public void attack() {
System.out.println("Warrior attacks with a sword!");
}

@Override
public void defend() {
System.out.println("Warrior defends with a shield!");
}
}
public class Wizard{
@Override
public void attack() {
System.out.println("Wizard attacks with magic!");
}

@Override
public void defend() {
System.out.println("Wizard defends with a spell!");
}
}
public class Archer{
@Override
public void attack() {
System.out.println("Archer attacks with a bow!");
}

@Override
public void defend() {
System.out.println("Archer defends with a dodge!");
}
}
public class Game {
public static void main(String[] args) {
// Создание персонажа Warrior
Character warrior = new Warrior();
warrior.attack();
warrior.defend();

// Создание персонажа Wizard
Character wizard = new Wizard();
wizard.attack();
wizard.defend();

// Создание персонажа Archer
Character archer = new Archer();
archer.attack();
archer.defend();

// выполнение действий с персонажами ...
}
}

В этой версии кода мы вручную создаем каждый тип объекта персонажа (Wizard, Warrior, Archer) и вызываем их методы attack() и defend() напрямую. Такой подход будет работать для небольших проектов с ограниченным числом объектов, но по мере роста их числа и расширения связанной с ними логики станет громоздким и сложным в обслуживании.

В чем проблема такой структуры кода?

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

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

Тут нам на помощь приходит интерфейс.

Сначала мы определим интерфейс Character , который будет представлять обобщенного персонажа игры:

public interface Character {
void attack();
void defend();
}

Далее мы создадим конкретные классы, реализующие интерфейс Chatacter. Вот пример класса Warrior:

Игровой персонаж Warrior:

public class Warrior implements Character {
@Override
public void attack() {
System.out.println("Warrior attacks with a sword!");
}

@Override
public void defend() {
System.out.println("Warrior defends with a shield!");
}
}

Аналогичным образом мы создадим классы Wizard и Archer, реализующие интерфейс Character.

Игровой персонаж Wizard:

public class Wizard implements Character {
@Override
public void attack() {
System.out.println("Wizard attacks with magic!");
}

@Override
public void defend() {
System.out.println("Wizard defends with a spell!");
}
}

Игровой персонаж Archer:

public class Archer implements Character {
@Override
public void attack() {
System.out.println("Archer attacks with a bow!");
}

@Override
public void defend() {
System.out.println("Archer defends with a dodge!");
}
}

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

public abstract class CharacterFactory {
public abstract Character createCharacter();
}

Обратите внимание, что метод CreateCharacter() абстрактный, то есть каждый подкласс CharacterFactory должен будет предоставлять собственную реализацию этого метода.

Теперь можно создавать конкретные классы, расширяющие CharacterFactory и реализующие метод CreateCharacter() для возвращения конкретного типа персонажа. Вот пример WarriorFactory:

public class WarriorFactory extends CharacterFactory {
@Override
public Character createCharacter() {
return new Warrior();
}
}

По той же схеме мы создадим классы WizardFactory и ArcherFactory, которые расширят CharacterFactory и реализуют метод CreateCharacter() для возвращения Wizard или Archer соответственно. 

Наконец, можно использовать CharacterFactory и его подклассы для создания новых персонажей в классе Game:

public class Game{
public static void main(String[] args) {
CharacterFactory factory = new WarriorFactory();
Character character = factory.createCharacter();
character.attack();
character.defend();
}
}

В этом примере у нас получилось три класса, реализующих интерфейс Character: Warrior, Wizard и Archer. У нас также есть три фабричных класса: WarriorFactory, WizardFactory и ArcherFactory, каждый из которых создает нужный тип объекта Character.

В классе Game мы создаем фабричный объект (CharacterFactory) и далее используем его для создания объекта Character (character). Затем можно использовать методы attack и defend в объекте character, не зная конкретный класс создаваемого объекта.

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

А теперь реальная магия

Игровой персонаж Ninja:

Предположим, что хотим добавить в игру новый тип персонажа  —  Ninja.

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

public class Ninja implements Character {
@Override
public void attack() {
System.out.println("Wizard attacks with magic!");
}

@Override
public void defend() {
System.out.println("Wizard defends with a spell!");
}
}

public class NinjaFactory extends CharacterFactory {
@Override
public Character createCharacter() {
return new Ninja();
}
}
public class Game {
public static void main(String[] args) {
CharacterFactory factory = new NinjaFactory();
Character ninja = factory.createCharacter();
ninja.attack();
ninja.defend();
}
}

Как вы видите, здесь мы просто создаем новый класс NinjaFactory, расширяющий абстрактный класс CharacterFactory и реализующий метод CreateCharacter для возвращения нового объекта Ninja. Далее в классе Game мы используем NinjaFactory для создания объекта Ninja без непосредственной отсылки к классу Ninja.

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

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

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

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Sumonta Saha Mridul: The factory design pattern

Предыдущая статьяTailwind CSS: как разработать продвинутую пользовательскую анимацию
Следующая статьяРуководство по модулю Python itertools