Фабрички метод (пројектни узорак)

С Википедије, слободне енциклопедије
Фабрички метод у UML-у

Фабрички метод је објектно-оријентисани пројектни узорак. Као и остали пројектни узорци креирања, бави се проблемима креирања објеката без навођења конкретне класе објекта који се креира. Фабрички метод решава овај проблем тако што дефинише посебну методу за креирање објеката, коју онда поткласе преклапају да одреде тачан подтип објекта (тј. изведену класу) који ће бити креиран. Општије, фабрички метод се често користи да означи било коју методу чија је основна сврха креирање објеката.

Дефиниција[уреди | уреди извор]

Суштина фабричког метода је да „дефинише интерфејс за креирање објеката, али да остави поткласама да одлуче чије објекте креирају. Фабрички метод допушта класи да делегира стварање објекта поткласи.“[1]

Честе употребе[уреди | уреди извор]

Фабрички метод се често користи у радним оквирима (енгл. frameworks) где кôд радног оквира треба да креира објекте којима је познат само надтип (који дефинише радни оквир), али не и конкретни подтип објекта који дефинише клијентски кôд који користи овај радни оквир.

Паралелне класе хијерархија често захтевају да објекти једне хијерархије креирају објеке из друге хијерархије.

Фабрички методи се често користе у test-driven развоју да би се класе лакше тестирале. [2] Претпоставимо да постоји класа Foo која креира други објекат класе Dangerous који се не може аутоматизовано тестирати јединичним тестовима (нпр. комуницира са продукционом базом). Тада се креирање објекта Dangerous ставља у виртуелну фабричку методу createDangerous() у класу Foo. За тестирање, прави се класа TestFoo са преклопљеном методом createDangerous() која креира и враћа лажни објекат FakeDangerous. Јединични тестови сада користе TestFoo да тестирају функционалност коју даје класа Foo без опасности и споредних ефеката који би настали да се користи објекат Dangerous.

Остале предности и варијанте[уреди | уреди извор]

Иако је основна мотивација фабричког метода да дозволи поткласама да одлуче тип објекта које креирају, постоје и остале предности коришћења фабричког метода, од којих многе не зависе од наслеђивања класа. Отуда, честе се дефинишу „фабрички методи“ који уопште нису полиморфни и који служе за креирање објеката, а само да би се добиле ове остале предности. Такви методи су обично статички.

Описна имена[уреди | уреди извор]

Фабрички метод може имати јасно име. Проблем је што у многим објектно-оријентисаним језицима, конструктор мора имати исто име као и класа у којој се налази, а то некад може довести до вишезначности ако постоји више од једног начина да се креира објекат. Фабрички методи немају оваква ограничења и могу имати описна имена. Као пример, када се комплексни бројеви креирају од два реална броја, та два броја могу да представљају или картезијанске или поларне координате, али коришћењем фабричке методе, значење је јасно (следећи примери су написани у програмском језику Јава):

class Complex
{
     public static Complex fromCartesian(double real, double imag) {
         return new Complex(real, imag);
     }
 
     public static Complex fromPolar(double modulus, double angle) {
         return new Complex(modulus * cos(angle), modulus * sin(angle));
     }
  
     private Complex(double a, double b) {
         //...
     }
}
  
 Complex c = Complex.fromPolar(1, pi); // Isto kao i fromCartesian(-1, 0)

Када се фабрички методи користе да се овако разреше вишезначности, обично се конструктори стављају да буду приватни да би се на тај начин клијенти приморали да користе фабричке методе.

Енкапсулација[уреди | уреди извор]

Фабрички методи енкапсулирају креирање објеката. Ово може бити корисно када је процес креирања објекта јако сложен (нпр. зависи од подешавања у конфигурационим датотекама или од корисничког уноса).

Размотрићемо пример програма који чита слике и од њих прави мање сличице. Програм подржава различите формате слика, представљене са по једном класом за читање сваког од формата:

public interface ImageReader {
    public DecodedImage getDecodedImage();
}
 
public class GifReader implements ImageReader {
    public DecodedImage getDecodedImage() {
        return decodedImage;
    }
}
 
public class JpegReader implements ImageReader {
    // ....
}

Сваки пут када програм прочита слику, мора да, на основу неких информација из датотеке, креира читача за одговарајући формат. Ова логика се може енкапсулирати у фабрички метод:

public class ImageReaderFactory {
    public static ImageReader getImageReader(InputStream is) {
        int imageType = determineImageType(is);

        switch(imageType) {
            case ImageReaderFactory.GIF:
                return new GifReader(is);
            case ImageReaderFactory.JPEG:
                return new JpegReader(is);
            // etc.
        }
    }
}

Исечак кôда из претходног примера користи наредбу switch да споји формат слике са одговорајућом фабриком објеката. Исто тако се могао користити и неки вид мапирања, при чему би се наредба switch заменила са асоцијативним низом чији су индекси формати слике, а чланови фабрике објеката.

Ограничења[уреди | уреди извор]

Постоје три ограничења везана за коришћење фабричког метода. Први је везан за рефакторисање постојећег кôда, а друга два за наслеђивање.

  • Прво ограничење је да рефакторисање постојеће класе, да би користила фабрике, нарушава постојеће клијенте те класе. На пример, да је класа Complex нека постојећа стандардна класа, имала би бројне клијенте који је користе на следећи начин:
Complex c = new Complex(-1, 0);
Онда када схватимо да нам требају две различите фабрике, изменићемо класу као у кôду приказаном изнад. Међутим, пошто су нам сада конструктори приватни, постојећи кôд више не може да се компајлира.
  • Друго ограничење је да, пошто се овај пројектни узорак ослања на коришћење приватних конструктора, од класе која садржи овај узорак не може да се изведе нова класа. Свака класа мора да позове наслеђени конструктор, али ово се не може извести пошто је конструктор приватан.
  • Треће ограничење је да, ако и наследимо класу (нпр. стављањем да је конструктор заштићен -- опасно, али могуће), свака поткласа мора да обезбеди сопствене имплементације свих фабричких метода са истоветним потписима тих метода. На пример, ако је класа StrangeComplex изведена из класе Complex, онда уколико класа StrangeComplex не обезбеди своје имплементације свих фабричких метода, позивом StrangeComplex.fromPolar(1, pi) ће се добити инстанца класе Complex (надкласе) уместо очекиване инстанце поткласе.

Сва три проблема се могу избећи када би се програмски језици тако изменили да фабрике постану првокласни чланови језика. [3]

Примери[уреди | уреди извор]

Јава[уреди | уреди извор]

abstract class Pizza {
    public abstract int getPrice(); // uzima cenu
}

class HamAndMushroomPizza extends Pizza {
    public int getPrice() {
        return 850;
    }
}

class DeluxePizza extends Pizza {
    public int getPrice() {
        return 1050;
    }
}

class HawaiianPizza extends Pizza {
    public int getPrice() {
        return 1150;
    }
}

class PizzaFactory {
    public enum PizzaType {
        HamMushroom,
        Deluxe,
        Hawaiian
    }

    public static Pizza createPizza(PizzaType pizzaType) {
        switch (pizzaType) {
            case HamMushroom:
                return new HamAndMushroomPizza();
            case Deluxe:
                return new DeluxePizza();
            case Hawaiian:
                return new HawaiianPizza();
        }
        throw new IllegalArgumentException("Tip pice " + pizzaType + " nije prepoznat.");
    }
}

class PizzaLover {
    /*
     * Pravi sve dostupne pice i ispisuje njihove cene
     */
    public static void main (String args[]) {
        for (PizzaFactory.PizzaType pizzaType : PizzaFactory.PizzaType.values()) {
            System.out.println("Cena pice " + pizzaType + " je " + PizzaFactory.createPizza(pizzaType).getPrice());
        }
    }
}

// Izlaz:
// Cena pice HamMushroom je 850
// Cena pice Deluxe je 1050
// Cena pice Hawaiian je 1150

C#[уреди | уреди извор]

public interface IPizza
{
    decimal Price { get; }
}

public class HamAndMushroomPizza : IPizza
{
    decimal IPizza.Price
    {
        get
        {
            return 8.5m;
        }
    }
}

public class DeluxePizza : IPizza
{
    decimal IPizza.Price
    {
        get
        {
            return 10.5m;
        }
    }
}

public class HawaiianPizza : IPizza
{
    decimal IPizza.Price
    {
        get
        {
            return 11.5m;
        }
    }
}

public class PizzaFactory
{
    public enum PizzaType
    {
        HamMushroom,
        Deluxe,
        Hawaiian
    }

    public static IPizza CreatePizza(PizzaType pizzaType)
    {
        IPizza ret = null;

        switch (pizzaType)
        {
            case PizzaType.HamMushroom:
                ret = new HamAndMushroomPizza();

                break;
            case PizzaType.Deluxe:
                ret = new DeluxePizza();

                break;
            case PizzaType.Hawaiian:
                ret = new HawaiianPizza();

                break;
            default:
                throw new ArgumentException("Tip pice " + pizzaType + " nije prepoznat.");
        }

        return ret;
    }
}

public class PizzaLover
{
    public static void Main(string[] args)
    {
        Dictionary<PizzaFactory.PizzaType, IPizza> pizzas = new Dictionary<PizzaFactory.PizzaType, IPizza>();
        
        foreach (PizzaFactory.PizzaType pizzaType in Enum.GetValues(typeof(PizzaFactory.PizzaType)))
        {
            pizzas.Add(pizzaType, PizzaFactory.CreatePizza(pizzaType));
        }
        
        foreach (PizzaFactory.PizzaType pizzaType in pizzas.Keys)
        {
            System.Console.WriteLine("Cena pice {0} je {1}", pizzaType, pizzas[pizzaType].Price);
        }
    }
}

// Izlaz:
// Cena pice HamMushroom je 8.5
// Cena pice Deluxe je 10.5
// Cena pice Hawaiian je 11.5

Пајтон[уреди | уреди извор]

#
# Pica
#
class Pizza:
    def __init__(self):
        self.price = None
    
    def get_price(self):
        return self.price
        
class HamAndMushroomPizza(Pizza):
    def __init__(self):
        self.price = 8.5
    
class DeluxePizza(Pizza):
    def __init__(self):
        self.price = 10.5
  
class HawaiianPizza(Pizza):
    def __init__(self):
        self.price = 11.5
#
# Fabrika pica
#
class PizzaFactory:
    @staticmethod
    def create_pizza(pizza_type):
        if pizza_type == 'HamMushroom':
            return HamAndMushroomPizza()
        elif pizza_type == 'Deluxe':
            return DeluxePizza()
        elif pizza_type == 'Hawaiian':
            return HawaiianPizza()
 
if __name__ == '__main__':
    for pizza_type in ('HamMushroom', 'Deluxe', 'Hawaiian'):
        print 'Cena pice {0} je {1}'.format(pizza_type, PizzaFactory.create_pizza(pizza_type).get_price())

PHP[уреди | уреди извор]

<?php

abstract class Pizza
{
    protected $_price;
    public function getPrice()
    {
        return $this->_price;
    }
}
 
class HamAndMushroomPizza extends Pizza
{
    protected $_price = 8.5;
}
 
class DeluxePizza extends Pizza
{
    protected $_price = 10.5;
}
 
class HawaiianPizza extends Pizza
{
    protected $_price = 11.5;
}
 
class PizzaFactory
{
    public static function createPizza($type)
    {
        $baseClass = 'Pizza';
        $targetClass = ucfirst($type).$baseClass;
        
        if (class_exists($targetClass) && is_subclass_of($targetClass, $baseClass))
            return new $targetClass;
        else
            throw new Exception("Tip pice nije prepoznat.");
    }
}

$pizzas = array('HamAndMushroom','Deluxe','Hawaiian');
foreach($pizzas as $p) {
    printf(
        "Cena pice %s je %01.2f".PHP_EOL ,
        $p ,
        PizzaFactory::createPizza($p)->getPrice()
    );
}

// Izlaz:
// Cena pice HamMushroom je 8.50
// Cena pice Deluxe je 10.50
// Cena pice Hawaiian je 11.50

?>

Коришћења[уреди | уреди извор]

  • У ADO.NET компонентама, IDbCommand.CreateParameter(језик: енглески) је пример коришћења фабричког метода који повезује паралелне хијерархије класа.
  • У Qt апликативном интерфејсу, QMainWindow::createPopupMenu(језик: енглески) је фабричка метода декларисана у радном оквиру која се може преклопити у клијентском кôду.
  • У Јави, у пакету javax.xml.parsers Архивирано на сајту Wayback Machine (6. август 2009)(језик: енглески) се користи неколико фабрика, нпр. javax.xml.parsers.DocumentBuilderFactory или javax.xml.parsers.SAXParserFactory.

Види још[уреди | уреди извор]

Референце[уреди | уреди извор]

  1. ^ Gang Of Four
  2. ^ Мајкл Федерс (Октобар 2004). Working Effectively with Legacy Code. ISBN 978-0131177055.  Проверите вредност парамет(а)ра за датум: |date= (помоћ)
  3. ^ Agerbo, Aino; Agerbo, Cornils (1998). „How to preserve the benefits of design patterns”. Conference on Object Oriented Programming Systems Languages and Applications. Vancouver, British Columbia, Canada: ACM: 134—143. ISBN 978-1-58113-005-8.  Непознати параметар |fist= игнорисан (помоћ)

Литература[уреди | уреди извор]

  • Федерс, Мајкл (Октобар 2004). Working Effectively with Legacy Code. ISBN 978-0131177055.  Проверите вредност парамет(а)ра за датум: |date= (помоћ)