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

    Однажды вечером, сразу после того, как я закончил разбираться с наследованием в 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 лежит отдельной папкой тут
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 46

      –34
      javascript singleton дальше не читал
        0
        можете рассказать почему?
          0
          я тоже дочитал только до singleton, и скажу почему: я работаю javascript разработчиком и в понедельник, сразу после рабочего дня, такие статьи читать противопоказано. Но статья полезная вроде :)
            0
            я не понял — почему противопоказано?
              0
              ну как бы утомление сильное, голова болит и прочие вещи. Надо же отдыхать?
                +8
                типо на хабре сиски ищите? а тут singleton? соболезную…
            0
            var singleton = new Singleton_B();
            var singleton2 = singleton.clone();
              0
              согласен. подумаю что с этим можно сделать.
                0
                Верните человеку карму. Я тоже дальше синглетонов не читал, пока не увидел, что человека заминусовали.
                  0
                  поправил. теперь даже если клонировать его( deepCopy, Shallow copy ) все все-равно будет работать корректно. покрайней мере с реализацией клонирования из jQuery. благодаря вашему комментарию стал немного умнее :)
                    0
                    Но зачем, зачем?
                      0
                      если уж сказал что его можно использовать как обычный объект и нипариться, надо знаичт до конца имплементировать все верно :)
                        0
                        Есть такая поговорка про простреленные ноги, но у вас другой бич, вы так тщательно пытаетесь упаковаться в бронежилеты что в конечном счете умираете от перегрева, я серьезно.
            0
            Итак, в честь 34 минусов, привожу свой вариант статьи

            Паттерны в javascript

            Decorator

            var Ball = function () {
            }

            Ball.prototype = new Object()

            Ball.prototype.draw = function () {

            console.log(«draw ball»)

            }

            var StripedBall = function () {

            Ball.apply(this, arguments)

            }

            StripedBall.prototype = new Ball()

            StripedBall.prototype.draw = function () {

            Ball.prototype.draw.apply(this, [])

            console.log(«with stripes!»)

            }

            Factory

            Важно понимать, что паттерны в общем случае это все же не готовые классы, это не код, это парадигмы, реализация которых в принципе не так важна

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

            var Shapes = { Circle: function ()…?

            // i dont think so

            var createCircle = funciton ()…

            // или

            var Shape = function ()…

            Shape.createCircle = function ()…

            Водицы полили, далее

            Singleton

            Процитирую автора: это объект, который есть в системе в одном экземпляре

            window.singleton = new Objet()

            что еще добавить? добавить нечего, увы

            Memoization — жевали в комментариях

            Mediator — реализация в статье, феерический п@#$%ц, вы только вдумайтесь в эго код. Пацаны с "-34" делают так:

            var Daddy = function (kitchen) {

            this.kitchen = kitchen

            }

            Daddy.prototype = new Object()

            Daddy.prototype.getBeer = function () {

            if (! this.kitchen…

            }

            var Mammy = function (kitchen) {



            }



            var Kitchen = function () {

            refrigerator = new BeerStorage(999)

            stash = new BeerStorage(888)


            }



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

            Observer — реализовать можно по разному, много хороших разных реализаций, лучше использовать стороннюю, $.on/$.off so on. Хотя реализация в статье, можно сказать, раскрывает суть, сойдет для ознакомления.

            P.S. Весь комментарий и код набраны в блокноте, ни разу не проверялись, комментарии ниже так же особо не мониторились. Извините если повторяюсь, и думайте головой, пожалуйста, вы сюда плюсы пришли ставить или свет нести?
            0
            Memoization вообще-то лучше куда-то вне ложить. Или в data в jquery или html5 storage.
              +1
              Cпасибо. нехотелось просто их впутывать. а чем такой подход плох?
                +2
                у вас будет постоянно расти стек. Зачем это вам? Если можно скидывать во внешний сторадж.
                  +1
                  Это будет уже не мемоиз а кеш.
                  Вот, например, нормальный декоратор — мемоизатор, хотя многим выносит мозги.
                  memoize = function(func, context, single) {
                  function memoizeArg (argPos, depth) {
                  var cache = {};
                  return function () {
                  if (argPos == 0 && depth == 0) {
                  argPos = arguments.length;
                  }
                  if (argPos <= 0) {
                  if (!(arguments[argPos] in cache)) {
                  cache[arguments[argPos]] = func.apply(context, arguments);
                  }
                  return cache[arguments[argPos]];
                  }
                  else {
                  if (!(arguments[argPos] in cache)) {
                  if (single) {
                  cache = {};
                  }
                  cache[arguments[argPos]] = memoizeArg(argPos - 1, depth + 1);
                  }
                  return cache[arguments[argPos]].apply(this, arguments);
                  }
                  }
                  }

                  return memoizeArg(func.length - 1, 0);
                  }
                  итого..
                  var calculationFunctor = memoize(calculation);
                  • UFO just landed and posted this here
                      0
                      Посмотев внимательно, можно заметить, что в комментарии предыдущего оратора присутствует HTML <code>, скорее всего попавший туда из как раз <source>. Увы, в последнее время на хабре что-то у этого тэга сломалось, и я избегаю его. По крайней мере в постах смотрится ужасно. Ваш комментарий лучше бы смотрелся так в сложившейся ситуации:

                      don't use <source>, Luke
                      
                0
                Действительно — для чайников. спасибо за работу.
                +2
                Рекомендую к прочтению www.ozon.ru/context/detail/id/6287517/ (да простят меня боги за ссылку на озон)
                  0
                  читал оригинал. если етсь какието косяки в реализациях буду очень благодарен если укажете
                    0
                    Ну реализация фабрики отличается, хотя не скажу что ваша хуже.
                    Еще неплохо было бы стратегию описать, в остальном все ок, спасибо)
                      0
                      я, может быть, потом еще одну статью про паттерны сделаю :) а некоторые реализации в той книжке мне показались не самыми удачными. например тот-же Decorator. если найду мыло автора обязательно спрошу про это — вдруг ответит :)
                  0
                  Плюс вам. Паттерны — это весело.
                    0
                    Спасибо за паттерны. Очень полезно.
                    Есть вопрос: зачем переопределять конструктор?
                      +2
                      Если вы о
                      Daddy.prototype = {
                      constructor: Daddy,
                      //...
                      }
                      , то тут недавно спрашивали: habrahabr.ru/blogs/javascript/132340/#comment_4393309
                        0
                        По той ссылке не ходите — там вирусы там я плохо написал :)
                        В общем, как только вы пишите
                        Daddy.prototype = {
                        все! Карабах! Вы затерли напрочь все, что раньше было в прототипе, в том числе и конструктор. И не мешало бы его восстановить. Честно-то сказать, единственное зачем это надо, так исключительно для корректной работы оператора typeof, все остальное само по себе работает. Или я не совсем прав, м, ребят?
                          0
                          Ололо! Не typeof, а instanceof!
                            0
                            круто обьясняешь :)) нет instanceof вообще не при делах — я обьяснял в той ветке :)
                              0
                              всмысле только что обьяснил %)
                                0
                                А вот до меня не дошло, что вы там писали в той ветке. Если все, что надо работает и так, зачем переопределять constructor заново?

                                И еще вопрос: как в паттерне Decorator реализовать наследование? Допустим у меня есть класс мужик, как мне от него сделать классы Папа и Сын?

                                var Man = function() {
                                this.hasBalls = true;
                                }

                                Man.prototype = {
                                say: function() {
                                console.log('Yeah!');
                                }
                                };

                                var Dad = function() {
                                // как автоматом проставить, чтоб у него были яйца?
                                // как получить доступ к методу say или переопределить его?
                                // только передав Man в качестве параметра?
                                }
                                  0
                                  простите, неочень понял вопрос — если надо отнаследовать смотрите статью про наследование или экстенд. какое отношение к наследованию имеет декоратор?
                                    0
                                    все работает до тех пор, пока мы не захотим обратиться к полю constructor. которое есть у каждого объекта. если не переопределять — там будет конструктор Object
                          +1
                          function calculation(x, y){
                          var key = x.toString() + y.toString();

                          У вас вызовы calculation(1, 23) и calculation(12, 3) будут на один и тот же ключ завязаны. Нужен разделитель.
                            0
                            Спасибо огромное — поправил :)
                            +1
                            Кстати, для функций в js можно писать декораторы так же, как и для python'а (кроме синтаксического сахара, кончно):
                            <code class="javascript">
                            function decorator(fn) {
                              function decoratorInner(arg) {
                                console.log('decorator');
                                return fn(arg);
                              }
                              
                              return decoratorInner;
                            };
                            
                            function test(arg) {
                              console.log('test ' + arg);
                              return 'test result';
                            };
                            
                            console.log(test('#test arg'));
                            
                            var decoratedTest = decorator(test);
                            console.log(decoratedTest('#decorated test arg'));
                            </code>
                            
                              0
                              Спасибо огромное. когда будет чуток побольше времени — разберусь в коде напишу примеры и добавлю в статью.
                              +3
                              Для чайников отлично, спасибо :)
                              P.S. Крокфорд советует не переносить открывающиеся фигурные скобки на новую строку. Причина — semicolon insertion. Да и смотрятся они на той же строке приятнее :)
                                +1
                                угу я знаю, надо переучиваться. спасибо за совет :)
                                0
                                Большое спасибо за статью и за ваш труд. Очень полезная статья для начинающих.
                                Буду исспользовать как дополнительный ресурс для изучения патернов.
                                  0
                                  Тщательнее надо — инициализация красным, а вывод зеленым.

                                  var ball2 = new StripedBall( new SpeckledBall( new Ball({ radius:100, color:«red»})));

                                  ball2.draw();

                                  ball drawn with radius:100 and color: green
                                  and with dots!
                                  and with stripes
                                    0
                                    спасибо огромное, поправил.
                                  • UFO just landed and posted this here

                                    Only users with full accounts can post comments. Log in, please.