Фабричный метод

Фабричный метод

Дмитрий Бахтенков

Это паттерн, который определяет контракт взаимодействия для создания объектов некоторого класса, но непосредственное решение о том, объект какого класса создавать, происходит в наследниках.

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

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

Чаще всего паттерн применяется, когда нам необходимо создавать много разных слабо связанных друг с другом типов, причём заранее неизвестно, сколько таких типов будет. Паттерн позволяет системе быть независимой от процесса создания новых объектов и расширяемой: в нее можно легко вводить новые классы, объекты которых система должна создавать. Например кейс импорта данных в систему: начальным требованием может быть импорт из Excel и Json, но в будущем могут добавиться и другие источники данных: от csv до импорта из какой-либо бд: mysql, mongo и т.д.

Реализации

Существует три реализации этого паттерна:

  1. Классическая - когда есть объекты-создатели (creator), и создание объекта конкретного типа делегируется им
  2. Статическая - когда есть класс-фабрика, которая возвращает конкретные объекты на основе конструкций if или switch
  3. Обобщённая фабрика - мы сами указываем необходимый тип, который фабрика пытается инициализировать и создать. В этой статье я не буду её рассматривать из-за некоторой сложности. Возможно, потом - в отдельной статье.

Классическая реализация

На UML классическая реализация выглядит следующим образом:

Класс Product - это базовый класс для объектов, которые мы хотим получать нашим фабричным методом. У него есть две реализации - ConcreteProductA и ConcreteProductB. С одной из этих реализаций будет работать метод Operation класса Creator. Так как абстрактные классы не могут быть инициализированы с помощью ключевого слова new (var c = new Creator() - компилятор будет выдавать ошибку), мы обязаны сделать конкретные реализации - ConcreteCreatorA и ConcreteCreatorB.

В коде это может выглядеть следующим образом:

abstract class Product
{
    //тут классы-наследники будут хранить своё имя
    public abstract string Name { get; }
}

class ConcreteProductA : Product
{
    public override string Name => nameof(ConcreteProductA);
}

class ConcreteProductB : Product
{
    public override string Name => nameof(ConcreteProductB);
}

abstract class Creator
{
    public void Operation()
    {
        var product = FactoryMethod();
        // какая-то логика с экземпляром класса Product
        Console.WriteLine(product.Name);
    }

    public abstract Product FactoryMethod();
}

class ConcreteCreatorA : Creator
{
    public override Product FactoryMethod()
    {
        return new ConcreteProductA();
    }
}

class ConcreteCreatorB : Creator
{
    public override Product FactoryMethod()
    {
        return new ConcreteProductB();
    }
}


class Program
{
    static void Main(string[] args)
    {
        var concreteCreatorA = new ConcreteCreatorA();
        concreteCreatorA.Operation();
        var concreteCreatorB = new ConcreteCreatorB();
        concreteCreatorB.Operation();
    }
}

Если у нас появится третий вид продукта, нужно будет просто сделать ещё один ConcreteCreator и реализовать в нём фабричный метод.

На примере:

Возьмём задачу импорта логов нашей системы. Необходимо создать модуль, который будет получать логи (из файла или по протоколу Http), анализировать их и возвращать массив объектов логов для дальнейшей визуализации.

В C# есть замечательная абстракция Stream, с помощью которой можно получать доступ к разным потокам данных - к файлу, в данным из памяти и т.д. Подробнее об этом можно прочитать в документации

UML-диаграмма:

Класс LogReaderBase имеет метод Read(), который будет получать и анализировать строки строки из объекта Stream, получаемого в фабричном методе. Непосредственно получение потока происходит в классах-наследниках.

Для начала следует определить вспомогательный класс LogEntry, который будет описывать объект лога. Сделаем его максимально простым.

public class LogEntry
{
    public string Text { get; set; }
}

Затем определим LogReaderBase:

public abstract class LogReaderBase
{
    public IEnumerable<LogEntry> Read()
    {
        using (var stream = GetStream())
        {
            using(var reader = new StreamReader(stream))
            {
                string line = null;
                while((line = reader.ReadLine()) != null)
                {
                    // логика по обработке данных
                    yield return new LogEntry{ Text = line };
                }
            }
        }
    }

    // фабричный метод        
    protected abstract Stream GetStream();    
}

Здесь метод GetStream является классическим фабричным методом. Он будет реализован в классах-наследниках. Метод Read использует объект Stream для чтения данных. Так как абстрактный класс нельзя инициализировать, использоваться будут только его наследники с уже реализованным фабричным методом, например реализация с файлом:

public class FileLogReader : LogReaderBase
{
    protected override Stream GetStream()
    {
        return File.Open("log.txt", FileMode.Open);
    }
}

или реализация с MemoryStream, которая может выступать в роли заглушки для юнит-теста:

public class HttpLogReader: LogReaderBase
{
    protected override Stream GetStream()
    {
     // используем MemoryStream для имитации http-запроса
     var stream = new MemoryStream();
        var data = "LOG ERROR\nLOG INFO";
     var bytes = Encoding.Default.GetBytes(data);
        stream.Write(bytes, 0, bytes.Length);
     //устанавливаем позицию в файле на начальную, 
     //чтобы данные можно было корректно считать
      stream.Seek(0, SeekOrigin.Begin);
         return stream;
    }
}

Для проверки сделаем файл log.txt и применим паттерн в классе Program:

Файл log.txt:

Класс 'Program':

static void Main(string[] args)
{
    var fileLogReader = new FileLogReader();
    foreach(var log in fileLogReader.Read())
    {
        Console.WriteLine(log.Text);
    }
}

Вывод:

Попробуйте подобным образом использовать второго наследника LogReaderBase. Ну а мы переходим к следующей реализации паттерна.

Статическая реализация

Эта реализация является самой простой - суть её заключается в наличии класса (обычно статического, но не всегда) Factory, метод которого возвращает определённые экземпляры класса на основе входных данных. Это, своего рода, стратегия порождения объектов.

UML:

Пример в коде:

public abstract class Product 
{}

public class ConcreteProductA : Product
{}

public class ConcreteProductB : Product
{}

public static class Factory
{
    public static Product CreateProduct(string arg)
    {
        if(arg == "A")
        {
            return new ConcreteProductA();
        }                
        if(arg == "B")
        {
            return new ConcreteProductB();    
        }
        else
        {
             //генерируем исключение
             throw new Exception("некорректный тип продукта");
        }
    }
}

На примере:

Для статической реализации возьмём кейс импорта данных в систему из разных форматов.

Здесь интерфейс IImporter определяет контракт взаимодействия - метод ExecuteImport(), с помощью которого будет происходить непосредственно загрузка данных в систему. Это не обязательно должен быть интерфейс, можно также использовать абстрактный класс - всё зависит от ситуации.

public interface IImporter
{
    public ImportResult ExecuteImport();
}

Реализации ExcelImporter и JsonImporter реализуют импорт для конкретных типов файлов.

public class ExcelImporter : IImporter
{
    public ImportResult ExecuteImport()
    {
        //логика по импорту из Excel
        return new ImportResult();
    }
}

public class JsonImporter : IImporter
{
    public ImportResult ExecuteImport()
    {
        //логика по импорту из Json
        return new ImportResult();
    }
}

Класс ImportFactory является фабрикой - его метод GetImporter() возвращает конкретную реализацию IImporter в зависимости от расширения файла.

public static class ImportFactory
{
    public static IImporter GetImporter(string fileName)
    {
        var extension = Path.GetExtension(fileName);
            
        return extension switch
        {
            ".json" => new JsonImporter(),
            ".xlsx" => new ExcelImporter(),
            _ => throw new Exception("Некорректное расширение файла")
        };
    }
}

Попробуем использовать нашу фабрику в классе Program:

class Program
{
    static void Main(string[] args)
    {
        var importer = ImportFactory.GetImporter("import.json");
        Console.WriteLine(importer.ToString());
    }
}

В результате мы получили экземпляр класса JsonImporter:

Вывод:

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

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

С вами был Flex Code! Исходный код можно посмотреть тут

Vk | Telegram

Report Page