Фабричный метод
Дмитрий Бахтенков
Это паттерн, который определяет контракт взаимодействия для создания объектов некоторого класса, но непосредственное решение о том, объект какого класса создавать, происходит в наследниках.
Говоря проще, сутью этого паттерна является создание конкретных типов на основе каких-то внешних условий.
Применимость
Чаще всего паттерн применяется, когда нам необходимо создавать много разных слабо связанных друг с другом типов, причём заранее неизвестно, сколько таких типов будет. Паттерн позволяет системе быть независимой от процесса создания новых объектов и расширяемой: в нее можно легко вводить новые классы, объекты которых система должна создавать. Например кейс импорта данных в систему: начальным требованием может быть импорт из Excel и Json, но в будущем могут добавиться и другие источники данных: от csv до импорта из какой-либо бд: mysql, mongo и т.д.
Реализации
Существует три реализации этого паттерна:
- Классическая - когда есть объекты-создатели (creator), и создание объекта конкретного типа делегируется им
- Статическая - когда есть класс-фабрика, которая возвращает конкретные объекты на основе конструкций
ifилиswitch - Обобщённая фабрика - мы сами указываем необходимый тип, который фабрика пытается инициализировать и создать. В этой статье я не буду её рассматривать из-за некоторой сложности. Возможно, потом - в отдельной статье.
Классическая реализация
На 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! Исходный код можно посмотреть тут