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

Мечи:

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.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.