Wzorzec metoda wytwórcza (factory metod)

Jeżeli masz do czynienia z tworzeniem różnych obiektów bazujących na predefiniowanym modelu, to powinieneś/aś skorzystać z metody wytwórczej. Kreacyjny wzorzec projektowy factory method pozwala być fancy w tworzeniu obiektów.


Metoda wytwórcza, co to?

Metoda wytwórcza jest kreacyjnym wzorcem projektowym służącym do zwinnego wytwarzania obiektów. Po co nam taki wzorzec? Chodzi pogłoska, że w programowaniu istnieją dwie trudne rzeczy: nazywanie zmiennych i obsługa cache'u. Dodałbym do tego zestawu jeszcze jedną pozycję, mianowicie tworzenie obiektów. Proces skomplikowany nie jest, raczej nie ma filozofii w użyciu operatora new i wskazaniu klasy na podstawie której ma powstać obiekt. Problem pojawia się, gdy tworzenie klas jest rozsiane po całym systemie, a w pewnym momencie okazuje się, że dochodzi nowy podtyp tworzonego obiektu. 

Przykład metody wytwórczej

Na ratunek... factory method! Chyba każdy z nas spotkał się kiedyś z obsługą różnych typów sprowadzających się do tego samego interfejsu. Przykładem może być tutaj użytkownik prywatny i firma. Ten pierwszy identyfikuje się numerem PESEL, zaś drugi NIPem. Koniec końców mają one jednak większość tożsamych zachowań, a abstrakcje systemu nawet nie rozróżniają ich typów. Spójrzmy poniżej, gdzie widzimy serwis przyjmujący dane do wystawienia faktury oraz dane kontrahenta. 

export interface Contractor {
  getBillingIdentifier(): string;
  getBillingName(): string;
}

export interface ContractorFactoryInterface {
  createFrom(dto: ContractorDto): Contractor;
}

export class InvoicesService {
  constructor(
    private readonly invoiceRepository: InvoiceRepository,
    private readonly contractorFactory: ContractorFactoryInterface,
  ) {}

  issue(invoiceDto: IncoiceDto, contractorDto: ContractorDto) {
    const contractor = this.contractorFactory.createFrom(contractorDto);

    const invoice = Invoice.create({
       ...invoiceDto,
       contractorName: contractor.getBillingName(),
       contractorBillingIdentifier: contractor.getBillingIdentifier()
    });

    this.invoiceRepository.save(contractor);
  }
}
Używa on fabryki kontrahentów, która na podstawie przekazanych danych o kontrahencie tworzy jego odpowiedni typ. Dzięki temu zabiegowi serwis nie musi zastanawiać się czy ma do czynienia z firmą czy osobą prywatną, zwyczajnie go to nie obchodzi, ponieważ otrzymuje od fabryki spójny interfejs na którym może oprzeć swoją logikę.

export class Company implements Contractor {
  constructor(
    private readonly companyName: string,
    private readonly vatId: string
  ) {}

  getBillingIdentifier = (): string => this.vatId;

  getBillingName = (): string => this.companyName;
}

export class Person implements Contractor {
  constructor(
    private readonly firstName: string,
    private readonly lastName: string,
    private readonly PESEL: string
  ) {}

  getBillingIdentifier = (): string => this.PESEL;

  getBillingName = (): string => `${this.firstName} ${this.lastName}`;
}

export class ContractorFactory implements ContractorFactoryInterface {
  createFrom(dto: ContractorDto): Contractor {
    if (dto.vatId) {
      return new Company(dto.companyName, dto.vatId);
    }

    if (dto.PESEL) {
      return new Person(dto.firstName, dto.lastName, dto.PESEL);
    }

    throw new RuntimeException('Incorrect contractor data.');
  }
}
Jeżeli boli nas ifologia w metodzie createFrom() możemy wdrożyć wzorzec strategii (o którym niebawem powiem) i nie martwić się o to, że w przyszłości dojdzie jakiś nowy typ użytkownika i będziemy musieli dopisać kolejnego ifa.

Mapper vs. Factory method

Jakiś czas temu, podczas omawiania koncepcji mappera wspomniałem, że powiem czym różni się on od metody wytwórczej. Granica między tymi podejściami jest bardzo cienka i umowna, bo zależy to w głównej mierze od technologii w jakiej pracujemy. Myślę, że z powodzeniem uda nam się znaleźć przykłady kodu podobnego do tego powyżej, ale ochrzczonego mianem mappera. Mapper jest jednak bardziej narzędziem służącym do zamiany jednego typu do drugiego (translacją?), podczas gdy fabryka skupia się na samych aspektach kreacji. Inaczej mówiąc, mapper zamienia jedną reprezentację obiektu w inną, a fabryka tworzy sam obiekt na podstawie dostarczonych danych. Dodatkowo, mapper może używać fabryk, bo przecież zamiana typu może wymagać utworzenia innych obiektów.

Pozostając więc przy powyższym przykładzie, doróbmy tematyczny mapper. Powiedzmy, że dane kontrahenta chcemy zapisać u zewnętrznego dostawcy, ale wymaga on na pozór innych danych. Użyjemy w tym celu mappera, który zaadaptuje obiekt kontrahenta do tego, wymaganego przez wspomniany system.

export type ExternalContractor {
  name: string;
  identifier: string;
}

export class ExternalContractorMapper {
  mapContractor(contractor: Contractor): ExternalContractor {
    return {
      name: contractor.getBillingName(),
      identifier: contractor.getBillingIdentifier()
    };
  }

  mapUser(user: User): ExternalContractor { ... }
}

Podsumowanie




Polecane wpisy:

Wzorzec adapter

Strukturalny wzorzec projektowy adapter to spore udogodnienie w walce z systemami legacy. Pozwala użyć niepasującego interfejsu w innym. Dzięki temu zabiegowi pomimo, iż nie posiadamy spójnej abstrakcji możemy zastosować pożądaną logikę.

Sprawdź ten wpis

Wzorzec dekorator

Strukturalny wzorzec projektowy dekorator, to lekarstwo na problemy z mnogością różnych podtypów. Pozwala on na dynamiczne generowanie kolejnych typów obiektów bazujących na pierwotnym typie, bez konieczności deklarowania nowych, dedykowanych klas.

Sprawdź ten wpis

Wzorzec budowniczy (builder)

Wzorzec projektowy budowniczy służy do dynamicznej budowy obiektów. Pozwala na kreowanie różnych typów obiektu w zależności od przesłanych wymagań. Dzięki temu posiadamy centralny punkt odpowiedzialny jedynie za tworzenie wariacji danego typu.

Sprawdź ten wpis

SOLID z przykładami w TypeScript

SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.

Sprawdź ten wpis

Jak zostać architektem oprogramowania?

Aby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.