Как вы уже догадались из названия поста, речь пойдёт о широко известном паттерне проектирования Factory. Вы спросите: «зачем этот пост, ведь сеть уже и так наполнена массой подобных постов?». Но не судите меня за желание поделиться плодами своего труда.

Немножко о себе. Я молодой, не опытный, перспективный разработчик в области BigData, короче говоря junior. Получил задание разобраться в некоторых паттернах проектирования (на мой вкус), и первым я решил исследовать Factory Design Pattern.

Что собой представляет Factory


Паттерн относится к «порождающей» группе паттернов. Название «фабрика» говорит само за себя. Фабрика занимается созданием некоторой продукции. Мы поставляем ей сырье и в результате получаем полноценный продукт. Получается такой себе чёрный ящик, в котором механизм создания объектов скрыт. Это и есть цель паттерна фабрика. Скрыть излишки логики от глаз. Рисунок даст возможность лучше понять, как этот паттерн должен быть реализован.

image

Есть какая-то иерархия классов, для которой вы хотели бы иметь удобную фабрику и по желанию получать тот или иной объект без использования 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)


  1. bromzh
    11.05.2016 17:24
    +2

    Так где же ответ на самый главный вопрос этой статьи: "зачем этот пост, ведь сеть уже и так наполнена массой подобных постов?"


    1. HikenLVK
      11.05.2016 17:49

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


      1. bromzh
        11.05.2016 18:27

        Ну просто реализация обычных фабрик и так довольно тривиальна и есть даже на вики. Хорошо, что вы осознаёте нехватку опыта, но не стоит учиться путём публикования своих великов. Если хочется что-то узнать про свой код, то есть тостер.


        А "параметризированная" фабрика из статьи больше смахивает на сервис-локатор. Но опять же, все популярные паттерны проектирования обсуждались кучу раз. Ещё один "туториал" не нужен.


  1. nomit
    11.05.2016 17:28

    С такой реализаций через generic одни проблемы. Как минимум можно heap pollution хапнуть.


  1. KReal
    11.05.2016 17:32

    А зачем нужна generic фабрика, если внутри неё просто дёргается конструктор и никакой логики сверх этого?


    1. HikenLVK
      11.05.2016 17:53

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


  1. mbait
    11.05.2016 18:41

    ведь сеть уже и так наполнена массой подобных постов

    перспективный разработчик


    Перспективный разработчик в первую очередь учится делать простые вещи простыми и не порождать лишних сущностей. Советую вместо паттернов прочитать про KISS и бритву Оккама.


    1. HikenLVK
      11.05.2016 21:00

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


      1. mbait
        11.05.2016 21:16

        Я не совсем понял, о чем комментарий. Выше я намекал, что прежде чем что-то добавлять, разработчик должен проверить сушествующие решения. На Хабра они (статьи про фабрики) есть, поэтому я не вижу смысла в существовании ещё одной. То, что лично вы разобрались — это здорово, но не обязательно всему интернету знать об этом. В любом случае — важность шаблонов в разработке сильно преувеличена. Иногда даже вредна. Основная причина — шаблонов гораздо больше, чем в той же книге банды четырёх, потому свежий программист начитавшись, при решении реальной задачи сразу думает «Ага, какой же шаблон я буду тут использовать?», вместо того, чтобы просто логически или интуитивно прикинуть зависимость будущих компонентов.


  1. RomanVPro
    11.05.2016 21:01

    Оставляю вопрос полезности данной статьи открытым, но у меня есть несколько замечаний к самому коду. По-моему, создание экземпляров класса через getConstructor().newInstance() весьма криво и непрактично: разве IDE сможет провести рефакторинг в данной случае? Например, я захочу добавить еще один параметр в конструктор. IntelliJ IDEA позволяет это сделать весьма просто и быстро. Но вот разобраться с newInstance() ей уже не под силу. Второй момент в передаваемом параметре type — я считаю, что это должен быть enum, потому что это сильно обегчает дальнейшее использование кода — IDE будет подсказывать варианты, программисту не будет нужно лезть в исходники/читать доки, чтобы найти поддерживаемые типы. В результате, я бы заменил рефлексию на switch по enum.


    1. HikenLVK
      11.05.2016 21:13

      Касаемо добавления нового аргумента в конструктор. В последнем примере опасаться рефакторинга не стоит, так как при создании объекта я использую vararg поэтому все будет работать как надо, если передать все нужные аргументы. По этому можно смело изменять конструктор. «создание экземпляров класса через getConstructor().newInstance() весьма криво и непрактично» — да я упоминал что использование рефлексии всячески нежеланно, но хотел попробовать что из этого получится.


  1. vyatsek
    12.05.2016 12:45

    > Рисунок даст возможность лучше понять
    Диаграмма Карл, ДИАГРАММА! Как же так молодой и перспективный разработчик UML схемы называет рисунком?

    >Мейн
    4To6bl y 4uTaTeJl9l rJla3a /7oJlaMaJlucb?

    Примеры очень вырожденные. Классика фабрики это читаем пишем в разные типы хранилищ: база/файл/сервер. Там будут свои иерархии отвечающие за подключение, запись, нотификации. Например попробуйте реализовать push нотификацию.

    Мне бы были интересны диаграммы, которые отражают структуру работающего кода, т.к. смотреть сам код и восстанавливать общую картину дело неблагодарное.

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