Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

🔥 Zgarnij PŁATNY STAŻ w 3 edycji kursu programowania front end (zapisy do 22.01) 🔥

Wzorzec projektowy Factory Method (Metoda wytwórcza, Fabryka)

Zobacz przykłady i wykorzystaj OOP w projekcie JavaScript

Dzięki metodzie wytwórczej poznasz prosty i bezpieczny sposób na tworzenie obiektów, a następnie całych szkieletów do budowy frameworków! Korzystaj z tych samych metod w różnych obiektach bez martwienia się o szczegóły implementacji.

Spis treści

Metoda wytwórcza (ang. Factory Method) to kreacyjny wzorzec projektowy. Dzięki niej możemy w wygodny sposób tworzyć obiekty o wspólnych cechach z pominięciem jawnego używania słowa kluczowego new. Za pomocą interfejsu określamy wspólne pola i metody. Następnie implementujemy go i dzięki klasie bazowej tworzymy „potomków”, czyli obiekty o wspólnych polach i metodach.

Brzmi skomplikowanie? Wcale nie musi takie być! JavaScript nie powstał jako język obiektowy, co oznacza, że możemy tworzyć nowe obiekty jako rezultat działania funkcji. Dlatego w JS metoda wytwórcza jest dużo prostsza do zastosowania niż w innych językach. Nosi nawet zmienioną nazwę – funkcja wytwórcza. Poznamy ją, a gdy poczujemy się pewnie w jej użytkowaniu, przejdziemy do metody wytwórczej.

 Zastosowanie wzorca projektowego Factory Method

Dzięki metodzie wytwórczej możemy wygodnie tworzyć obiekty o wspólnych cechach. Na myśl przychodzi szereg zastosowań, m.in.:

  • Budowanie kolekcji elementów, na których planujemy podobne działania:
  • Tworzenie obiektów, które przynależą do różnych grup, ale mają wspólne pola lub metody:
    • e-commerce, gdzie możemy sprzedawać i dostarczać np. usługę, subskrypcję lub konkretną rzecz,
    • gry, w których postacie czy przedmioty mają podobną logikę działania (np. szachy).

Wyobraź sobie, że tworzymy webową grę RPG. Występują w niej różne postacie, w tym kontrolowane przez gracza i przez komputer. Mimo że będą miały odmienną logikę działania, to jednak niektóre z ich cech będą wspólne. Co więcej, nie jesteśmy jeszcze pewni, o jakie funkcje rozszerzymy konkretne klasy postaci. Dobrym pomysłem będzie zastosowanie funkcji wytwórczej, by stworzyć dwie pierwsze postacie w naszej grze. Spójrz na poniższy przykład.

// CharacterFactory to funkcja wytwórcza
import { characterFactory } from '../factory-function/characterFactory.js';

const humanCharacter = characterFactory({
    type: 'human',
    level: 0,
    money: 0,
    health: 100
});

const shopNPC = characterFactory({
    type: 'shop',
    money: 100,
    health: null
});

// dzięki wspólnym cechom obiektów możemy je grupować
const characters = [humanCharacter, shopNPC];

// możemy użyć tej samej metody
humanCharacter.renderOnMap({
    x: 0,
    y: 0
});

// możemy użyć tej samej metody
shopNPC.renderOnMap({
    x: 10,
    y: 10
});

// możemy wykorzystać fakt, że metody są współdzielone
for (const character of characters) {
    character.renderOnMap({
        x: Math.random() * 10,
        y: Math.random() * 10
    });
}

Powyższy przykład pokazuje największe korzyści wynikające z użycia funkcji wytwórczej. Na początku importujemy naszą funkcję wytwórczą. Ta funkcja zwraca dwie nowe postacie (humanCharacter oraz shopNPC). Dzięki zastosowaniu wzorca otrzymaliśmy obiekty postaci o wspólnej metodzie nazwanej renderOnMap. Nie musimy zagłębiać się w szczegóły implementacji postaci, by pokazać je na mapie. Wiemy, że dzielą tę samą metodę, i możemy bezpiecznie jej użyć. Bardzo wygodne!

Przejdźmy do szczegółów implementacji funkcji wytwórczej.

 Funkcja wytwórcza

Aby stworzyć funkcję wytwórczą wystarczy nam… funkcja zwracająca obiekt. Stwórzmy ją.

export const characterFactory = characterObject => ({
    ...characterObject,
    location: { x: 0, y: 0 },
    renderOnMap: function (location) {
        this.location = location;
        console.log(this);
    }
});

Funkcja characterFactory przyjmuje tylko jeden parametr – obiekt, na podstawie którego stworzy postać w grze. Dzięki użyciu funkcji strzałkowej mogę pominąć słowo kluczowe return (tzw. implicit return) i od razu zwrócić obiekt postaci.

Za pomocą destrukturyzacji (...characterObject) do nowo tworzonego obiektu przypiszę właściwości i metody obiektu, który przekazuję w parametrze funkcji. Dodam nowe pole location oraz metodę renderOnMap. Od tego momentu wszystkie obiekty stworzone na podstawie funkcji wytwórczej będą dzielić nazwy elementów (funkcji i zmiennych). Zauważ, że tworząc kolejne obiekty, tworzysz również kolejne funkcje. Może mieć to wpływ na wydajność programu, jeśli obiektów są setki czy tysiące.

Wydajność tworzenia nowych obiektów znacznie różni się między funkcją wytwórczą a metodą wytwórczą (o wydajności piszę trochę niżej).

Działający kod z przykładu możesz podejrzeć w tym repozytorium.

Przejdźmy do metody!

 Metoda wytwórcza

JavaScript to język, który daje dużą swobodę programiście. Napisaliśmy działający kod funkcyjny, który tworzy postać w grze. Teraz spróbujmy zrobić to samo, ale korzystając z programowania obiektowego (OOP). Do dzieła!

Na samym początku napiszmy interfejs Character, który będzie definiował wspólne pola (location i renderOnMap) dla wszystkich obiektów stworzonych za pomocą metody wytwórczej. O tym, że w JS interfejs to nazwa umowna, przeczytasz we fragmencie artykułu „4 filary programowania obiektowego”.

export class CharacterInterface {
    constructor() {
        this.location = { x: 0, y: 0 };
    }

    renderOnMap(location) {
        throw new Error('renderOnMap not implemented');
    }
}

Teraz możemy stworzyć dwie klasy: Player oraz NPC, które będą implementować ten sam interfejs.

import { CharacterInterface } from '../interfaces/CharacterInterface.js';

export class Player extends CharacterInterface {
    renderOnMap(location) {
        this.location = location;
        console.log(this);
    }

    // inne metody charakterystyczne dla gracza
    move(direction) {
        // ...
    }
}
import { CharacterInterface } from '../interfaces/CharacterInterface.js';

export class NPC extends CharacterInterface {
    renderOnMap(location) {
        this.location = location;
        console.log(this);
    }

    // inne metody charakterystyczne dla NPC
    sell(item) {
        // ...
    }
}

Teraz napiszemy kod Twórcy, czyli klasy, która posiada metodę wytwórczą. Zobacz przykład poniżej.

import { NPC } from './NPC.js';
import { Player } from './Player.js';

export class CharacterCreator {
    create(characterType) {
        switch (characterType) {
            case 'player':
                return new Player();
            case 'npc':
                return new NPC();
            default:
                throw new Error('Unknown character type');
        }
    }
}

Poskładajmy wszystko w spójną całość i stwórzmy dwie nowe postacie.

import { CharacterCreator } from './classes/CharacterCreator.js';

// inicjuję klasę-Twórcę
const creator = new CharacterCreator();

// korzystam z metody wytwórczej
const humanCharacter = creator.create('player');

// korzystam z metody wytwórczej
const shopNPC = creator.create('npc');

// używam wspólnej metody
shopNPC.renderOnMap({
    x: 10,
    y: 10
});

// używam wspólnej metody
humanCharacter.renderOnMap({
    x: 0,
    y: 0
});

Na samym początku importuję i inicjuję klasę-Twórcę, czyli CharacterCreator. Następnie korzystam z metody wytwórczej create, dzięki której otrzymuję dwie nowe postacie: humanPlayer i shopNPC. Korzystają one z tego samego interfejsu Character, co pozwala mi korzystać bez żadnych obaw ze wspólnej metody renderOnMap.

Kod z przykładu znajdziesz w tym repozytorium.

 Wydajność w funkcji wytwórczej i metodzie wytwórczej

Z poprzedniego artykułu na temat OOP wiesz, że klasy w JS to nic innego jak lukier składniowy na funkcje-konstruktory. Nowo utworzone metody w klasie przenoszone są niejawnie do prototypu danego obiektu. Zatem metoda w klasie zajmuje tylko „jedno” miejsce w pamięci. Dzięki temu możemy mieć setki obiektów korzystających z tej samej metody. Takie zachowanie obserwujemy w metodzie wytwórczej.

Sytuacja ma się odwrotnie w funkcji wytwórczej. Metody zadeklarowane są bezpośrednio w obiekcie (a nie prototypie). Oznacza to, że za każdym razem, gdy tworzymy nowy obiekt za pomocą funkcji wytwórczej, tworzymy kolejne instancje metod, które zajmują miejsce w pamięci. Nie jest to rozwiązanie optymalne. Warto mieć to na uwadze podczas tworzenia aplikacji, które wykorzystują dużo obiektów i muszą działać szybko (np. gry webowe).

 Diagram UML

Gdy poznałeś już funkcję oraz metodę wytwórczą, zaprezentuję Ci ich diagram UML. Stosując go, możesz w prosty sposób zaimplementować własną wersję wzorca!

Diagram UML dla wzorca metody wytwórczej (Factory Method)

 Podsumowanie

Właśnie poznałeś prosty i bezpieczny sposób na tworzenie kolekcji obiektów, które dzielą pola i metody. Poznanie funkcji wytwórczej pozwoli Ci tworzyć kod jeszcze sprawniej i czytelniej. Warto stosować ten wzorzec szczególnie w początkowej fazie projektu, gdy tworzysz dużo nowych funkcjonalności o wspólnych cechach. Pomyśl tylko, jak łatwo rozbudujesz swoje obiekty o dodatkową logikę!

 Źródła

Udostępnij ten artykuł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.

Mam coś dla Ciebie!

W każdy piątek rozsyłam motywujący do nauki programowania newsletter!

Dodatkowo od razu otrzymasz ode mnie e-book o wartości 39 zł. To ponad 40 stron konkretów o nauce programowania i pracy w IT.

PS Zazwyczaj wysyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu e-mail.