Wzorzec projektowy fabryka - okładka

Wzorzec projektowy Factory (Fabryka)

Opublikowano Kategorie Czysty kodCzas czytania 8min

Fabryka jest powszechnie używanym i łatwym w zastosowaniu wzorcem projektowym. Mówiąc o wzorcu projektowym Fabryka, można wyróżnić cztery rodzaje fabryk:

  • Factory (fabryka);
  • Factory Method (metoda wytwórcza);
  • Static Factory (fabryka statyczna)
  • Abstract Factory (fabryka abstrakcyjna).

Głównym założeniem fabryki, tak jak w rzeczywistym świecie, jest wytwarzanie obiektów. Dzięki wykorzystaniu fabryki można ukryć szczegóły implementacyjne tworzenia obiektów i odseparować je od logiki biznesowej.

Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce, to koniecznie sprawdź mój wpis o wzorcach projektowych. We wpisach z tego cyklu w przykładach wykorzystuję TypeScript. Jeżeli miałeś/aś jakąkolwiek styczność z językami silnie typowanymi, to zrozumienie przykładów nie powinno stanowić dla Ciebie problemu.

Factory (Fabryka)

Aby lepiej zobrazować praktyczne zastosowanie fabryki, nakreślę przykładowy problem do rozwiązania. Firma, dla której tworzymy oprogramowanie, posiada system sprzedażowy i potrzebuje generować faktury dla klientów. Ponieważ klienci pochodzą z różnych części świata, faktury muszą być dostosowane do kraju pochodzenia klienta. Przykładowymi dostosowaniami są: format daty, kierunek ułożenia tekstu (od lewej do prawej lub od prawej do lewej) oraz waluta.

Potencjalnie tworzy to wiele różnych możliwości i jeśli chcielibyśmy obsłużyć wszystkie możliwe kombinacje jedną klasą lub metodą, to kod stałby się jedną wielką ifologią. Innymi słowy, kod byłby zupełnie nieczytelny, nie wspominając nawet o możliwościach jego rozbudowy.

Krokiem naprzód w designie omawianego rozwiązania może być stworzenie prostej fabryki, gdzie faktura dla każdego obsługiwanego kraju będzie generowana w osobnej prywatnej metodzie. Do generowania faktury zostanie udostępniona w naszym przypadku jedynie jedna publiczna metoda pozwalająca na zdefiniowanie, dla jakiego kraju faktura ma zostać wygenerowana. Prostą implementację omawianej fabryki przestawia poniższy przykład. Pominąłem implementacje klas i interfejsów nieistotnych z punktu widzenia omawianego wzorca.


class InvoiceFactory {
    public createInvoice( params: InvoiceParams ): IInvoice {
        switch( params.country ) {
            case 'PL':
                return this._createInvoicePL( params );
            case 'USA':
                return this._createInvoiceUSA( params );
            case 'SAU':
                return this._createInvoiceSAU( params );
            default:
                throw new Error( 'Unsupported country!' );
        }
    }

    private _createInvoicePL( params: InvoiceParams ): InvoicePL {
        return new InvoicePL( params );
    }

    private _createInvoiceUSA( params: InvoiceParams ): InvoiceUSA {
        return new InvoiceUSA( params );
    }

    private _createInvoiceSAU( params: InvoiceParams ): InvoiceSAU {
        return new InvoiceSAU( params );
    }
}

Mając już gotową fabrykę, jedyne co trzeba zrobić to stworzyć instancję klasy InvoiceFactory i wywołać metodę createInvoice w celu wygenerowania faktury. Oczekiwanym rezultatem wywołania metody createInvoice jest zwrócenie obiektu spełniającego interfejs IInvoice. Dzięki wykorzystaniu fabryki, z poziomu logiki biznesowej detale dotyczące generowania faktur są niewidoczne. Co więcej, dzięki odseparowaniu od siebie poszczególnych wariantów faktur w kodzie fabryki, logika generowania faktur została nieco uproszczona. Tworzenie nowej faktury z użyciem nowo powstałej fabryki przedstawia kod poniżej.


const factory: InvoiceFactory = new InvoiceFactory()

const invoice: IInvoice = factory.createInvoice( { country: 'PL', ...params } );

Obecna implementacja pozwala na wygenerowanie faktur dla klientów z Polski, Stanów Zjednoczonych oraz Arabii Saudyjskiej. W celu dodania wsparcia dla kolejnych krajów należałoby rozszerzyć fabrykę o kolejne metody oraz warunek w metodzie createInvoice.

Factory method (metoda wytwórcza)

Prosta fabryka przedstawiona we wcześniejszej części wpisu ma pewien zasadniczy problem. Poza generowaniem elementów faktur, specyficznych dla kraju pochodzenia klienta, istnieje część faktury, która będzie identyczna niezależnie od kraju pochodzenia klienta. W celu dalszego uproszczenia kodu wykorzystany zostanie wzorzec projektowy metody wytwórczej. Dzięki metodzie wytwórczej możliwe jest zdefiniowanie części wspólnej dla generowanej faktury oraz części specyficznych dla konkretnych podklas.


abstract class Invoice {
    protected abstract format( invoice: IInvoice ): IInvoice;
    
    public create(): IInvoice {
        return this.format( this._generateInvoice() );
    }
}

class InvoicePL extends Invoice {
    protected format( invoice: IInvoice ): IInvoice {
        invoice.direction = 'ltr';
        invoice.currency = 'PLN';
        invoice.dateFormat = 'DD/MM/YYYY';

        return invoice;
    }
}

class InvoiceFactory {
    public createInvoice( params: InvoiceParams ): IInvoice {
        switch( params.country ) {
            case 'PL':
                return this._createInvoicePL( params );
            case 'USA':
                return this._createInvoiceUSA( params );
            case 'SAU':
                return this._createInvoiceSAU( params );
            default:
                throw new Error( 'Unsupported country!' );
        }
    }

    private _createInvoicePL( params: InvoiceParams ): InvoicePL {
        const invoice: InvoicePL = new InvoicePL( params );

        return invoice.create();
    }

    private _createInvoiceUSA( params: InvoiceParams ): InvoiceUSA {
        const invoice: InvoiceUSA = new InvoiceUSA( params );

        return invoice.create();
    }

    private _createInvoiceSAU( params: InvoiceParams ): InvoiceSAU {
        const invoice: InvoiceSAU = new InvoiceSAU( params );

        return invoice.create();
    }
}

Część wspólna procesu generowania faktury dla każdego kraju została zamknięta w prywatnej metodzie _generateInvoice klasy abstrakcyjnej Invoice, natomiast części specyficzne dla poszczególnych krajów są definiowane w chronionej metodzie format, która jest implementowana w klasach potomnych, czyli w naszym wypadku klasach faktur dla poszczególnych krajów. Powyższy kod de facto łączy ze sobą wzorce fabryka oraz metoda wytwórcza. Decydując się na złączenie dwóch wzorców w jednym fragmencie kodu, chciałem pokazać, że dzięki umiejętnemu wykorzystaniu oraz łączeniu wzorców projektowych można uzyskać czysty i czytelny kod.

Ponadto, kod ten jest czytelny, pozbawiony duplikatów oraz będzie jeszcze łatwiejszy w rozbudowie. W przypadku potrzeby dodania kolejnego kraju do listy obsługiwanych krajów wystarczy zdefiniować klasę z implementacją metody format oraz dodać ją do fabryki. Dodatkowy benefit zastosowania metody wytwórczej to, że jeśli programista, który pracuje z przedstawioną fabryką, pierwszy raz dostanie zadanie dodania do listy nowego kraju, to nie musi on się wgryzać w logikę tworzenia faktury. Wystarczy, że zaimplementuje metodę format w nowej klasie oraz doda jej obsługę do fabryki.

Fabryka - obrazek dekoracyjny

Static Factory (Fabryka Statyczna)

Mając już kod fabryki odpowiedzialnej za generowanie faktur, pojawił się pewien problem. Mianowicie, w systemie istnieje kilka instancji fabryk i zaszła sytuacja, że kilku klientów otrzymało faktury z tymi samymi numerami. W celu uniknięcia duplikatów generowanych faktur można wykorzystać wzorzec fabryki statycznej. Fabryka statyczna może przechowywać listę uprzednio wygenerowanych faktur i jeśli w systemie została już wygenerowana faktura o podanym numerze, to fabryka może na przykład rzucić błędem lub poprosić o inny numer faktury. Taka fabryka staje się jednocześnie Singletonem. Przykładowa implementacja fabryki statycznej, w omawianym przeze mnie przypadku mogłaby wyglądać następująco.


class InvoiceFactory {
    private static generatedInvoices: Set = new Set();

    public static createInvoice( params: InvoiceParams ): IInvoice {
        if ( InvoiceFactory.generatedInvoices.has( params.invoiceId ) ) {
            throw new Error( 'invoiceId already used!' );
        }

        switch( params.country ) {
            case 'PL':
                return this._createInvoicePL( params );
            case 'USA':
                return this._createInvoiceUSA( params );
            case 'SAU':
                return this._createInvoiceSAU( params );
            default:
                throw new Error( 'Unsupported country!' );
        }
    }

    private _createInvoicePL( params: InvoiceParams ): InvoicePL {
        const invoice: InvoicePL = new InvoicePL( params );

        return invoice.create();
    }

    private _createInvoiceUSA( params: InvoiceParams ): InvoiceUSA {
        const invoice: InvoiceUSA = new InvoiceUSA( params );

        return invoice.create();
    }

    private _createInvoiceSAU( params: InvoiceParams ): InvoiceSAU {
        const invoice: InvoiceSAU = new InvoiceSAU( params );

        return invoice.create();
    }
}

const invoice: IInvoice = InvoiceFactory.createInvoice( { country: 'PL', ...params } );

Posiadanie listy wygenerowanych faktur w fabryce pozwala w prosty sposób uniknąć duplikatów. Alternatywą dla tego rozwiązania byłoby powołanie do życia obiektu odpowiedzialnego za kontrolę numerów faktur i wstrzyknięcie go do fabryk. Każda instancja fabryki, chcąc wygenerować fakturę, musiałaby wykonać zapytanie do Singletona.

Statyczne fabryki są równie przydatne w przypadku różnego rodzaju klas z helperami. W takim wypadku nie ma większego sensu tworzenia instancji klasy przy każdym wykorzystaniu. Zdecydowanie prościej jest wykorzystać metodę statyczną np. HelperClass.doX( params ).

Abstract Factorty (Fabryka Abstrakcyjna)

Przedstawiony system generowania faktur wdrożono na serwer produkcyjny i okazało się, że działa tak dobrze, że postanowiono go rozbudować. Oprócz generowania faktur konieczne będzie generowanie raportów sprzedaży dla poszczególnych klientów. Do poradzenia sobie z tym zadaniem można wykorzystać fabrykę abstrakcyjną. W fabryce abstrakcyjnej każda konkretna fabryka produkuje rodziny powiązanych obiektów.


class Invoice implements IDocument {}

class Report implements IDocument {}

interface IAbstractFactory {
  generate(): IDocument;
}

class InvoicesFactory implements IAbstractFactory {
  generate(): IDocument {
    return new Invoice();
  }
}

class ReportsFactory implements IAbstractFactory {
  generate(): IDocument {
    return new Report();
  }
}

class PrintingService {
  public constructor( private readonly _documentFactory: IAbstractFactory ) {}
  public generate(): IDocument {
    return this._sendToPrinter( this._documentFactory.generate() );
  }
}

const invoicePrintingService = new PrintingService( new InvoicesFactory() );
const reportPrintingService = new PrintingService( new ReportsFactory() );

invoicePrintingService.generate();
reportPrintingService.generate();

W tym miejscu należy zaznaczyć, że tym przypadku zwracane z fabryk InvoiceFactory oraz ReportFactory obiekty muszą implementować wspólny interfejs IDocument. PrintingService służy do drukowania dokumentów i oczekuje przekazania konkretnej fabryki. Fabryki abstrakcyjne znajdują swoje zastosowanie, gdy w systemie istnieje kilka fabryk zwracających obiekty wywodzące się z „jednej rodziny”. W naszym przypadku są to fabryki zwracające różne rodzaje dokumentów.

Podsumowanie

Mam nadzieję, że przedstawione przeze mnie przykłady przedstawiły Ci w jasny i przejrzysty sposób, czym są fabryki oraz jak i kiedy je stosować. Zachęcam do wypróbowania fabryk w praktyce, pozostawienia komentarza i zapoznania się z materiałami dodatkowymi.

Źródła i materiały dodatkowe:

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

0 komentarzy
Inline Feedbacks
View all comments