Pull to refresh

JavaScript паттерны… для чайников

Reading time 8 min
Views 180K
Однажды вечером, сразу после того, как я закончил разбираться с наследованием в JS, мне пришла в голову идея, что пора бы заняться чем-нибудь посложнее — например паттернами. На столе внезапно оказалась книжка Gof, а на экране ноутбука появился труд с названием «JavaScript patterns».

В общем, спустя пару вечеров, у меня появились описания и реализации на JavaScriptе самых основных паттернов — Decorator, Observer, Factory, Mediator, Memoization (не совсем паттерн, а скорее техника, но мне кажется что она прекрасно в этот ряд вписывается) и Singleton.


Decorator


Чертовски приятный паттерн, с его помощью можно менять поведение объекта на лету, в зависимости от каких-нибудь условий.

Допустим, у нас есть такой код:

function Ball( param )
{
    this._radius = param.radius;
    this._color = param.color;
}
Ball.prototype = 
{
    constructor: Ball,

    INCREMENTATION_STEP: 5,

    draw: function(){console.log("ball drawn with radius:" + this._radius + " and color: " + this._color)},
    inc: function(){ this._radius += this.INCREMENTATION_STEP }
}

new Ball({ radius:100, color:"red"});

Здесь мы создаем новый красный мячик, а что делать если мячик нужен не просто красный, а красный в полоску? Вот тут на сцену и выходит Decorator.

Особый шарм ему придает то, что первоначальный Ball вообще не подозревает о том, что он может быть в полоску, или что у него могут быть какие-то там декораторы.

Реализовать паттерн можно несколькими способами:

Способ первый — комплексный

function StripedBall( ball )
{
    this._ball = ball    
}
StripedBall.prototype = 
{
    constructor: StripedBall,

    draw: function()
    {
        this._ball.draw();
        console.log("and with stripes");
    },
    inc: function()
    {
        return this._ball.inc();
    }
}

function SpeckledBall( ball )
{
    this._ball = ball    
}
SpeckledBall.prototype = 
{
    constructor: SpeckledBall,

    draw: function()
    {
        this._ball.draw();
        console.log("and with dots!");
    },
    inc: function()
    {
        return this._ball.inc();
    }
}

В каждом декораторе нужно воссоздать все функции которые должны быть в объекте родителе, и в тех из них, поведение которых мы менять не хотим, нужно просто перенаправлять запрос родителю. Этот способ лучше применять когда происходят серьезные изменения, которые затрагивают > 1 — 2 функций

Пишем простенький тест:

var ball1 = new SpeckledBall( new StripedBall( new Ball({ radius:100, color:"red"})));
var ball2 = new StripedBall( new SpeckledBall( new Ball({ radius:100, color:"green"})));

ball1.draw();
ball1.inc();
ball1.draw();

ball2.draw();

Глубокий вздох, и проверка:

ball drawn with radius:100 and color: red
and with stripes
and with dots!
ball drawn with radius:105 and color: red
and with stripes
and with dots!
ball drawn with radius:100 and color: green
and with dots!
and with stripes

Зря волновался — работает все как надо.

Способ второй — легковесный

function MakeStripedBall( ball )
{
    var function_name = "draw";
    var prev_func = ball[ function_name ];

    ball[ function_name ] = function()
    {
        prev_func.apply( this, arguments )
        console.log("and with stripes");
    };

    return ball;
}
function MakeSpeckledBall( ball )
{
    var function_name = "draw";
    var prev_func = ball[function_name];

    ball[function_name] = function ()
    {
        prev_func.apply(this, arguments)
        console.log("and with dots!");
    };

    return ball;
}

Кода, конечно, нужно меньше чем в первом случае, зато, если изменяемых функций больше чем 1-2, или изменения комплексные — разобраться во всем этом будет намного сложнее.

Пишем тест:

var ball3 = MakeStripedBall( MakeSpeckledBall( new Ball({ radius: 150, color: "blue" })));
var ball4 = MakeSpeckledBall( MakeStripedBall(new Ball({ radius: 150, color: "blue" })));

ball3.draw();
ball3.inc();
ball3.draw();

ball4.draw();

И проверяем, как все это работает:

ball drawn with radius:150 and color: blue
and with dots!
and with stripes
ball drawn with radius:155 and color: blue
and with dots!
and with stripes
ball drawn with radius:150 and color: blue
and with stripes
and with dots!

Все как надо.

Factory


Собственно, основной задачей фабрики в статически типизируемых языках является создание разных объектов с одинаковым интерфейсом, в зависимости от ситуаций, в JavaScript этак проблема так остро не стоит, так что появляется вопрос — зачем эта фабрика тут вообще нужна?

Все просто — помимо этой, первой, цели, у нее есть еще и вторая — фабрика может проводить какую-то первичную инициализацию объектов.

Например, предположим, у нас есть объекты Daddy, Mammy, и lad, создавая их с помощью фабрики мы можем просто сказать — familyfactory.createLad(); familyfactory.createDaddy(), а уж то, что они оба рыжие и 210см. роста, за нас решит фабрика — эти параметры мы не задаем.

Собственно, для того чтобы фабрика могла создавать какие-то объекты, для них сначала неплохо бы задать конструкторы (в этом примере объекты, к сожалению, не такие интересные как несколькими строками выше ):

var Shapes =
{
    Circle: function (param)
    {
        console.log("new " + param.color + " circle created with radius " + param.radius + "px");
    },
    Square: function (param)
    {
        console.log("new " + param.color + " square created with " + param.side + "px on a side ");
    },
    Triangle: function (param)
    {
        console.log("new " + param.color + " triangle created with " + param.side + "px on a side ");
    }
}

А теперь можно сделать и саму фабрику — выглядеть она может так:

function ShapeFactory(size, color)
{
    this.size = size;
    this.color = color;
}

ShapeFactory.prototype =
{
    constructor: ShapeFactory,

    makeCircle: function () { return new Shapes.Circle({ radius: this.size / 2, color: this.color }); },
    makeSquare: function () { return new Shapes.Square({ side: this.size, color: this.color }); },
    makeTrinagle: function () { return new Shapes.Triangle({ side: this.size, color: this.color }); }
}

Пишем скромненький тест:

var factory = new ShapeFactory(100, "red")

factory.makeSquare();
factory.makeSquare();
factory.makeTrinagle();
factory.makeCircle();
factory.makeTrinagle();

И смотрим в консоль:

new red square created with 100px on a side 
new red square created with 100px on a side 
new red triangle created with 100px on a side 
new red circle created with radius 50px
new red triangle created with 100px on a side 

Всё работает

Singleton


Что же такое синглтон? Объяснение будет сложным, долгим и нетривиальным — это объект, который есть в системе в одном экземпляре. Тадаам — конец объяснения.

Я частенько вообще не задумываюсь над тем, что это вообще-то тоже паттерн. Стоит сказать что перед тем как его применять стоит хорошенько подумать — на самом деле нужен синглетон не очень часто.
Сделать его можно несколькими способами.

Способ первый — тривиальный

var singleton_A =
{
    log: function( text ){ console.log(text); }
}

Это простой наглядный и эффективный метод, который, даже, в объяснении, по-моему, не нуждается.

Способ второй — выпендрежный

Основная его задача — это показать какой ты крутой однокурсникам или другим джуниорам. Кроме этого он, конечно, может быть действительно полезен — с таким подходом проще перестраиваться если планы изменились и где-то в середине проекта синглетон решили заменить несколькими объектами

var Singleton_B;
(function(){
    var instance;
    var anticlone_proxy;

    Singleton_B = function(){
        if( instance ){ return instance; }

        instance = 
        {
            _counter: 0,
            log: function( text ){ this._counter++; console.log( text + this._counter ); }
        }

        anticlone_proxy =
        {
            log: function( text ){ return instance.log( text ); }
        }

        return anticlone_proxy;
    };
})();

Его фишка в том что мы просто создаем объект, а синглетон он, или нет — нас в общем-то не очень волнует:

        function NonSingleton() { }
        NonSingleton.prototype =
        {
            consturctor: NonSingleton,
            scream: function(){console.log("Woooohoooooo!")}
        }

        var singleton = new Singleton_B();
        var nonsingleton = new NonSingleton();
        
        singleton.log("3..2..1... ignition!");
        nonsingleton.scream();

Если этот код выполнить, то в консоли мы увидим:

3..2..1... ignition!
Woooohoooooo!


Memoization


Очень простая и полезная техника — суть её в том, что для функции которая может долго вычислять результат, мы создаем небольшой кэш ответов. Работает это, разумеется, только в том случае, когда при одинаковых входных параметрах результат функции тоже должен быть одинаковый.

Создаем какую-нибудь медленную функцию, которая использует эту технику:

    function calculation(x, y)
    {
        var key = x.toString() + "|" + y.toString();
        var result = 0;

        if (!calculation.memento[key])
        {
            for (var i = 0; i < y; ++i) result += x;
            calculation.memento[key] = result;
        }
        return calculation.memento[key];
    }
    calculation.memento = {};

И проверяем сколько мы можем выйграть времени:

    console.profile();
    console.log('result:' + calculation(2, 100000000));
    console.profileEnd();

    console.profile();
    console.log('result:' + calculation(2, 100000000));
    console.profileEnd();

    console.profile();
    console.log('result:' + calculation(2, 10000000));
    console.profileEnd();

Если этот код теперь запустить в FF с Firebug, то мы увидим следующую статистику:

Profile1: 626.739ms
result:200000000
0.012ms
result:200000000
63.055msresult:20000000

Как видно из логов — при повторном запросе мы сэкономили кучу времени.

Mediator


Mediator — это такая штука, которая помогает в особо запущенных случаях взаимодействия между обьектами, например, когда у нас, скажем, 5 обьектов более-менее разного типа, и все почему-то знают друг о друге, стоит серьезно задуматься о медиаторе.

В качестве подготовки сначала сделаем несколько классов, которые в перспективе медиатор будут использовать (подсказка в данном случае медиатор будет называтья kitchen:)
function Daddy() { }
Daddy.prototype =
{
    constructor: Daddy,

    getBeer: function ()
    {
        if (!kitchen.tryToGetBeer())
        {
            console.log("Daddy: Who the hell drank all my beer?");
            return false;
        }

        console.log("Daddy: Yeeah! My beer!");
        kitchen.oneBeerHasGone();
        return true;
    },
    argue_back: function () { console.log("Daddy: it's my last beer, for shure!"); }
}

function Mammy() { }
Mammy.prototype =
{
    constructor: Mammy,

    argue: function ()
    {
        console.log("Mammy: You are f*king alconaut!");
        kitchen.disputeStarted();
    }
}

function BeerStorage(beer_bottle_count)
{
    this._beer_bottle_count = beer_bottle_count;
}
BeerStorage.prototype =
{
    constructor: BeerStorage,

    takeOneBeerAway: function ()
    {
        if (this._beer_bottle_count == 0) return false;
        this._beer_bottle_count--;
        return true;
    }
}

А теперь пора написать и сам медиатор:

var kitchen =
{
    daddy: new Daddy(),
    mammy: new Mammy(),
    refrigerator: new BeerStorage(3),
    stash: new BeerStorage(2),

    tryToGetBeer: function ()
    {
        if (this.refrigerator.takeOneBeerAway()) return true;
        if (this.stash.takeOneBeerAway()) return true;

        return false
    },
    oneBeerHasGone: function (){ this.mammy.argue(); },
    disputeStarted: function (){ this.daddy.argue_back(); }
}


И так, у нас есть 4 объекта работа со взаимодействием между которыми, могла бы превратиться в неплохое наказание, если бы проходила не через Mediator.

Пишем код проверки:

var round_counter = 0;
while (kitchen.daddy.getBeer())
{
    round_counter++
    console.log( round_counter + " round passed"); 
}

Спрашиваем у консоли — все ли идет по плану:

Daddy: Yeeah! My beer!
Mammy: You are f*king alconaut!
Daddy: it's my last beer, for shure!
1 round passed
...
Daddy: Yeeah! My beer!
Mammy: You are f*king alconaut!
Daddy: it's my last beer, for shure!
5 round passed
Daddy: Who the hell drank all my beer?

Некоторую часть этой душевной беседы я вырезал, но в целом все как надо.

Observer


Это тот самый паттерн, который мы используем по пятьдесят раз в день даже особенно об этом незадумывасяь — $(#some_useful_button).click( blah_blah_blah ) — знакомая конструкция? В ней click — это событие, а blah_blah_blah какраз и есть тот самый Observer который за этим событием наблюдает.

Для своих коварных планов, можно реализовать и собственную систему событий и наблюдателей, которая будет уметь реагировать не только на действия пользвоателя, но и на что-нибудь еще.

Ключевым её компонентом является объект событие:

Event = function()
{
    this._observers = [];
}

Event.prototype =
{
    raise: function (data)
    {
        for (var i in this._observers)
        {
            var item = this._observers[i];
            item.observer.call(item.context, data);
        }
    },
    subscribe: function (observer, context)
    {
        var ctx = context || null;
        this._observers.push({ observer: observer, context: ctx });
    },
    unsubscribe: function (observer, context )
    {
        for (var i in this._observers)
            if ( this._observers[i].observer == observer &&
                 this._observers[i].context == context )
                    delete this._observers[i];
    }
} 

Вообще, я тут подумал, что как-то скучно без скриншотов и ссылок, поэтому в этот раз примера будет два.

Первый — простой


var someEvent = new Event();
someEvent.subscribe(function ( data ) { console.log("wohoooooo " + data ) });

var someObject =
{
    _topSecretInfo: 42,
    observerFunction: function () { console.log("Top Secret:" + this._topSecretInfo) }
}
someEvent.subscribe(someObject.observerFunction, someObject); 
someEvent.raise("yeaah!");
someEvent.raise();

И консоль подтверждает что все работает.

wohoooooo yeaah!
Top Secret:42
wohoooooo undefined
Top Secret:42

И второй… тоже простой, но посимпатичнее




Потрогать его можно тут

На сегодня, я думаю всё. Все исходники кроме Observer можно посмотреть вот здесь, а Observer лежит отдельной папкой тут
Tags:
Hubs:
+98
Comments 46
Comments Comments 46

Articles