Итак, представим, что у нас есть магическая кузница, где создаются зачарованные мечи — огненные, ледяные и другие, каждый со своими особыми свойствами.
Мечи:
public interface IWeapon
{
string Name { get; }
void Attack();
}
public class FireSword : IWeapon
{
public string Name => "Огненный меч";
public void Attack() => Console.WriteLine("Меч выпускает волну пламени!");
}
public class IceSword : IWeapon
{
public string Name => "Ледяной меч";
public void Attack() => Console.WriteLine("Меч выпускает морозный удар!");
}
Магическая кузница:
public class MagicForge
{
public int Energy { get; private set; } = 50;
public IWeapon CreateWeapon(string type)
{
// Общие шаги «ритуала» создания
if (Energy < 10)
throw new InvalidOperationException("Недостаточно магической энергии.");
Energy -= 10;
Console.WriteLine("Кузнец начертил руны, разжёг магическое пламя...");
switch (type.ToLowerInvariant())
{
case "fire":
return new FireSword();
case "ice":
return new IceSword();
default:
throw new ArgumentException($"Неизвестный тип оружия: {type}");
}
}
}
Пример:
public class Program
{
public static void Main()
{
var forge = new MagicForge();
IWeapon fireSword = forge.CreateWeapon("fire");
Console.WriteLine($"{fireSword.Name} создан.");
fireSword.Attack();
Console.WriteLine();
IWeapon iceSword = forge.CreateWeapon("ice");
Console.WriteLine($"{iceSword.Name} создан.");
iceSword.Attack();
}
}
Ваша задача — внимательно изучить приведённый ниже код (можете даже его запустить) и описать словами, какие архитектурные проблемы вы здесь видите.
Не исправляйте код — только проанализируйте его структуру и принципы, которые он нарушает.
Итак, как же можно решить эту ситуацию аккуратно и без switch?
Ключевая идея фабричного метода — вынести решение о том, какой конкретный объект создавать, в отдельный «создатель».
А клиентский код (в нашем случае — кузница) будет работать только с абстракцией.
Тогда при добавлении нового типа оружия нам не придётся менять старые классы — достаточно просто добавить нового создателя.
Давайте опишем наше новое решение с помощью UML-диаграммы.
На этой схеме хорошо видно, что кузница (MagicForge) теперь не зависит от конкретных видов оружия. Она вообще ничего о них не знает — ни про FireSword, ни про IceSword.
То же самое касается и конкретных создателей оружия (FireSwordCreator, IceSwordCreator). Каждый из них занимается только созданием своего типа меча и не вмешивается в работу кузницы.
Кузница знает лишь одно — ей передадут некий объект-создатель, реализующий общий контракт (WeaponCreator). Дальше всё просто: кузница «толкает» (вызывает) этого создателя, чтобы тот создал оружие, и затем возвращает готовое оружие наружу, даже не подозревая, какого оно типа.
Так мы полностью избавляемся от прямых зависимостей между кузницей и конкретными продуктами, делая систему гибкой, расширяемой и легко тестируемой.
Начнём с кузницы
Раньше кузница принимала строку ("fire", "ice") и сама решала, какой меч создать — из-за этого появлялся switch, new и жёсткая зависимость от конкретных классов. Теперь же мы будем передавать кузнице аж целый объект создателя меча, у которого есть фабричный метод.
Кузница больше не знает, какой именно меч будет создан — все что ей нужно - просто вызвать команду создания.
public class MagicForge
{
public int Energy { get; private set; } = 50;
public IWeapon CreateWeapon(WeaponCreator creator)
{
// Общие шаги «ритуала» создания
if (Energy < 10)
throw new InvalidOperationException("Недостаточно магической энергии.");
Energy -= 10;
Console.WriteLine("Кузнец начертил руны, разжёг магическое пламя...");
return creator.ForgeSword(); //использование фабричного метода
}
}
Такой подход снижает связность, упрощает тестирование и делает систему легко расширяемой.
А вот и сам фабричный метод.
public abstract class WeaponCreator
{
public abstract IWeapon ForgeSword(); //Фабричный метод
}
Хотя это обычный метод, для него создаётся базовый класс-создатель, который объявляет абстрактный контракт создания.
Конкретные подклассы решают, какой именно продукт (в нашем случае — меч) они создают.
Каждый конкретный создатель знает только свой тип оружия и отвечает за его создание.
Никаких switch, никаких строк — вся логика выбора инкапсулирована в отдельных классах.
Если появится новый тип меча (например, Ядовитый меч), просто добавим нового создателя, не меняя код кузницы.
// Конкретные создатели
public class FireSwordCreator : WeaponCreator
{
public override IWeapon ForgeSword() => new FireSword(); //Конкретная реализация фабричного метода
}
public class IceSwordCreator : WeaponCreator
{
public override IWeapon ForgeSword() => new IceSword(); //Конкретная реализация фабричного метода
}
Пример использования:
public class Program
{
public static void Main()
{
var forge = new MagicForge();
var fireSword = forge.CreateWeapon(new FireSwordCreator());
Console.WriteLine($"{fireSword.Name} создан.");
fireSword.Attack();
Console.WriteLine();
var iceSword = forge.CreateWeapon(new IceSwordCreator());
Console.WriteLine($"{iceSword.Name} создан.");
iceSword.Attack();
}
}
Таким образом, бизнес-логика кузницы остаётся простой и независимой от конкретных типов оружия.
Итог:
Теперь наша MagicForge не зависит от конкретных классов мечей. Добавить новый тип оружия можно, не трогая старый код — просто создать новый подкласс WeaponCreator. Так работает паттерн “Фабричный метод”: он передаёт ответственность за создание объектов подклассам и избавляет систему от жёстких зависимостей.
Теперь давайте чуть подробнее поговорим о самом паттерне.
Фабричный метод — это порождающий паттерн, который переносит решение о том, какой конкретный объект создать, из кода клиента в отдельный "класс-создатель": Этот базовый класс объявляет абстрактный метод создания (фабричный метод), а подклассы переопределяют его, возвращая нужные реализации создавая нужные объекты.
В общем и целом UML-схема этого паттерна выглядит следующим образом:
Проблема нашего исходного подхода была в том, что раньше мы передавали кузнице строку ("fire", "ice"), и она сама решала, какой меч создать. Внутри метода находился switch с new FireSword() и new IceSword(). Клиентский код (кузница) знал о конкретных классах и менялся при добавлении каждого нового типа оружия. Это нарушает принципы SRP и OCP — код становится хрупким и плохо расширяемым.
Мы перенесли ответственность с класса кузницы на другие объекты. Кузница больше не решает, какой именно меч создавать, и не содержит логики выбора. Всё что она делает - просто получает объект и "пинает" его заставляя создать и отдать ей объект.
И теперь, чтобы добавить новый тип оружия, достаточно:
1. Создать новый класс меча (например, LightningSword, ShadowSword и т.д.);
2. Добавить для него свой порождающий класс(LightningSwordCreator).
Код кузницы при этом всегда остаётся неизменным.
Теперь MagicForge полностью изолирована от конкретных реализаций мечей, а система легко расширяется без правок существующих классов. Фабричный метод не только избавляет от switch, но и делает архитектуру гибкой, чистой и устойчивой к изменениям.
И немного советов по поводу использования:
| Когда применять |
|---|
В коде появился switch/if-else по типам для new .... |
| Клиентский код знает о конкретных классах и трудно тестируется. |
| Нужно добавлять новые варианты продукта без правок существующего кода (OCP). |
| Когда не нужен: | | --- | | Создание простое, вариантов немного, расширять не планируется. | | Нет общего алгоритма вокруг создания — иногда достаточно простой фабрики (интерфейс + одна реализация) или DI-контейнера. | Фабричный метод — это порождающий паттерн проектирования, который делегирует создание объектов подклассам, позволяя базовому классу работать с ними через общий интерфейс.
Фабричный метод (Factory Method) — порождающий паттерн, который делегирует создание объектов подклассам, позволяя базовому классу работать с ними через общий интерфейс.
Клиентский код не зависит от конкретных классов продуктов. Добавление нового типа продукта не требует правок существующего кода.
Паттерн применяется, когда в коде появляется switch/if-else по типам для создания объектов, и нужно соблюсти принципы OCP и DIP.