Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do naszej społeczności na Discordzie!
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.
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.
Dzięki metodzie wytwórczej możemy wygodnie tworzyć obiekty o wspólnych cechach. Na myśl przychodzi szereg zastosowań, m.in.:
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.
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!
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.
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).
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!
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ę!
Udostępnij ten artykuł:
Potrzebujesz cotygodniowej dawki motywacji?
Zapisz się i zgarnij za darmo e-book o wartości 39 zł!
PS. Zazwyczaj rozsyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu email.
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.