Hermetyzacja w Javascript

Hermetyzacja (zwana też enkapsulacją) to kolejne pojęcie nierozłącznie związane z programowaniem obiektowym. Polega ono na rozdzieleniu wewnętrznego i zewnętrznego interfejsu naszego obiektu.

Wyobraź sobie, że robisz dość skomplikowany mechanizm - np. machinę produkującą kebaby. Obiekt taki będzie miał pełno właściwości i metod, które będą działać w jego wnętrzu.

Ty jako programista raczej byś nie chciał, by inni programiści mogli grzebać w każdej składowej takiego obiektu, ponieważ potencjalnie mogli by coś popsuć i nasze dzieło przestało by prawidłowo działać.

Tu właśnie pojawia się pojęcie hermetyzacji, która polega na odpowiednim ukrywaniu pewnych metod i właściwości naszego obiektu przed zewnętrznym środowiskiem. Nasz obiekt może z nich korzystać, natomiast zewnętrzne środowisko może używać tylko rzeczy, które my mu świadomie udostępnimy.

Prywatne i publiczne

W programowaniu zorientowanym obiektowo metody i właściwości możemy podzielić na dwie grupy:

  • prywatne - mają do nich dostęp tylko metody danego obiektu
  • publiczne - mają do nich dostęp metody danego obiektu, ale i zewnętrzne środowisko

W wielu językach bazujących na obiektach do takiej klasyfikacji używa się słów kluczowych private i public. Poniżej zamieszczam zapożyczony z Wikipedii przykład klasy z Java:


class KontoBankowe {
    private TypPieniedzy saldo;

    public KontoBankowe(TypPieniedzy saldoPoczatkowe) {
        saldo = saldoPoczatkowe;
    };

    public KontoBankowe() {
        KontoBankowe(0);
    };

    public boolean wplac( TypPieniedzy kwota ) {
        if ( kwota > 0 ) {
            saldo += kwota;
            return true;
        }
        return false;
    }

    public boolean wyplac( TypPieniedzy kwota ) {
        // Powiększenie kwoty o 10% prowizji.
        TypPieniedzy kwotaProw = kwota*1.1;
        if ( ( kwotaProw > 0 ) && ( kwotaProw <= saldo ) ) {
            saldo -= kwotaProw;
            return true;
        }
        return false;
    }

    public TypPieniedzy podajStanKonta() {
    	return saldo;
    }
};

Widzisz modyfikatory public i private? To właśnie tymi słowami określamy która metoda i właściwość ma być prywatna lub publiczna. W niektórych językach pojawia się też słowo protected, ale nie będę się tutaj na nim skupiał.

Hermetyzacja w Javascript

A jak to działa w Javascript?

Hmm. Nie działa.

A przynajmniej nie tak jak w innych językach, ponieważ nie było tutaj oficjalnego podziału na prywatne i publiczne składowe obiektów.

Javascript został stworzony jako język do tworzenia prostych funkcjonalności na stronie. W założeniu język ten miał być jak najprostszy, przez co nie wymagał od programisty stosowania wszystkich dogmatów programowania zorientowanego obiektowo, a dzięki temu miał (i ma) o wiele mniejszy próg wejścia.

Język ten jest równocześnie na tyle elastyczny, że co bardziej sprytni programiści znaleźli obejścia braku pewnych klasycznych rozwiązań.

Tutaj mała uwaga, którą zapewne narażę się niektórym Javascriptowym nerdom 🙄.
Moim zdaniem w większości skryptów nie jest wymagane sięganie po omawiane tutaj rozwiązania. To, że takie rzeczy da się stosować, wcale nie oznacza, że musisz to robić za każdym razem. W Javascript w większości przypadków spokojnie wystarcza stosowanie zakresów (funkcji, bloków) oraz umiejętne dzielenie kodu na moduły.

Jeżeli natomiast mówimy o bardzo rozbudowanych aplikacjach, które bez takiej hermetyzacji się nie obejdą, prawdopodobnie lepszym rozwiązaniem będzie użycie Typescript, który akurat posiada podział na prywatne/publiczne zmienne plus daje wiele innych dobroci.

Dla naszych rozważań stwórzmy prostą klasę:


//chcemy by poszczególne metody i właściwości obiektu były:

class SimpleClass {
    constructor(nr) {
        this.publicNumber = nr; //to publiczne
        this.privateNumber = 102; //to ma być prywatne
    }

    publicMethod() { //to ma być publiczne
        console.log(this.publicNumber);
    }

    privateMethod() {
        console.log(this.privateNumber); //to ma być prywatne
    }
}

Jeżeli taką klasę stworzymy w klasyczny sposób (tak jak powyżej), wszystkie jej metody i właściwości będą publiczne. Oznacza to, że w każdej chwili mogę w obiekcie tworzonym na jej bazie wszystko zmieniać.


const my = new SimpleClass(10);
my.privateNumber = "ala ma kota"; //nadpisałem właściwość
my.privateMethod = "ala ma kota"; //nadpisałem funkcję

Jak to rozwiązać?

Jednym z dość często stosowanych rozwiązań jest zastosowanie konwencji poprzedzania prywatnych składowych znakiem podłogi:


class SimpleClass {
    constructor(nr) {
        this.publicNumber = nr; //to publiczne
        this._privateNumber = 102; //to ma być prywatne
    }

    publicMethod() { //to ma być publiczne
        console.log(this.publicNumber);
    }

    _privateMethod() {
        console.log(this._privateNumber); //to ma być prywatne
    }
}

Konwencja ta nie tyczy się tylko klas, a o wiele częściej stosowana jest przy tworzeniu pojedynczych obiektów.

Dzięki niej osoba używająca naszego kodu będzie wiedziała, że danej składowej nie powinna ruszać spoza obiektu.


const my = new SimpleClass();

//tego nie powinienem
my._privateMethod();
my._privateNumber = "ala ma kota";

//to mogę
my.publicMethod();
my.publicNumber = "ala ma kota";

Konwencja ta nie zabezpiecza nam kodu, a tylko daje wskazówkę dla innych programistów. Podobnych konwencji mamy w Javascript kilka - konstruktory piszemy z dużej litery, niektórzy programiści piszą nazwy stałych dużymi literami, pierwsze parametry funkcji w Node są miejscem na błędy, nadużywamy klas w html, bo nie umiemy css itp.

Kolejnym sposobem - tym razem już w pełni działającym jest zastosowanie czegoś, co już poznawaliśmy tutaj - czyli zasady, że funkcje i zakresy zagnieżdżone mają dostęp do zewnętrznego środowiska, natomiast zewnętrzne środowisko nie ma dostępu do wnętrza funkcji.

W Javascript możemy używać modułów, o których pomówimy tutaj. W każdym takim module wyznaczamy rzeczy, które zostaną wystawione poza dany plik. Dzięki temu inne pliki mają dostęp do rzeczy z danego pliku, które wystawiliśmy, natomiast nie mają dostępu do całej reszty kodu, który nie został przez nas wystawiony. Możemy to wykorzystać w naszym przypadku wyrzucając metody klasy poza jej ciało, dzięki czemu będzie miała do nich dostęp tylko nasza klasa, natomiast reszta plików zobaczy tylko klasę, którą właśnie wystawiliśmy.


//plik simple-class.js ----------
let privateNumber = 102;

function privateMethod() {
    console.log(privateNumber);
}

export class MyClass { //wystawiam klasę
    constructor(nr) {
        this.publicNumber = nr;
    }

    publicMethod() {
        console.log(this.publicNumber); //działa
        console.log(privateNumber); //działa
        privateMethod(); //działa
    }
}


//plik ----------
import { MyClass } from "./simple-class.js";

const my = new MyClass();

my.publicMethod(); //działa
console.log(my.publicNumber); //działa

my.privateMethod(); //błąd
console.log(my.privateNumber); //błąd

Jeżeli w danym projekcie nie używasz modułów, możesz jawnie okryć swój kod funkcją - np. IIFE, lub zastosować tak zwany wzorzec fabryki. Polega on na stworzeniu funkcji, która zwraca nam jakiś obiekt. Zwracany obiekt widzi swoje środowisko (czyli wnętrze funkcji), natomiast reszta kodu nie będzie miała do tych rzeczy dostępu.


function MyClass(nr) {
    let privateNumber = 102;

    function privateMethod() {
        console.log(privateNumber);
    }

    return {
        publicNumber: nr,

        publicMethod() {
            console.log(this.publicNumber); //działa
            console.log(this.privateNumber); //działa
            privateMethod(); //działa
        }
    }
}


const my = new MyClass();

my.publicMethod(); //działa
console.log(my.publicNumber); //działa

my.privateMethod(); //błąd
console.log(my.privateNumber); //błąd

Prywatne właściwości i metody w nowym Javascript

Od kilku lat Javascript mocno ewoluuje, a programiści coraz częściej za jego pomocą tworzą pokaźne aplikacje. Dlatego chcąc nie chcąc środowisko to musiało zaproponować oficjalne rozwiązania.

Jednym z nich jest propozycja wprowadzenia w Javascript składni określającej właściwości prywatne (1, 2).

Tak tworzone właściwości są dostępne dla klas i powinny rozpoczynać się znakiem #:


class KebabMachine {
    #kebab = { //prywatne
        roll : false,
        stuff : []
    }

    #makeRoll() { //prywatna
        this.#kebab.roll = true;
    }

    #makeStuff(stuff) { //prywatna
        this.#kebab.stuff = stuff.map(el => el.toLowerCase());
    }

    createKebab(...components) { //publiczna
        this.#makeRoll();
        this.#makeStuff(components);

        return this.#kebab;
    }
}

const machine = new KebabMachine();
machine.createKebab("mało mięsa", "dużo kapusty");
console.log(machine);

machine.#kebab = 100; //Private field '#kebab' must be declared in an enclosing class
machine.#makeStuff(); //Private field '#makeStuff' must be declared in an enclosing class

Tak czy siak nasz kod przydało by się przepuścić przez Babel wraz z zainstalowanym odpowiednim pluginem.

Setter i getter

Może się zdarzyć sytuacja, że będziemy chcieli dać zewnętrznemu środowisku dostęp do jakiejś wewnętrznej zmiennej naszego obiektu, ale tak, by mieć kontrolę nad tym jak ta wartość będzie pobierana i ustawiana.

Służą do tego tak zwane settery i gettery.

Funkcje zwane setterami są automatycznie odpalane, gdy będziemy chcieli ustawić właściwość o nazwie jaką ma dany setter.

Funkcje getter natomiast służą do pobierania wartości danej właściwości. Dodatkowo nie możemy tworzyć im żadnych parametrów.


const ob = {
    set pet(newName) { ... } //setter - odpalany gdy chcemy ustawić właściwość ob.pet
    get pet() { ... } //getter - odpalany gdy chcemy pobrać właściwość ob.pet
}

ob.pet = "pies";
console.log(ob.pet);

Spójrzmy na kilka przykładów:


const ob = {
    _age : 20,

    set age(newAge) {
        if (newAge > 0) {
            this._age = newAge;
        }
    }
}

ob.age = -2;
console.log(ob.age); //20
ob.age = 10;
console.log(ob.age); //10

const ob = {
    _name : "Marcin",

    set name(newName) {
        if (typeof newName === "string") {
            this._name = newName[0].toUpperCase() + newName.substr(1);
        }
    },

    get name() {
        return this._name;
    }
}

ob.name = "przemek";
console.log(ob.name); //Przemek

const car = {
    services : ["2020-10-10", "2021-02-05", "2022-03-10"],

    get last() {
        if (!this.services.length) {
            return undefined;
        }
        return this.services[this.services.length - 1];
    }
}

console.log(car.last); //2022-03-10

const user = {
    name : "Marcin",
    surname : "Przykładowy",

    get fullName() {
        return this.name + " " + this.surname;
    },

    set fullName(name) {
        if (typeof name !== "string") return false;
        const parts = name.split(" ");
        this.name = parts[0];
        this.surname = parts[1];
    }
}

user.fullName = "Karol Nowak";
console.log(user.name); //Karol
console.log(user.surname); //Nowak
console.log(user.fullName); //Karol Nowak

Kolejny przykład pokazuje zastosowania geterów i seterów wraz z powyżej pokazanym wzorcem closures, który wykorzystujemy do zabezpieczenia zmiennych lokalnych przed niepożądanym dostępem:


function makeObj() {
    let _name = "";
    let _counter = 0;

    return {
        set name(newName) {
            _name = newName;
        },
        get name() {
            return _name;
        },

        set counter(nr) {
            _counter = nr;
        },
        get counter(){
            return _counter;
        }
    }
}

const ob = makeObj();
console.log(ob._counter); //undefined

ob.counter = 20;
console.log(ob.counter); //20

ob._name = "Piotr";
ob.name = "Karol";
console.log(ob.name); //"Karol"

Podobnie getterów i setterów możemy użyć dla składni class:


// ES6 get and set
class Person {
    #name;

    constructor(name) {
        this.#name = name;
    }

    set name(newName) {
        this.#name = newName;
    }

    get name() {
        return this.#name[0].toUpperCase() + this.#name.substr(1);
    }

    walk() {
        console.log(this.#name + ' sobie idzie');
    }
}

const user = new Person("piotrek");
console.log(user.name); //Piotrek

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.

Menu