Фабрика - Паттерн разработки

 Определение

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

Классическая схема фабричного метода представлена выше. Здесь Х - базовый класс, а может быть даже интерфейс. Классы XY и XZ по-разному реализуют интерфейс Х. Для выбора конкретной реализации используется класс Factory, где в методе getClass происходит анализ внешних параметров abc, и, основываясь на этих данных, метод возвращает объект класса X используя реализацию XY либо XZ. Таким образом, внешняя программа не имеет представления о том, объект какого именно класса - XY или XZ - ей вернули, поскольку оба эти класса реализуют общий интерфейс Х, а значит, вызов их внешних методов ничем не отличается. Это удобно.

Когда используется и в чем его достоинства

  • Позволяет связать параллельные иерархии классов
  • Когда конкретная реализация объектов данного класса Х задается с помощью подклассов
  • Когда заранее не известны конкретные реализации данного класса Х
  • Когда требуется использовать интерфейс Х вместо конкретных реализующих классов
  • Для скрытия конкретных классов от клиента
  • Фабричные методы могут быть параметризованы
  • Позволяет следовать общепринятым правилам именования, помогает реорганизовать код при необходимости

Примеры

 

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

 

Для теста всех примеров я создам пакет org.test.technerium.patterns.factory, а в нем класс RunTestFactory с обычным main'ом, в котором буду запускать классы примеров в тестовыми аргументами. Об этом будет сказано ниже.

package org.test.technerium.patterns.factory;

public class RunTestFactory {
    public static void main(String[] args){
        log(">Start test for Factory pattern");
        //тут будут вызываться тесты
    }
    
    public static void log(String msg){ //А это для вывода сообщений в консоль, для удобства
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

1. Классическая фабрика

В качестве примера рассмотрим ситуацию, в которой в нашей программе мы создаем объект некоего клиента, а затем обращаемся к его методам. При этом существует несколько реализаций клиента - для разных операционных систем. Естественно, что в таком случае удобно написать некий интерфейс Client, определяющий набор методов, к которым мы хотим обращаться. А конкретную реализацию этих методов для разных ОС определить в классах ClientLinuxImpl и ClientWinImpl. Затем создать класс-фабрику, который будет принимать, скажем, id операционной системы и возвращать объект соответствующего клиента.

 

Посмотрим как это реализовано в коде в пакете org.test.technerium.patterns.factory.ex1

 

Интерфейс Client задает наличие всего одного метода - getTargetOS, который, очевидно, возвращает название операционной системы, для которой предназначен клиент:

package org.test.technerium.patterns.factory.ex1;

public interface Client {
    public String getTargetOS();
}

 

Затем реализации клиентов для Linux и для Windows:

package org.test.technerium.patterns.factory.ex1;

public class ClientLinuxImpl implements Client{

    @Override
    public String getTargetOS() {
        return "Gentoo Linux";
    }

}

 

package org.test.technerium.patterns.factory.ex1;

public class ClientWinImpl implements Client{

    @Override
    public String getTargetOS() {
        return "Windows Vista";
    }

}

Дальше пишем простейшую фабрику, в которой метод getClient получает id операционной системы и возвращает объект соответствующего класса, либо null если айдишник не соответсвует ни одной ОС:

 

package org.test.technerium.patterns.factory.ex1;


public class Factory {
    
    public Client getClient(String currentOS){
        if(currentOS.equals("win"))
            return new ClientWinImpl();
        else if(currentOS.equals("linux"))
            return new ClientLinuxImpl();
        return null;
    }
}

Для тестирования примера создадим еще класс RunTestFactoryEx1:

 

package org.test.technerium.patterns.factory.ex1;

import org.test.technerium.patterns.factory.RunTestFactory;

public class RunTestFactoryEx1 extends RunTestFactory{
    
    public static void main(String[] args){
        log("----> Start Example 1");
        String currentOS = args[0];//получаем айдишник ОС
        Factory factory = new Factory(); //Инициализируем фабрику

        log("    OS id: [" + currentOS + "], creating client");
        Client client = factory.getClient(currentOS);//Инициализируем клиент типа Client
        
        if(client!=null){//Фабрика нашла подходящий клиент, хорошо
            log("    Client created for OS:");
            log(client.getTargetOS());//Конкретная реализация метода здесь не видна, что дает нам свободу
        }else{
            log("    No client!");
        }
        log("<---- Finish Example 1");
    }
}

Здесь мы получаем айдишник ОС из параметров методам main, создаем переменную клиента, инициализируем фабрику и получаем объект нашего клиента. Заметьте: клиент у нас типа Client, то есть в момент написания кода мы понятия не имеем о конкретных реализациях интерфейса для разных ОС. Таким образом, наша работа с клиентом совершенно свободна от того, как тот реализован(ClientWinImpl или ClientLinuxImpl). Сегодня это может быть Win, завтра Linux, а послезавтра программисты из другого подразделения добавят реализацию под MacOS, а наш код этого даже не заметит, достаточно будет добавить пару строк в код фабрики. Фабричный метод getClient, кстати, мог быть и статическим, разницы в данном случае не было бы.

Далее проверяем, что клиент создан нормально и обращаемся к его методу getTargetOS, в противном случае выводим сообщение об ошибке.

 

Добавляем вызовы нашего теста в класс RunTestFactory, о котором я писал в самом начале:

 

package org.test.technerium.patterns.factory;

import org.test.technerium.patterns.factory.ex1.RunTestFactoryEx1;

public class RunTestFactory {
    public static void main(String[] args){
        log(">Start test for Factory pattern");

        RunTestFactoryEx1.main(new String[]{"win"});
        RunTestFactoryEx1.main(new String[]{"linux"});
        RunTestFactoryEx1.main(new String[]{"mac"});
        log("==");
        
    }
    
    public static void log(String msg){
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

Итак, сначала должен быть создан клиент для Windows, затем Linux, а далее должно быть сообщение об ошибке. Запускаем наш тест и в консоли получаем:

 

>Start test for Factory pattern
----> Start Example 1
    OS id: [win], creating client
    Client created for OS:
Windows Vista
<---- Finish Example 1
----> Start Example 1
    OS id: [linux], creating client
    Client created for OS:
Gentoo Linux
<---- Finish Example 1
----> Start Example 1
    OS id: [mac], creating client
    No client!
<---- Finish Example 1
==

Что же, фабрика работает. При желании, теперь можно добавить хоть 10 реализаций клиента, определить соответствующие условия в Factory и всё будет работать, править другой код не придется.

 


2. Неклассическая фабрика enlightened

На самом деле, если в фабрике сделать не один параметризованный метод для всех клиентов, а по одному непараметризованному методу для каждой реализации клиента, и выбирать, какой метод вызывать, прямо в классе для тестирования (то есть в нашем микро-приложении), то это все равно можно рассматривать как фабричный паттерн. Для этого примера я создал новый пакет org.test.technerium.patterns.factory.ex2 и изменил код фабрики:

 

package org.test.technerium.patterns.factory.ex2;

public class Factory {
    public Client getWinClient(){
        return new ClientWinImpl();
    }
    
    public Client getLinuxClient(){
        return new ClientLinuxImpl();
    }
}

 

Соответственно, поменяем и код приложения:

package org.test.technerium.patterns.factory.ex2;

import org.test.technerium.patterns.factory.RunTestFactory;

public class RunTestFactoryEx2 extends RunTestFactory{
    
    public static void main(String[] args){
        log("----> Start Example 2");
        String currentOS = args[0];
        Client client = null;
        Factory factory = new Factory();
        log("    OS id: [" + currentOS + "], creating client");
        if(currentOS.equals("win"))
            client = factory.getWinClient();
        else if(currentOS.equals("linux"))
            client = factory.getLinuxClient();
        //Всё, что ниже, свободно от конкретной реализации клиента
        if(client!=null){
            log("    Client created for OS:");
            log(client.getTargetOS());
        }else{
            log("    Wrong OS id provided!");
        }
        log("<---- Finish Example 2");
    }
}

 

Добавим в наш главный тестовый класс код для вызова 2 теста с тремя разными параметрами:

 

package org.test.technerium.patterns.factory;

import org.test.technerium.patterns.factory.ex1.RunTestFactoryEx1;
import org.test.technerium.patterns.factory.ex2.RunTestFactoryEx2;

public class RunTestFactory {
    public static void main(String[] args){
        log(">Start test for Factory pattern");

        RunTestFactoryEx1.main(new String[]{"win"});
        RunTestFactoryEx1.main(new String[]{"linux"});
        RunTestFactoryEx1.main(new String[]{"mac"});
        log("==");
        
//Для второго примера:
        RunTestFactoryEx2.main(new String[]{"win"});
        RunTestFactoryEx2.main(new String[]{"linux"});
        RunTestFactoryEx2.main(new String[]{"mac"});
        log("==");
        
    }
    
    public static void log(String msg){
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

Запустим тест, в консоли увидим новые сообщения, говорящие о том, что всё работает:

>Start test for Factory pattern
...
==
----> Start Example 2
    OS id: [win], creating client
    Client created for OS:
Windows Vista
<---- Finish Example 2
----> Start Example 2
    OS id: [linux], creating client
    Client created for OS:
Gentoo Linux
<---- Finish Example 2
----> Start Example 2
    OS id: [mac], creating client
    Wrong OS id provided!
<---- Finish Example 2
==

 


3. Перегрузка конструктора фабрики

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

 

Создаем новый пакет org.test.technerium.patterns.factory.ex3, меняем код фабрики на что-то вроде этого:

package org.test.technerium.patterns.factory.ex3;

public class Factory {
    private Client innerClient; //внутренний клиент
    public Factory() { // конструктор без аргументов
        log("    Initialize factory w/o args");
        innerClient = new ClientLinuxImpl();
    }
    
    public Factory(String arg) { // С одним аргументом
        log("    Initialize factory with arg: " + arg);
        innerClient = new ClientWinImpl();
    }
    
    public void runClient(){ // метод для работы клиента, свободный от конкретной реализации клиента
        log("    Inner client created for OS: " + innerClient.getTargetOS());
    }
    
    public static void log(String msg){// Вывод в консоль
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

 

Здесь два конструктора - один без аргументов, создает клиент для линукса, а второй с одним аргументом типа String - создает клиент под Windows. Можно было бы сделать первый конструктор с параметром int, к примеру. Как мы видим, опять рабочий код (метод runClient) будет независим от реализации клиента.

 

Тестовый класс:

 

package org.test.technerium.patterns.factory.ex3;

import org.test.technerium.patterns.factory.RunTestFactory;

public class RunTestFactoryEx3 extends RunTestFactory{
    
    public static void main(String[] args){
        log("----> Start Example 3");
        String parameter = null;
        if(args!=null && args.length>0)
            parameter = args[0];
        Factory factory = null;
        if(parameter == null)
            factory = new Factory();
        else
            factory = new Factory(parameter);
        
        factory.runClient();
        log("<---- Finish Example 3");
    }
}

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

Добавим еще пару строк в главный тестовый класс:

package org.test.technerium.patterns.factory;

import org.test.technerium.patterns.factory.ex1.RunTestFactoryEx1;
import org.test.technerium.patterns.factory.ex2.RunTestFactoryEx2;
import org.test.technerium.patterns.factory.ex3.RunTestFactoryEx3;

public class RunTestFactory {
    public static void main(String[] args){
        log(">Start test for Factory pattern");

        RunTestFactoryEx1.main(new String[]{"win"});
        RunTestFactoryEx1.main(new String[]{"linux"});
        RunTestFactoryEx1.main(new String[]{"mac"});
        log("==");
        

        RunTestFactoryEx2.main(new String[]{"win"});
        RunTestFactoryEx2.main(new String[]{"linux"});
        RunTestFactoryEx2.main(new String[]{"mac"});
        log("==");
        
        RunTestFactoryEx3.main(null);
        RunTestFactoryEx3.main(new String[]{"arg"});
        log("==");
        
    }
    
    public static void log(String msg){
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

Запустим его и в консоли увидим:

>Start test for Factory pattern
...
==
----> Start Example 3
    Initialize factory w/o args
    Inner client created for OS: Gentoo Linux
<---- Finish Example 3
----> Start Example 3
    Initialize factory with arg: arg
    Inner client created for OS: Windows Vista
<---- Finish Example 3
==

 

4. Перегрузка метода фабрики

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

package org.test.technerium.patterns.factory.ex4;

public class Factory {
    public Factory() {
        log("    Will call Factory Method w/o args");
        getTargetOS();
    }
    
    public Factory(String arg) {
        log("    Will call Factory Method with arg: " + arg);
        getTargetOS(arg);
    }

    private void getTargetOS(){
        log("Gentoo Linux");
    }

    private void getTargetOS(String arg){
        log("Windows Vista");
    }
    public static void log(String msg){
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
    
}

Мне не очень нравится этот пример, однако он иллюстрирует так называемый фабричный метод.

В главном тестовом классе добавим еще пару строк

package org.test.technerium.patterns.factory;

import org.test.technerium.patterns.factory.ex1.RunTestFactoryEx1;
import org.test.technerium.patterns.factory.ex2.RunTestFactoryEx2;
import org.test.technerium.patterns.factory.ex3.RunTestFactoryEx3;
import org.test.technerium.patterns.factory.ex4.RunTestFactoryEx4;

public class RunTestFactory {
    public static void main(String[] args){
        log(">Start test for Factory pattern");

        RunTestFactoryEx1.main(new String[]{"win"});
        RunTestFactoryEx1.main(new String[]{"linux"});
        RunTestFactoryEx1.main(new String[]{"mac"});
        log("==");
        

        RunTestFactoryEx2.main(new String[]{"win"});
        RunTestFactoryEx2.main(new String[]{"linux"});
        RunTestFactoryEx2.main(new String[]{"mac"});
        log("==");
        
        RunTestFactoryEx3.main(null);
        RunTestFactoryEx3.main(new String[]{"arg"});
        log("==");
        
        RunTestFactoryEx4.main(null);
        RunTestFactoryEx4.main(new String[]{"arg"});
        log("==");
      
        
    }
    
    public static void log(String msg){
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

При запуске теста увидим, что фабрика отработала правильно:

...
==
----> Start Example 4
    Will call Factory Method w/o args
Gentoo Linux
<---- Finish Example 4
----> Start Example 4
    Will call Factory Method with arg: arg
Windows Vista
<---- Finish Example 4
==

5. Вообще обойдемся без класса фабрики

Естественно, что можно просто перенести выбор реализации объекта из фабрики в начало нашего рабочего кода, так что для последнего тестового пакета org.test.technerium.patterns.factory.ex5 создадим следующий тестовый класс:

package org.test.technerium.patterns.factory.ex5;

import org.test.technerium.patterns.factory.RunTestFactory;

public class RunTestFactoryEx5 extends RunTestFactory{
    
    public static void main(String[] args){
        log("----> Start Example 5");
        String currentOS = args[0];
        Client client = null;
        log("    OS id: [" + currentOS + "], creating client");
        if(currentOS.equals("win"))
            client = new ClientWinImpl();
        else if(currentOS.equals("linux"))
            client = new ClientLinuxImpl();
        //Далее следует код, свободный от конкретной реализации клиента
        if(client!=null){
            log("    Client created for OS:");
            log(client.getTargetOS());
        }else{
            log("    Wrong OS id provided!");
        }
        log("<---- Finish Example 5");
    }
}

Естественно, добавим из в пакет интерфейс Client и два класса его реализующих и предыдущих примеров. Допишем еще несколько строк вызова теста в главном тестовом классе:

package org.test.technerium.patterns.factory;

import org.test.technerium.patterns.factory.ex1.RunTestFactoryEx1;
import org.test.technerium.patterns.factory.ex2.RunTestFactoryEx2;
import org.test.technerium.patterns.factory.ex3.RunTestFactoryEx3;
import org.test.technerium.patterns.factory.ex4.RunTestFactoryEx4;
import org.test.technerium.patterns.factory.ex5.RunTestFactoryEx5;

public class RunTestFactory {
    public static void main(String[] args){
        log(">Start test for Factory pattern");

        RunTestFactoryEx1.main(new String[]{"win"});
        RunTestFactoryEx1.main(new String[]{"linux"});
        RunTestFactoryEx1.main(new String[]{"mac"});
        log("==");
        

        RunTestFactoryEx2.main(new String[]{"win"});
        RunTestFactoryEx2.main(new String[]{"linux"});
        RunTestFactoryEx2.main(new String[]{"mac"});
        log("==");
        
        RunTestFactoryEx3.main(null);
        RunTestFactoryEx3.main(new String[]{"arg"});
        log("==");
        
        RunTestFactoryEx4.main(null);
        RunTestFactoryEx4.main(new String[]{"arg"});
        log("==");
        

        RunTestFactoryEx5.main(new String[]{"win"});
        RunTestFactoryEx5.main(new String[]{"linux"});
        RunTestFactoryEx5.main(new String[]{"mac"});
        log("<Finish test for Factory pattern");
        
    }
    
    public static void log(String msg){
        if(msg==null)
            msg = "null";
        System.out.println(msg);
    }
}

Запустим тест:

>Start test for Factory pattern
...
==
----> Start Example 5
    OS id: [win], creating client
    Client created for OS:
Windows Vista
<---- Finish Example 5
----> Start Example 5
    OS id: [linux], creating client
    Client created for OS:
Gentoo Linux
<---- Finish Example 5
----> Start Example 5
    OS id: [mac], creating client
    Wrong OS id provided!
<---- Finish Example 5
<Finish test for Factory pattern

Надеюсь, теперь вы получили представление о паттерне Фабрика на примере Java. Все примеры в архиве проекта для Eclipse можно скачать по ссылке.

Возможно, вам будут интересны и другие шаблоны, представленные в обзорной статье

Была ли статья полезна: 

Комментарии

return null не нужно писать там где if else в первом примере.

Я так понял, вы про этот кусок:

 

package org.test.technerium.patterns.factory.ex1;


public class Factory {
    
    public Client getClient(String currentOS){
        if(currentOS.equals("win"))
            return new ClientWinImpl();
        else if(currentOS.equals("linux"))
            return new ClientLinuxImpl();
        return null;
    }
}

 

Вы правы, тут действительно можно не писать return null если убрать последний if. Иначе будет ошибка.

 

package org.test.technerium.patterns.factory.ex1;


public class Factory {
    
    public Client getClient(String currentOS){
        if(currentOS.equals("win"))
            return new ClientWinImpl();
        else
            return new ClientLinuxImpl();
    }
}

Разве тогда не получится, что для "mac" он будет создавать ClientLinuxImpl ?

Хм, действительно. Значит при ответе на предыдущий комментарий я не очень внимательно посмотрел код. В статье всё правильно, если задаем параметр для "mac" то должен возвращаться null, спасибо за ваш комментарий.

Wrong OS id provided и No client! надо местами поменять

Вот спасибо за внимательность) Поправил.

Добавить комментарий

HTML

  • Разрешённые HTML-теги: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <pre> <p>
  • Строки и параграфы переносятся автоматически.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Поместите примеры вашего исходного кода в теги <code>...</code> or <source>...</source> и он будет красиво отформатирован.

Plain text

  • Поместите примеры вашего исходного кода в теги <code>...</code> or <source>...</source> и он будет красиво отформатирован.
  • Строки и параграфы переносятся автоматически.
  • Разрешённые HTML-теги: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <code> <source>
CAPTCHA
Пожалуйста, подтвердите, что вы человек.