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:
- frameworki UI, m.in. Material UI, Bootstrap,
- biblioteki programistyczne, np. Redux, React-dnd.
- 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
- Aleksander Shvets, Wzorce projektowe. Nowoczesny podręcznik, Refactoring.Guru, 2022
- Dokumentacja UML w wersji 2.4
- Factory Functions - Programowanie funkcyjne w JavaScript
- https://github.com/mateuszjansowa/article-patterns-js-factory-method

