Metody statyczne w klasach JavaScript – zalety i zagrożenia

Wraz z pojawieniem się ECMAScript 6 programiści JavaScript dostali nowy element składniowy w postaci klas. W poprzednim artykule omawiałem jak „pod spodem” działa tworzenie klas, ale pominąłem kilka elementów, w tym m.in. możliwość tworzenia w klasach metod statycznych. Zobaczmy jednak czy na pewno słowo „static” w JavaScript jest dobrym rozwiązaniem…

Na wstępie wyjaśnijmy czym właściwie jest tzw. metoda statycza. Otóż jest to metoda, która nie jest wywoływana w kontekście żadnej konkretnej instancji obiektu danej klasy. W praktyce oznacza to po prostu, że nie trzeba tworzyć obiektu, aby móc skorzystać z metod statycznych.

Aby lepiej to zrozumieć zobaczmy poniższy przykład:

class MathOperation {
    static sum (...numbers) {
        return [...numbers].reduce((a,b) => a + b, 0);
    }
}

MathOperation.sum(3,4,5); //12

Widać więc, że użycie słowa static pozwoliło nam użyć metody sum() bez konieczności wcześniejszego tworzenia obiektu klasy MathOperation z użyciem słowa new.

Teoretycznie w pierwszej chwili mogłoby się wydawać, że daje nam to duże możliwości tworzenia klas uniwersalnych. Można by w ten sposób stworzyć klasę zawierającą skomplikowane metody obliczeniowe, np. do wykonywania analiz statystycznych, bez konieczności tworzenia jakichkolwiek nowych obiektów. Weźmy chociażby naszą metodę sum() – pobiera ona jako argumenty kolejne liczby do zsumowania i w zasadzie tworzenie jakiegoś obiektu w stylu new MathOperation() nie ma większego sensu… Czy jednak na pewno?

Rozbudujmy nieco naszą klasę MathOperation…

Dodajmy do naszej klasy jeszcze dwie metody, jedną służącą do pomnożenia wszystkich liczb i drugą, zwracającą sumę tylko liczb parzystych (liczby nieparzyste są ignorowane w sumowaniu):

class MathOperation {
    static add (...numbers) {
        return [...numbers].reduce((a,b) => a + b,0);
    }
    
    static product (...numbers) {
        return [...numbers].reduce((a,b) => a * b, 1);
    }
    sumEven (...numbers) {
        return [...numbers].filter(n => ~n&1).reduce((a,b) => a + b,0);
    }
}

Uważny czytelnik, który stosował już składnię static szybko dostrzeże jeden, poważny błąd – przed deklaracją metody sumEven pominięto słowo static. Czy zrobiono to celowo czy przypadkowo to nie ma znaczenia… zastanówmy się, jaki ma to wpływ na działanie naszej klasy:

MathOperation.sum(3,4,5); //12
MathOperation.product(2,5); //10
MathOperation.sumEven(2,5,5,4); //TypeError

Ups… co się tutaj stało? Otóż metoda sumEven nie jest metodą statyczną, zatem wymaga wcześniejszego stworzenia instancji klasy MathOperation, czego my oczywiście nie zrobiliśmy. W związku z tym otrzymujemy błąd TypeError gdyż metoda sumEven nie jest poprawną funkcją.

Stwierdziliśmy jednak, że mimo wszystko potrzebujemy do pracy wszystkich trzech metod, w związku z czym postanowiliśmy stworzyć instancję klasy MathOperation, aby móc wywołać metodę sumEven:

const operation = new MathOperation();

operation.sumEven(2,5,5,4); //6 teraz jest ok

operation.sum(3,4,5); //TypeError ehh... co znowu?

Widać więc, że udało nam się „naprawić” jeden problem (sumEven), ale teraz z kolei nie działają pozostałe metody, które są zadeklarowane jako statyczne.

Ale zaraz zaraz… przyjrzyjmy się dokładniej obiektowi operation

Nie wyciągajmy jednak pochopnych wniosków… czy na pewno obiekt operation jest pozbawiony metod, przed którymi znajduje się słowo static? Otóż może Cię zaskoczę, ale istnieje możliwość ich użycia 🙂

Wymaga to jednak głębszego przeanalizowania łańcucha prototypów obiektu operation.  Obiekt operation dziedziczy po obiekcie globalnym Object.prototype, dysponuje więc metodami takimi jak hasOwnProperty, toString itp. Możemy się o tym przekonać wywołując poniższe polecenie:

typeof operation.toString; //"function"

Ponad to w obiekcie operation, po za odniesieniem do Object.prototype znajdują się dwa elementy: constructor oraz sumEven. Oba te elementy również są funkcjami (pamiętaj przy tym, że w JavaScript funkcja jest też obiektem z łańcuchem prototypów):

typeof operation.constructor; //"function"
typeof operation.sumEven;     //"function"

Object.getPrototypeOf(operation) === MathOperation.prototype; //true

No dobrze, zostawmy na razie na boku sumEven (bo przecież działa jak należy) i zastanówmy się co właściwie „siedzi” w konstruktorze:

typeof operation.constructor.sum;     //"function"
typeof operation.constructor.product; //"function"
typeof operation.constructor.sumEven; //"undefined"

typeof operation.sumEven; //"function"

Co się tutaj stało? Otóż udało nam się dostać do metod, zadeklarowanych jako statyczne! Spróbujmy więc je użyć:

operation.constructor.sum(2,3,4); //9
operation.constructor.product(2,5); //10

operation.constructor.sumEven(2,3,4); //TypeError ups nasz błąd...
operation.sumEven(2,3,4); //6

Ok, widać zatem, że mamy możliwość dostania się zarówno do metod statycznych jak i deklarowanych bez słowa static. Wymaga to stworzenia instancji obiektu MathOperation i małej sztuczki z odniesieniem do constructor.

Konwencje nazewnicze i wywoływanie metod statycznych

Można by w tym miejscu powiedzieć, że po co właściwie te dywagacje i szukanie trików i sztuczek jeśli po prostu można by wszystkie metody zadeklarować albo jako statyczne albo „zwykłe”. Mamy wtedy sprawę jasną – jeśli metody są static to nie tworzymy żadnego obiektu, a jeśli są „zwykłe” to musimy wcześniej wywołać new MathOperation.

Powiem tak – teoria teorią, ale odpowiedz sobie na pytanie czy na pewno od razu zauważyłeś w pierwszym listingu brak jednego słowa static? A jeśli takich metod byłoby nie 3 ale 30? Czasami w pośpiechu o błąd na prawdę nie trudno. Jeśli stosujesz się do zasady dokładnego testowania kodu to zapewne takie problemy wyłapiesz, ale zawsze pewne ryzyko pozostaje…

A teraz z innej strony… Czy zauważyłeś jakąś różnicę w sposobie wywoływania metod statycznych i niestatycznych? Nie chodzi mi tutaj o odniesienie się do obiektu operation czy bezpośrednio do MathOperation. W obu przypadkach stosujemy tradycyjny zapis z kropką. Osoby mające doświadczenie np. z językiem PHP doskonale pewnie wiedzą, że istnieje tam wyraźnie rozróżnienie w wywoływaniu metod statycznych i zwykłych:

//Metoda niestatyczna:
$instancja = new Klasa();
$result = $instancja->metoda();

//Metoda statyczna:
$result = Klasa::metoda()

W przypadku metody statycznej użyliśmy symbolu podwójnego dwukropka (określanego jako Paamayim nekudotayim) co jednoznacznie wskazuje na rodzaj metody.

Możesz powiedzieć, że przecież wiesz jak deklarujesz metody w klasach i wiesz czy są one statyczne czy nie… Dzisiaj to wiesz, ale czy za miesiąc lub za rok również będziesz o tym pamiętał? Czy czytając swój kod w celu dodania nowej lub poprawy istniejącej funkcjonalności na pewno będziesz pamiętał, że klasa X zawiera metody static, a klasa Y tylko metody zwykłe? Ponad to rodzi się pytanie jak wyraźnie wskazać innym programistom, że dana klasa zawiera tylko metody statyczne żeby nie tworzyli instancji z użyciem new Klasa().

Kolejna kwestia to konwencja nazewnicza. Przyjęła się w JavaScript zasada, że nazwy konstruktorów piszemy w notacji PascalCase (a nie camelCase jak nazwy metod i zmiennych). W czasach tzw. funkcji konstruktorów mówiło to wyraźnie innym programistom: „Zanim mnie użyjesz wywołaj new Konstruktor().

Ale co w naszym przypadku? Również mamy nazwę z wielkich liter MathOperation, co może sugerować konieczność stworzenia instancji tej klasy. Z kolei jeśli zastosujemy zapis mathOperation to sugerujemy, że jest to jakaś funkcja (metoda) lub zmienna… z deszczu pod rynnę…

Podsumowanie

Czy zatem metody statyczne to proszenie się o problemy? Otóż nie do końca. Jest to jeden z nowych elementów w JavaScript, który wg mnie został wprowadzony nieco w pośpiechu. Jeśli nie zastosowano oddzielnej metody wywoływania metod statycznych (np. z „::”) to myślę, że powinno zostać narzucone ograniczenie dopuszczające deklarowanie wszystkich metod klasy albo jako static albo jako zwykłych (bez możliwości ich mieszania). Szkoda, że nie można np. zastosować składni static class Klasa {...}.

Nie mamy jednak wpływu na to jak zostało zaimplementowane słowo static, ale zalecam dużą ostrożność w jego stosowaniu.

Według mnie jeśli zajdzie potrzeba aby w kodzie użyć klas z metodami statycznymi to warto po pierwsze cofnąć się o krok i ponownie przeanalizować strukturę aplikacji, a jeśli nadal static okaże się najlepszym wyjściem to stwórzmy do tego odpowiednio dobrą dokumentację.  I co ważne, chodzi tu zarówno o dokumentację „zewnętrzną” jak i dokładne skomentowanie sposobu użycia metod bezpośrednio w kodzie.

Chodzi mi o to, aby zabezpieczyć się przed problemami, które omówiłem w moim artykule. Jest to ważne szczególnie przy bardziej rozbudowanych aplikacjach, gdzie klasy tworzone są przez kilku programistów.

 

  • Zgadzam się z tym co piszesz, nie chodziło mi o „demonizowanie” i kategoryczne odstraszenie od używania metod statycznych. Chciałem tylko zwrócić uwagę na pewne przypadki przy nie do końca przemyślanym tworzeniu takich klas i szczególnie uczulić na ryzyko związane z mieszaniem w jednej klasie metod zwykłych i statycznych (co już uważam, że nie jest dobrym pomysłem i może wcześniej czy później sprawić problemy).

    Myślę, że lepiej zdawać sobie sprawę z różnych problemów, żeby bardziej uważać przy pisaniu kodu i szybciej móc znaleźć błędy 🙂

  • IMO zbytnio demonizujesz metody statyczne, zwłaszcza, że to nic nowego, bo istniały w JS od zawsze i zawsze działały tak samo. Od ES6 pojawiła się Javowata forma zapisu – tyle.

    Wystarczy uzmysłowić sobie, czym tak naprawdę są tak hucznie nazywane metody statyczne: to po prostu własności funkcji będącej konstruktorem.