Паттерн Factory (Фабрика), теория
Ранее уже разбирались паттерн Singleton (создание единственного в своём роде объекта) и паттерн Prototype (создание объектов на основе общего шаблона). В обоих этих методах объекты создаются «вручную».
А что если требуется создать одну функцию/класс, который может «производить» объекты разных типов, чтобы в каждом внутри была «зашита» собственная логика и данные?
Для этого можно использовать паттерн Factory (Фабрика). Он представляет собой функцию или метод, принимающий параметры, и на основе параметров создающий объект нужного типа («фабрика» производит объекты):
В упрощенном виде фабрика — это одна функция/«суперкласс», который отвечает за всё сразу:
export class FileProcessor { static processFile(type, content) { if (type === "image") { // Уникальная логика для изображений return { type, content, createImage() {}, }; } else if (type === "text") { // Уникальная логика для текстовых файлов return { type, content, createTextFile() {}, }; } else if (type === "pdf") { // Уникальная логика для PDF-файлов return { type, content, createPDF() {}, }; } }}import { FileProcessor } from "./fileProcessor";
// Использованиеconst imageFile = FileProcessor.processFile("image", "photo.jpg");imageFile.createImage(); // Создание изображения…
const textFile = FileProcessor.processFile("text", "Hello World");textFile.createTextFile(); // Создание текстового файла…У такого подхода есть очевидные недостатки:
- один класс отвечает сразу за всё
- трудно расширять: при добавлении нового типа файла нужно модифицировать всю логику, то есть появляется риск случайно нарушить работу уже существующего кода
- дублируется логика создания для каждого отдельного кейса
Поэтому такой вариант реализации подходит для случая, если логика несложная, типов возможных объектов немного или не планируется добавлять новые.
Но могут быть другие требования и условия:
- внутри каждого типа «производимого» объекта присутствует разнообразная комплексная логика
- все типы объектов заранее неизвестны, предполагается, что будут добавляться новые типы
- нужен удобный способ создания разных объектов в зависимости от среды, где выполняется код
- нужно реализовать одинаковый «контракт» для создание разных объектов
При таких условиях стоит выделить каждый «фабричный» тип объекта в отдельный класс/функцию. Как будет выглядеть класс фабрики с таким подходом:
// Базовый класс Fileclass File { constructor(content) { this.content = content; }
create() { throw new Error("create() должен быть имплементирован"); }}
// Конкретные классы разных документовclass Image extends File { create() { // логика создания изображения }}
class TextFile extends File { create() { // логика создания текстового документа }}
class PDFFile extends File { create() { // логика создания PDF-файла }}
// Фабрикаexport class FileFactory { static processFile(type, content) { switch (type) { case "image": return new Image(content); case "text": return new TextFile(content); case "pdf": return new PDFFile(content); default: throw new Error("Неизвестный тип документа"); } }}import { FileFactory } from "./fileFactory";
// Использованиеconst imageFile = FileFactory.processFile("image", "photo.jpg");imageFile.create(); // Создание изображения…
const textFile = FileFactory.processFile("text", "Hello World");textFile.create(); // Создание текстового файла…Что получаем с таким подходом:
- лучшее разделение ответственности: каждый тип файла отделяется в самостоятельную сущность (внутри каждого типа может реализовываться своя специфичная логика)
- легче добавлять новые типы файлов без затрагивания уже существующих типов
- сама «фабрика» тоже отделена, её можно доработать или заменить без модификации кода отдельных типов файлов
Но при этом такой «развесистый» подход не стоит использовать:
- когда логика создания разных объектов одинакова и не предполагается, что она будет меняться в дальнейшем
- когда типов объектов немного
- в простых системах, где не хочется усложнять понимание работы