Немножко о себе. Я молодой, не опытный, перспективный разработчик в области BigData, короче говоря junior. Получил задание разобраться в некоторых паттернах проектирования (на мой вкус), и первым я решил исследовать Factory Design Pattern.
Что собой представляет Factory
Паттерн относится к «порождающей» группе паттернов. Название «фабрика» говорит само за себя. Фабрика занимается созданием некоторой продукции. Мы поставляем ей сырье и в результате получаем полноценный продукт. Получается такой себе чёрный ящик, в котором механизм создания объектов скрыт. Это и есть цель паттерна фабрика. Скрыть излишки логики от глаз. Рисунок даст возможность лучше понять, как этот паттерн должен быть реализован.
Есть какая-то иерархия классов, для которой вы хотели бы иметь удобную фабрику и по желанию получать тот или иной объект без использования new. В качестве базового класса может выступать интерфейс, абстрактный класс или обычный класс. Но помните, что паттерны — это скорее рекомендации, чем правила, и ваша реализация может отличатся (вы сможете это увидеть ниже в моем коде) от общепринятых подходов к его имплементации.
Ближе к коду
Модель
В качестве модели для фабрики я создал небольшую иерархию из абстрактного базового класса и двух его реализаций.
Базовый класс
public abstract class Computer {
protected String ram;
protected String hdd;
protected String cpu;
public Computer() {
}
public Computer(String ram, String hdd, String cpu) {
this.ram = ram;
this.hdd = hdd;
this.cpu = cpu;
}
public String getCPU() { return cpu; }
public String getHDD() { return hdd; }
public String getRAM() { return ram; }
@Override
public String toString() {
return "RAM= " + getRAM() + ", Hdd= " + getHDD() + ", CPU= " + getCPU();
}
}
Реализация
public class Server extends Computer {
public Server(){}
public Server(String ram, String hdd, String cpu) {
super(ram, hdd, cpu);
}
}
public class PC extends Computer {
public PC(){}
public PC(String ram, String hdd, String cpu) {
super(ram, hdd, cpu);
}
}
Разобравшись с нашей иерархией мы можем приступать к реализации фабрики. Я подготовил несколько реализаций.
Простая Фабрика
Эта реализация не выделяется ничем особенным.
public class SimpleFactory {
private static final Map<String, Class<? extends Computer>> factoryForm = new HashMap<>();
private static Computer computer;
static {
factoryForm.put("pc", PC.class);
factoryForm.put("server", Server.class);
}
private SimpleFactory(){}
private static Computer getComputerTrobelsome(String type, String ram, String hdd, String cpu) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NullPointerException {
if( factoryForm.containsKey(type)) {
return factoryForm.get(type).getConstructor(String.class, String.class, String.class).newInstance(ram, hdd, cpu);
}
throw new NullPointerException();
}
public static Computer getComputer(String type, String ram, String hdd, String cpu) {
try {
computer = getComputerTrobelsome(type, ram, hdd, cpu);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}catch (NullPointerException e){
System.out.println("No such computer type.");
}
return computer;
}
}
Есть Map, который содержит классы, при помощи которых можно будет создать интересующие нас объекты. Метод getComputer принимает необходимее аргументы, достаёт из factoryForm необходимый класс и при помощи нехитрой рефлексии мы получаем интересующие нас объекты. Да, использование рефлексии чревато излишками в коде, и это плохо, но, во-первых, суть поста не в рефлексии, а в паттерне, во-вторых, я использовал рефлексию, потому что могу. В своей реализации можете использовать любой другой подход.
Enum в качестве фабрики
После реализации предыдущего примера я начал думать, что бы ещё такое сделать, и мне в голову пришла мысль: почему бы не использовать перечисления в качестве фабрики.
public enum EnamFactory {
PC(com.epam.pattern.factory.model.PC.class) {
@Override
public Computer getInstance(String ram, String hdd, String cpu) {
return new PC(ram, hdd, cpu);
}
}, SERVER(Server.class) {
@Override
public Computer getInstance(String ram, String hdd, String cpu) {
return new Server(ram, hdd, cpu);
}
};
private Class<? extends Computer> pattern;
EnamFactory(Class<? extends Computer> pattern) {
this.pattern = pattern;
}
public abstract Computer getInstance(String ram, String hdd, String cpu);
public static Computer getInstance(EnamFactory pattern, String ram, String hdd, String cpu) {
return pattern.getInstance(ram, hdd, cpu);
}
}
Здесь все предельно ясно. Создаём перечисление с каким-то абстрактный методом, который будет возвращать нам объект конкретных классов. Для каждого перечисления этот метод будет содержать логику для создания конкретных объектов.
Параметризованная фабрика
Мне всё ещё было недостаточно того, что я сделал. Потому представляю вашему вниманию последнюю на сегодня реализацию паттерна фабрика. Она универсальная и даёт возможность создавать объекты различных иерархий, даже если классы будут иметь конструкторы с различным набором аргументов, она будет работать. Конечно, возможно, я упустил некоторые возможные ошибки и буду благодарен, если вы любезно укажете на них в ваших комментариях.
public class GenericFactory<T> {
private static final Logger LOGGER = Logger.getLogger(GenericFactory.class);
private Map<String, Class<? extends T>> factoryForm = new HashMap<>();
private T computer;
public GenericFactory(Class<? extends T>... patterns) {
for (Class pattern : patterns) {
factoryForm.put(pattern.getSimpleName(), pattern);
}
}
private T getInstanceTrobelsome(String type, Object... params) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NullPointerException {
if (factoryForm.containsKey(type)) {
return factoryForm.get(type).getConstructor(getAttrClasses(params)).newInstance(params);
}
throw new NullPointerException();
}
private Class[] getAttrClasses(Object... params) {
Class[] classes = new Class[params.length];
for (int index = 0; index < params.length; index++) {
classes[index] = params[index].getClass();
}
return classes;
}
private T getInstanceHandler(String type, Object... params) {
try {
computer = getInstanceTrobelsome(type, params);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NullPointerException e) {
LOGGER.error("No such factory pattern !");
}
return computer;
}
public T getInstance(String type) {
return getInstanceHandler(type, new Object[0]);
}
public T getInstance(Class<? extends T> type) {
return getInstanceHandler(type.getSimpleName(), new Object[0]);
}
public T getInstance(Class<? extends T> type, Object... params) {
return getInstanceHandler(type.getSimpleName(), params);
}
public T getInstance(String type, Object... params) {
return getInstanceHandler(type, params);
}
}
Опять же, эта реализация полнится рефлексией, но благодаря этому и возможно такое поведение робота с несколькими иерархиями. Ещё один недостаток этой реализации: в качестве аргументов в конструкторе нельзя использовать примитивы. И, наконец, я приведу результат роботы этой чудесной фабрики.
Вторая модель
public abstract class Fruit {
protected String name;
private int weight;
public Fruit(){};
public Fruit(String name, Integer weight){
this.name = name;
this.weight = weight;
}
@Override
public String toString() {
return String.format("Data = %s, Wetdht = %s", name, weight);
}
}
public class Apple extends Fruit {
public Apple() {}
public Apple(String data, Integer weight) {
super(data, weight);
}
}
public class Pomegranate extends Fruit {
public Pomegranate(){}
public Pomegranate(String data, Integer weight){
super(data, weight);
}
}
Мейн
public class Main {
public static void main(String[] args) {
GenericFactory<Computer> computerFactory = new GenericFactory<>(PC.class, Server.class);
System.out.println("PC\t"+computerFactory.getInstance(PC.class.getSimpleName()));
System.out.println("Server\t"+computerFactory.getInstance(Server.class, "32", "23", "32"));
System.out.println("PC\t"+computerFactory.getInstance("PC", "32", "23", "32"));
GenericFactory<Fruit> fruitFactory = new GenericFactory<>(Pomegranate.class, Apple.class);
System.out.println("Pomegranate\t"+ fruitFactory.getInstance(Pomegranate.class));
System.out.println("Apple\t"+fruitFactory.getInstance(Apple.class, "adas", 23));
}
}
И наконец результат роботы программы
PC RAM= null, Hdd= null, CPU= null
Server RAM= 32, Hdd= 23, CPU= 32
PC RAM= 32, Hdd= 23, CPU= 32
Pomegranate Data = null, Wetdht = 0
Apple Data = adas, Wetdht = 23
Выводы
Паттерн Фабрика удаляем создание экземпляра из клиентского кода, что делает его более устойчивым, менее связанным и легко расширяемым. Обеспечивает абстракцию между реализацией и клиентскими классами посредством наследования.
Что ж, на этом все. Спасибо за внимание. Надеюсь, этот пост был для вас полезным. Буду благодарен за ваши комментарии (возможно, в посте были допущены какие-то ошибки или неточности).
Комментарии (12)
nomit
11.05.2016 17:28С такой реализаций через generic одни проблемы. Как минимум можно heap pollution хапнуть.
KReal
11.05.2016 17:32А зачем нужна generic фабрика, если внутри неё просто дёргается конструктор и никакой логики сверх этого?
HikenLVK
11.05.2016 17:53Я пытался рассмотреть различные подходы к реализациям паттерна. Нет сомнений, что этот дженерик скорее всего не найдёт нигде своего места, но мне было интересно рассмотреть такую возможность и возможно кому-то это тоже будет интересно.
mbait
11.05.2016 18:41ведь сеть уже и так наполнена массой подобных постов
перспективный разработчик
Перспективный разработчик в первую очередь учится делать простые вещи простыми и не порождать лишних сущностей. Советую вместо паттернов прочитать про KISS и бритву Оккама.HikenLVK
11.05.2016 21:00«Перспективный разработчик в первую очередь учится делать простые вещи простыми и не порождать лишних сущностей.» — согласен, но я считаю что ограничивая себя готовыми тривиальными примерами не очень полезно. Я не призываю прибегать к такому коду в реальных проектах, такой подход поможет развить не стандартное мышление и лучше изучить возможности языка.
mbait
11.05.2016 21:16Я не совсем понял, о чем комментарий. Выше я намекал, что прежде чем что-то добавлять, разработчик должен проверить сушествующие решения. На Хабра они (статьи про фабрики) есть, поэтому я не вижу смысла в существовании ещё одной. То, что лично вы разобрались — это здорово, но не обязательно всему интернету знать об этом. В любом случае — важность шаблонов в разработке сильно преувеличена. Иногда даже вредна. Основная причина — шаблонов гораздо больше, чем в той же книге банды четырёх, потому свежий программист начитавшись, при решении реальной задачи сразу думает «Ага, какой же шаблон я буду тут использовать?», вместо того, чтобы просто логически или интуитивно прикинуть зависимость будущих компонентов.
RomanVPro
11.05.2016 21:01Оставляю вопрос полезности данной статьи открытым, но у меня есть несколько замечаний к самому коду. По-моему, создание экземпляров класса через getConstructor().newInstance() весьма криво и непрактично: разве IDE сможет провести рефакторинг в данной случае? Например, я захочу добавить еще один параметр в конструктор. IntelliJ IDEA позволяет это сделать весьма просто и быстро. Но вот разобраться с newInstance() ей уже не под силу. Второй момент в передаваемом параметре type — я считаю, что это должен быть enum, потому что это сильно обегчает дальнейшее использование кода — IDE будет подсказывать варианты, программисту не будет нужно лезть в исходники/читать доки, чтобы найти поддерживаемые типы. В результате, я бы заменил рефлексию на switch по enum.
HikenLVK
11.05.2016 21:13Касаемо добавления нового аргумента в конструктор. В последнем примере опасаться рефакторинга не стоит, так как при создании объекта я использую vararg поэтому все будет работать как надо, если передать все нужные аргументы. По этому можно смело изменять конструктор. «создание экземпляров класса через getConstructor().newInstance() весьма криво и непрактично» — да я упоминал что использование рефлексии всячески нежеланно, но хотел попробовать что из этого получится.
vyatsek
12.05.2016 12:45> Рисунок даст возможность лучше понять
Диаграмма Карл, ДИАГРАММА! Как же так молодой и перспективный разработчик UML схемы называет рисунком?
>Мейн
4To6bl y 4uTaTeJl9l rJla3a /7oJlaMaJlucb?
Примеры очень вырожденные. Классика фабрики это читаем пишем в разные типы хранилищ: база/файл/сервер. Там будут свои иерархии отвечающие за подключение, запись, нотификации. Например попробуйте реализовать push нотификацию.
Мне бы были интересны диаграммы, которые отражают структуру работающего кода, т.к. смотреть сам код и восстанавливать общую картину дело неблагодарное.
Одно главное вам уже сказали, что не раскрыта проблема при которой паттерн необходимо применять, а вторая главная вещь, какие минусы и ограничения этого паттерна.
bromzh
Так где же ответ на самый главный вопрос этой статьи: "зачем этот пост, ведь сеть уже и так наполнена массой подобных постов?"
HikenLVK
Ну конечно, в первую очередь я написал его для себя. И потом я захотел поделиться своими соображениями по этому поводу и своей реализацией этого паттерна. Да он явно не тянет на лучший (наверное наоборот грето на дне хабры) и мне не хватает опита в подобной деятельности. Надеюсь мои следующие посты (если такие будут) будут лучше.
bromzh
Ну просто реализация обычных фабрик и так довольно тривиальна и есть даже на вики. Хорошо, что вы осознаёте нехватку опыта, но не стоит учиться путём публикования своих великов. Если хочется что-то узнать про свой код, то есть тостер.
А "параметризированная" фабрика из статьи больше смахивает на сервис-локатор. Но опять же, все популярные паттерны проектирования обсуждались кучу раз. Ещё один "туториал" не нужен.