Wzorzec metoda wytwórcza (factory method)

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 NIP-em. 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 mapper-a 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 mapper-a. 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 mapper-a, 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

  • Wzorzec metody wytwórczej pozwala zapanować nad chaosem związanym z kreacją obiektów.
  • Stosując factory method posiadamy jedno miejsce w systemie, które wie jak stworzyć konkretny obiekt.
  • Dzięki temu patternowi niwelujemy rozsiany coupling.
  • Fabryki mogą być z powodzeniem używane w mapperach.
Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również