Скорее всего до этого момента ты уже сталкивался с паттернами проектирования. Например, с одиночкой (singleton).

Давай вспомним, что такое паттерны, зачем они нужны, что такое порождающие паттерны (к которым и относится одиночка), и изучим новый паттерн — фабричный метод.

Шаблон проектирования или паттерн (design pattern) в разработке программного обеспечения — это повторяемая архитектурная конструкция, которая представляет собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.

Обычно шаблон не является законченным образцом, который может быть прямо преобразован в код, это лишь пример решения задачи, который можно использовать в различных ситуациях.

Порождающие шаблоны (creational patterns) — шаблоны проектирования, которые имеют дело с процессом создания объектов. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов.

Фабричный метод (factory method) — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в родительском классе, предоставляя возможность создания этих самых объектов своим наследникам. В момент создания наследники могут определить, какой класс создавать.

Какую проблему решает паттерн?

Представь, что ты решил создать программу доставки. Изначально ты будешь нанимать курьеров с автомобилями и в программе в качестве средства доставки использовать объект Автомобиль. Курьеры развозят посылки из пункта А в пункт Б, В и так далее. Всё просто.

Программа набирает популярность, твой бизнес растет, ты хочешь расширяться, выходя на новые рынки. Так, например, можно дополнительно начать доставлять еду и заниматься грузовыми перевозками. Тогда еду могут доставлять и пешие курьеры, и на самокатах, и на велосипедах, а под грузовые нужды нужны грузовые автомобили.

Теперь тебе важно знать, когда, кому, что и сколько конкретно будет доставлено, учитывая, сколько каждый курьер может перевозить или переносить. У новых видов транспортных средств разная скорость и вместимость. Тогда ты обнаружишь, что большая часть сущностей в программе сильно связаны с объектом Автомобиль, и чтобы заставить твою программу работать с другими способами доставки, тебе придется переписывать имеющуюся кодовую базу и так повторять каждый раз для каждого нового транспорта.

В итоге получается ужасающий код, наполненный условными операторами, которые выполняют то или иное действие в зависимости от транспорта.

Решение проблемы

Паттерн фабричный метод предлагает создавать объекты не напрямую, используя оператор new, а через вызов особого фабричного метода. Подклассы класса, который содержит фабричный метод, могут изменять создаваемые объекты конкретных создаваемых транспортных средств. На первый взгляд это может показаться бессмысленным: мы просто переместили вызов конструктора из одного конца программы в другой. Но теперь ты сможешь переопределять фабричный метод в подклассе, чтобы изменить тип создаваемого транспорта.

Посмотрим на диаграмму классов такого подхода:

Чтобы эта система заработала, все возвращаемые объекты должны иметь общий интерфейс. Подклассы смогут производить объекты различных классов, следующих одному и тому же интерфейсу.

Например, классы Грузовик и Автомобиль реализуют интерфейс Курьерский Транспорт с методом доставить. Каждый из этих классов реализует метод по-своему: грузовики доставляют грузы, а автомобили — еду, посылки и так далее. Фабричный метод в классе Создатель грузовиков вернёт объект-грузовик, а класс Создатель автомобилей — объект-автомобиль.

Для клиента фабричного метода нет разницы между этими объектами, так как он будет трактовать их как некий абстрактный Курьерский Транспорт. Для него будет важно, чтобы объект имел метод доставить, а как конкретно он работает — не важно.

Реализация на Java:


public interface CourierTransport {
	void deliver();
}
public class Car  implements CourierTransport{
	@Override
	public void deliver() {
    		System.out.println("The parcel is delivered by car ");
	}
}
public class Truck implements CourierTransport{
	@Override
	public void deliver() {
    		System.out.println("Cargo is delivered by truck");
	}
}
public abstract class CourierTransportCreator {
	public abstract CourierTransport createTransport();
}
public class CarCreator extends CourierTransportCreator {
	@Override
	public CourierTransport createTransport() {
    		return new Car();
	}
}
public class TruckCreator extends CourierTransportCreator{
	@Override
	public CourierTransport createTransport() {
    		return new Truck();
	}
}
 
public class Deliver {
	private String address;
	private CourierTransport courierTransport;
 
	public Deliver() {
	}
 
	public Deliver(String address, CourierTransport courierTransport) {
    	this.address = address;
    	this.courierTransport = courierTransport;
	}
 
	public CourierTransport getCourierTransport() {
    		return courierTransport;
	}
 
	public void setCourierTransport(CourierTransport courierTransport) {
    		this.courierTransport = courierTransport;
	}
 
	public String getAddress() {
    		return address;
	}
 
	public void setAddress(String address) {
    		this.address = address;
	}
}
public static void main(String[] args) {
    	//принимаем новый вид заказа с базы (псевдокод)
    	String type = database.getTypeOfDeliver();
 
    	Deliver deliver = new Deliver();
    	
    	//заполняем транспорт в доставку
        deliver.setCourierTransport(getCourierTransportByType(type));
    	
    	//доставляем
        deliver.getCourierTransport().deliver();
 
	}
 
	public static CourierTransport getCourierTransportByType(String type) {
    	switch (type) {
        	case "CarDeliver":
            	return new CarCreator().createTransport();
        	case "TruckDeliver":
            	return new TruckCreator().createTransport();
        	default:
            	throw new RuntimeException();
	    }
	}
    

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

Когда применять паттерн?

1. Когда заранее неизвестны типы и зависимости объектов, с которыми должен работать твой код.

Фабричный метод отделяет код производства транспорта от остального кода, который этот транспорт использует. Благодаря этому код создания объектов можно расширять, не трогая основной.

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

2. Когда ты хочешь экономить системные ресурсы, повторно используя уже созданные объекты вместо порождения новых.

Такая проблема обычно возникает при работе с тяжёлыми ресурсоемкими объектами, такими, как подключение к базе данных, файловой системе и т. д.

Представь, сколько действий тебе нужно совершить, чтобы повторно использовать существующие объекты:

  1. Сначала тебе следует создать общее хранилище, чтобы хранить в нем все создаваемые объекты.

  2. При запросе нового объекта нужно будет заглянуть в хранилище и проверить, есть ли там неиспользуемый объект.

  3. Вернуть объект клиентскому коду.

  4. Но если свободных объектов нет — создай новый, добавив его в хранилище.

Весь этот код нужно куда-то поместить, чтобы не засорять клиентский код. Самым удобным местом был бы конструктор объекта, ведь все эти проверки нужны только при создании объектов. Но, увы, конструктор всегда создаёт новые объекты, он не может вернуть существующий экземпляр.

Значит, нужен другой метод, который бы отдавал как существующие, так и новые объекты. Им и станет фабричный метод.

3. Когда ты хочешь дать возможность пользователям расширять части твоего фреймворка или библиотеки.

Пользователи могут расширять классы твоего фреймворка через наследование. Но как сделать так, чтобы фреймворк создавал объекты из этих новых классов, а не из стандартных?

Решением будет дать пользователям возможность расширять не только желаемые компоненты, но и классы, которые создают эти компоненты. А для этого создающие классы должны иметь конкретные создающие методы, которые можно определить.

Преимущества

  • Избавляет класс от привязки к конкретным классам транспорта.
  • Выделяет код создания транспорта в одно место, упрощая поддержку кода.
  • Упрощает добавление новых видов транспорта в программу.
  • Реализует принцип открытости/закрытости.

Недостатки

Может привести к созданию больших параллельных иерархий классов, так как для каждого класса продукта надо создать свой подкласс создателя.

Подведем итог

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

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