Własny model obiektowy w Javascripcie

Jedną z największych zalet jak i wad Javascriptu jest jego ogromna elastyczność i prostota. Dlaczego zalet jak i wad ? Dzięki elastyczności możemy więcej. Jednak z drugiej strony, możliwość rozwiązania jakiegoś problemu na kilka sposobów zmniejsza rozumienie kodu. Prostota i unifikacja JS wokół konceptu funkcji z jednej strony umożliwia wykorzystanie jednego mechanizmu enkapsulacji w wielu kontekstach. Natomiast z drugiej jest przez wielu nierozumiana, zwłaszcza przez osoby przyzwyczajone do języków gdzie występuje wiele słów kluczowych, przy pomocy których owy mechanizm jest realizowany. Elastyczność JS pozwala nam na takie zabawy, jak implementację własnego modelu obiektowego w efektywny sposób. Jakie cechy JS wspomagają ten proces ?

  1. Obiekty anonimowe
  2. Funkcja ‚__noSuchMethod__’ (odpowiednik __call w php)
  3. Funkcje anonimowe i domknięcia leksykalne

Tak na prawdę, przy pomocy JS można zaimplementować dowolną odmianę modelu obiektowego, który znajduję się w innych językach. Zarówno modele oparte na klasach, metaklasach, ich wariacje jak i na prototypach – które są wbudowane w JS. W tym artykule, który ma charakter badawczo – eksperymentalny chciałbym po pierwsze przedstawić model obiektowy jaki wymyśliłem, a po drugie jego implementację przy użyciu ww. listy cech języka. Mam nadzieje, że artykuł was zainteresuje oraz zainspiruje do podobnych eksperymentów :).

Skoro wiemy już jakiś środków użyjemy, należałoby postawić pytanie – co chcemy osiągnąć ?

Model obiektowy, który chciałbym zaimplementować będzie starał naśladować rzeczywistość. Założenia:

  1. Obiekty tworzone są w fabrykach
  2. Fabryki tworzą obiekty poprzez komponowanie ich z różnych elementów wg. planów / foremek
  3. Foremki są z natury niezmienne, ale można komponować z ich elementów nowe wzory wg uznania (można w ten sposób zaimplementować coś o funkcjonalności wielodziedzieczenia)
  4. Każdy obiekt po opuszczeniu fabryki żyje własnym życiem i przez swój cykl istnienia może diametralnie się zmienić w stosunku do swojego wzorca

Myślę, że założenia są dość logiczne i odzwierciedlają cykl jaki przechodzą właściwie wszystkie realnie istniejące obiekty na naszej planecie. Żeby nie być gołosłownym podam najbardziej oczywisty przykład – Samochód.

  1. Samochód jest tworzony w fabryce
  2. Fabryka tworzy samochód poprzez komponowanie go z różnych elementów (silnik, koła, skrzynia etc) wg projektu.
  3. Projekt samochodu jest raczej niezmienny, a jeżeli coś jest zmieniane to nazywa się to nowym modelem
  4. Samochód po opuszczeniu fabryki może trafić w ręce różnych ciekawych ludzi którzy np. poddadzą go wiejskiemu tuningowi, wtedy na 100% nie będzie wyglądał tak jak go projektant narysował :)

Czas na kod! Na początek – plan/foremka/template:


var fooTemplate = {
    state: {
        foo: {
            value: 1
        }
    },
    behavior: {
        incrementFoo: {
            scope: 'public',
            value: function(self){
                self.state.foo++
                console.log (self.state.foo, 'incrementFoo');
            }
        },
        decrementFoo: {
            scope: 'internal',
            value: function(self){
                self.state.foo--;
                console.log (self.state.foo);
            }
        }
    }
}

Template został zadeklarowany jako anonimowy obiekt przypisany do zmiennej „fooTemplate”. Wewnątrz niego trzymamy metadane dotyczące tego jak skonstruować działające „coś”. Zmienne oznaczone są jako „state”, natomiast metody jako „behavior”. Każdy element z oprócz domyślnej wartości (value) posiada różne adnotacje. Zakładam, że wszystkie zmienne obiektu są prywatne. Natomiast metody, mogą być publiczne, albo prywatne (odpowiednio public i internal). Oczywiście można dodać różne inne adnotacje wzorem tych z Javy albo C#. Każda metoda jako pierwszy argument przyjmuje „self” – wewnętrzną referencję do obiektu, odpowiednik „this”.

Kolejnym rzeczą jaka będzie nam potrzebna to fabryka obiektów, która będzie wiedziała jak z template’u stworzyć obiekt. Dla takiego rodzaju template’u, jak wyżej zaprezentowany stworzyłem fabrykę w postaci funkcji „genericFactory”:


function genericFactory(template){
    var self = {
        state: {},
        behavior: {
            'public': {},
            'internal': {}
        }
    };

    for(var i in template.state){
        self.state[i] = template.state[i].value
    }

    for(var i in template.behavior){
        if(template.behavior[i].scope == 'public'){
            self.behavior['public'][i] = template.behavior[i]['value']
        } else {
            self.behavior['internal'][i] = template.behavior[i]['value']
        }
    }

    function obj(){

        this.__noSuchMethod__ = function __noSuchMethod__(id, args){
            if(self.behavior['public'][id] != undefined){
                args.unshift(self);
                return self.behavior['public'][id].apply(this, args);
            }
            throw 'No such method!'
        }
    }
    return new obj;
}

Przetwarza ona template na konkretną instancję obiektu oraz enkapsuluje go w obiekcie „obj”. Obiekt „obj” posiada jedną metodę „__noSuchMethod__”. Jest ona odpowiednikiem „__call” w PHP. Sprawdza ona, czy metoda, która chcemy wywołać z obiektu jest możliwa do wywołania (jej scope jest „public”). Jeżeli tak, to jest wywoływana i zwracany jest efekt jej działania.

Jak już pisałem, efektem takiego podejścia do tworzenia obiektów jest możliwość zasymulowania wielodziedziczenia. Potrzebne będą nam do tego – jeszcze jeden template, oraz funkcja miksująca:


var barTemplate = {
    state: {
        bar: {
            value: 2
        }
    },
    behavior: {
        incrementBar: {
            scope: 'public',
            value: function(self){
                self.state.bar++
                console.log (self.state.bar, 'incrementBar');
            }
        },
        decrementBar: {
            scope: 'internal',
            value: function(self){
                self.state.bar--;
                console.log (self.state.bar);
            }
        }
    }
}

BarTemplate jest praktycznie identyczny jak fooTemplate, z tą różnicą, że zamiast słowa „foo” w nazwach metod używamy słowa „bar” ;).


function mixTemplates(){
    var resultTemplate = {
        state: {},
        behavior: {}
    };

    for(var i = 0; i< arguments.length; i++){
        var template = arguments[i];
        for(property in template){
            for(field in template[property]){
                resultTemplate[property][field] = template[property][field];
            }
        }

    }
    return resultTemplate;
}

Funkcja miksująca, „merguje” bez żadnych dodatkowych zabiegów dowolną ilość template’ów. Rezultatem zmiksowania kilku template’ów, jest template, który posiada wszystkie metody i właściwości z każdego składowego „template’a”. Generalnie końcowy rezultat jest identyczny z wielodziedziczeniem. Zaletą tego rozwiązania jest też to, że możemy dowolnie kształtować reguły „dziedziczenia” wg. np adnotacji. Można również w takiej funkcji zaimplementować własny mechanizm rozwiązywania konfliktów nazw.

Jak użyć przykładu:


var fooObj = genericFactory(fooTemplate);
fooObj.incrementFoo(); // 2 incrementFoo

var fooBarTemplate = mixTemplates(fooTemplate, barTemplate);

var fooBarObj = genericFactory(fooBarTemplate);
fooBarObj.incrementFoo(); // 2 incrementFoo
fooBarObj.incrementBar(); // 3 incrementBar

Podsumowując, efekt końcowy nie jest może do końca użyteczny, ponieważ należało by pewnie dopisać jeszcze sporo kodu, żeby można taką idee wykorzystywać w środowisku produkcyjnym. Jest to natomiast dobra baza dla tworzenia innych fajnych funkcjonalności. Szczególnie przydatna może okazać się możliwość adnotowania składowych „template’ów”, co otwiera pole do wielu zastosowań. Również mam wrażenie, że sposób zapisu template’ów, który przypomina sposób zapisu klas w innych językach, jest trochę bardziej „jadalny” niż tworzenie prototypów.

P.S:

Przykłady testowane na Firefox 3.6.13 😉

  1. Mocno zakombinowałeś, ale czyta się dobrze, warto zawsze poćwiczyć umysł. Ja prywatnie jestem zwolennikiem maksymalnej prostoty w kodzie, także skorzystałbym jednak z typowych rozwiązań. ;]

  2. @tomasz: tak jak mówiłem, to tylko taki eksperyment myślowy. Jest może to w pewien sposób skomplikowane, ale tylko od strony implementacji. Bo ideologicznie, przynajmniej moim zdaniem, jest bardziej logiczne niż inne rozwiązania :)

  3. Chciałbym zauważyć jeden szczegół;

    var self = {
            state: {},
            behavior: {
                'public': {},
                'internal': {}
            }
        };
    var id = 'foo';
    
    if (self.behavior['public'][id] != undefined) {
       console.log('istnieje');
    } else {
       console.log('nie ma')
    }

    wyświetli „nie ma”.

    to samo zostanie wyświetlone, kiedy:

    if (self.behavior['public'][id])

    co więcej, gdyby zrobić coś takiego:

    var self = {
            state: {},
            behavior: {
                'public': {},
                'internal': {}
            }
        };
    
    var id = 'foo';
    undefined = 1;
    
    if (self.behavior['public'][id] != undefined) {
       console.log('istnieje');
    } else {
       console.log('nie ma')
    }

    zostanie wyświetlone „istnieje” :)

    undefined jest zmienną:
    [quote]Description

    undefined is a property of the global object, i.e. it is a variable in global scope.[/quote]
    za: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/undefined

    wartość zmiennej może zostać zmieniona. Nie tylko `undefined’ może na tym ucierpieć :)
    http://www.yarpo.pl/2011/02/16/json-zamiast-konstruktorow/

    Co można zrobić?
    1. używać operatora typeof:

    if (typeof zmienna !== 'undefined')

    jak widać porównujemy tu do ciągu znaków, którego nie da się w żaden sposób nadpisać. Podobnie operator, w przeciwieństwie do zmiennej nie może być nadpisany.

    2. Używać operatora identyczny, a nie równy: http://www.yarpo.pl/2011/01/19/operatory-porownan-w-js/

    ma to związek z falsy values:
    http://ferrante.pl/frontend/javascript/falsy-values-i-operatory-porownania/

  1. There are no trackbacks for this post yet.

Leave a Reply

Informuj mnie o odpowiedziach poprzez e-mail. Możesz również subskrybować wpis bez zostawiania komentarza.