Что такое фабричный метод?
Фабричный метод (англ. Factory Method или Factory Design Pattern) — это порождающий шаблон проектирования, предоставляющий подклассам интерфейс для инстанцирования нужных экземпляров некоторого класса. Иными словами, подклассы берут на себя обязанность по созданию экземпляров базового класса.
Преимущества фабричного метода
- Инкапсуляция: фабричный метод инкапсулирует создание объектов, позволяя отделить клиентский код от создаваемых им объектов. В итоге мы получаем возможность легко изменять и расширять процесс их создания, не влияя на клиентский код.
- Гибкость: этот шаблон упрощает внедрение в систему новых типов объектов без изменения существующего кода. Это особенно важно в ситуациях, когда необходимые типы объектов остаются неизвестны вплоть до среды выполнения.
- Переиспользуемость: за счет централизации создания объектов в фабричном классе, или интерфейсе, мы получаем возможность повторно использовать этот код для создания объектов в различных частях приложения.
- Тестируемость: при использовании фабричного метода упрощается тестирование процесса создания объектов, поскольку его реализацию можно заменить макетом объекта для проведения такого тестирования.
- Обслуживание: применение фабричного метода упрощает дальнейшее обслуживание кода, так как изменения в реализации создания объектов можно будет вносить в одном месте, а не по всей базе кода.
Разбор фабричного метода на примере
Предположим, что создаем игру, в которой есть разные типы персонажей — 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
. Это делает код более модульным и гибким, а также удобным для дальнейшего обслуживания и расширения.
В заключении скажу, что фабричный метод является порождающим шаблоном проектирования, который позволяет создавать объекты без указания конкретного класса. Он инкапсулирует логику создания объекта в отдельном классе, делая код более гибким и легким в обслуживании. В примере с игрой, где присутствует несколько видов персонажей, фабричный метод позволил нам с легкостью добавлять их новые виды без изменения существующего кода. Это делает программу более масштабируемой и обслуживаемой.
В целом фабричный метод является мощным инструментом для проектирования ПО, способным упрощать сложные сценарии создания объектов.
Читайте также:
- Событийно-ориентированная разработка на основе браузерного расширения
- Spring Boot: реализация фабричного метода
- Представление концепций ООП с реальными сценариями
Читайте нас в Telegram, VK и Дзен
Перевод статьи Sumonta Saha Mridul: The factory design pattern