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:

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

Sprawdź ten wpis

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

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.