Суть паттерна в том, что есть класс с фактической функциональностью (компонент) и опциональными классами-обертками, которые дополняют основной функционал (декораторы). А фишка в том, что декораторов может быть сколько угодно, совмещаться они могут в произвольном порядке и (поскольку требуют от компонента только интерфейса) — могут работать с разными компонентами.
Безусловно, реализовать что-то похожее можно даже за счет только лишь того, что функции в JS являются объектами первого уровня, но мне бы хотелось поделиться реализацией весьма близкой кГОСТу GoF'у.
UPD: ссылка на рабочий пример, спасибо Barttos.
Перед хабракатом: в скрипте присутствует инкапсуляция, наследование (по сути) осуществляется через call, jQuery отсутствует — если Ваша идеология не позволяет принять такие ограничения, пожалуйста, не пишите об этом в комментах и, еще лучше, не читайте эту статью. Конструктивная критика и вопросы приветствуются.
Реализовывать будем простую листалку блоков. Не ново, но реализуем мы её так, что сможем перелистывать дивы без анимации или с ней (компоненты) и сможем выбирать, будут ли у нас кнопки переключения «страниц» :) или номер страницы (декораторы), или и то, и другое. Самое интересное, при использовании всего этого «богатства», нам будет без разницы ни как оно листается, ни сколько и каких UI-элементов задействовано.
HTML и CSS у листалки такой:
Самый-самый интерфейс называется Component (в листинге — Scroll), а его реализации — ConcreteComponent (в листинге: SimpleScroll и AnimScroll). Интерфейс декораторов, Decorator (и в листинге Decorator), тоже основан на интерфейсе Component. А уже реализации Decorator'а, ConcreteDecorator (в листинге: Decorator_SwitchPage и Decorator_PageNum), относятся к Component'у косвенно.
А вот nextPage и prevPage различаются, но декораторы просто вызывают методы и могут работать с обеими классами.
PageNum добавляет индикатор текущей страницы, и их общего количества.
После подключения Decorator'а нам надо сохранить ссылки на его методы — в качестве костыля выступает локальная переменная methods. Она используется в перегруженных методах, чтобы запустить соответствующий метод у вложенного компонента. Хороший момент, чтобы оценить разницу в количестве методов у Decorator'а и его реализаций ;)
И вот такой script:
Безусловно, реализовать что-то похожее можно даже за счет только лишь того, что функции в JS являются объектами первого уровня, но мне бы хотелось поделиться реализацией весьма близкой к
UPD: ссылка на рабочий пример, спасибо Barttos.
Перед хабракатом: в скрипте присутствует инкапсуляция, наследование (по сути) осуществляется через call, jQuery отсутствует — если Ваша идеология не позволяет принять такие ограничения, пожалуйста, не пишите об этом в комментах и, еще лучше, не читайте эту статью. Конструктивная критика и вопросы приветствуются.
Реализовывать будем простую листалку блоков. Не ново, но реализуем мы её так, что сможем перелистывать дивы без анимации или с ней (компоненты) и сможем выбирать, будут ли у нас кнопки переключения «страниц» :) или номер страницы (декораторы), или и то, и другое. Самое интересное, при использовании всего этого «богатства», нам будет без разницы ни как оно листается, ни сколько и каких UI-элементов задействовано.
HTML и CSS у листалки такой:
<html> <head> <title> </title> <style> #container { padding-top: 53px; padding-bottom: 3px; border: 1px solid gray; } #container, #scroll div { width: 100px; } #scroll, #scroll div { height: 50px; } #scroll div { float: left; } #container { position: relative; overflow: hidden; } #scroll { position: absolute; top: 0px; width: 1000px; border-bottom: 1px solid gray; } </style> <script> /* * в конце топика */ </script> </head> <body> <div id="container"> <div id="scroll" style="left: 0px;"> <div style="background: #ffc;">страница 1</div> <div style="background: #fcf;">страница 2</div> <div style="background: #cff;">страница 3</div> <div style="background: #fcc;">страница 4</div> <div style="background: #ccf;">страница 5</div> <div style="background: #cfc;">страница 6</div> <div style="background: #ccc;">страница 7</div> </div> </div> <script> /* * использование */ </script> </body> </html>
Как накрутить UI сверху
Компонент является самостоятельной частью, готовой перематывать страницы при вызове .nextPage и .prevPage. Чтобы накрутить что-нибудь сверху нам надо:- создать декоратор;
- передать декоратору компонент;
- сделать у декораторов теже методы, что у компонента;
- работать с методами декоратора, а он уже будет делать свою функциональность и вызывать теже методы у компонента.
Участники
Все компоненты должны иметь одинаковый интерфейс, и все декораторы должны иметь тот же самый интерфейс, но дополнительно расширенный, чтобы уметь принимать компонент.Самый-самый интерфейс называется Component (в листинге — Scroll), а его реализации — ConcreteComponent (в листинге: SimpleScroll и AnimScroll). Интерфейс декораторов, Decorator (и в листинге Decorator), тоже основан на интерфейсе Component. А уже реализации Decorator'а, ConcreteDecorator (в листинге: Decorator_SwitchPage и Decorator_PageNum), относятся к Component'у косвенно.
Scroll и Decorator
Рекомендую посмотреть Scroll и Decorator, листинг кода внизу статьи. Как видим, Decorator переписывает (перегружает) все методы Scroll'а:- комментировать каждый из них повторно не требуется :)
- Decorator запускает тот же метод у вложенного (через setComponent) компонента.
SimpleScroll и AnimScroll
Благодаря Scroll'у, оба класса умеют работать с локальными переменными container и scroll. Это ноды с position: relative и absolute соответственно. Методы hasNextPage, hasPrevPage, findPages и getCurPage совпадают, но я не стал выносить их в Scroll, чтобы тот хоть немного напоминал интерфейс. Вполне можно вынести эти методы в промежуточный класс.А вот nextPage и prevPage различаются, но декораторы просто вызывают методы и могут работать с обеими классами.
Decorator_SwitchPage и Decorator_PageNum
SwitchPage добавляет в container кнопки «вперед» и «назад».PageNum добавляет индикатор текущей страницы, и их общего количества.
После подключения Decorator'а нам надо сохранить ссылки на его методы — в качестве костыля выступает локальная переменная methods. Она используется в перегруженных методах, чтобы запустить соответствующий метод у вложенного компонента. Хороший момент, чтобы оценить разницу в количестве методов у Decorator'а и его реализаций ;)
Использование
При использовании будет создаваться компонент — SimpleScroll или AnimScroll. Затем декораторы: PageNum и SwitchPage. В первый декоратор передается компонент, во второй декоратор — первый декоратор. Работать мы будем с крайним (самым верхним) декоратором, а он будет отправлять вызов методов вниз по цепочке.SimpleScroll + PageNum + SwitchPage
// создаем компонент - основу для дальнейшей работы
component = new SimpleScroll();
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
// создаем первый декоратор
decorator1 = new Decorator_PageNum();
decorator1.setComponent(component); // заворачиваем компонент в декоратор
// создаем еще один декоратор
decorator2 = new Decorator_SwitchPage();
decorator2.setComponent(decorator1); // заворачиваем первый декоратор во второй
decorator2.init();
decorator2.nextPage();
Без декораторов
component = new SimpleScroll();
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
component.init();
component.nextPage();
AnimScroll + SwitchPage
component = new AnimScroll();
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
decorator1 = new Decorator_SwitchPage(); // или new Decorator_PageNum();
decorator1.setComponent(component);
decorator1.init();
decorator1.nextPage();
JavaScript
// Component (интерфейс) // из-за scope, в нем так же будут заданы локальные переменные и сеттеры/геттеры к ним - не академично, но и не страшно // если есть желание использовать только прототипирование, то надо создавать локальные переменные "на месте", // а все функции, которые их используют в Scroll оставить пустыми function Scroll() { var container, scroll; this.setContainer = function(val) { container = val; }; this.setScroll = function(val) { scroll = val; }; this.getContainer = function() { return container; }; this.getScroll = function() { return scroll; }; this.init = function() { }; // ручная инициализация после задания container и scroll // это несколько упростит задачу, т.к. нам не надо будет переписывать сеттеры this.nextPage = function() { }; // перематываем вперед this.prevPage = function() { }; // перематываем назад this.hasNextPage = function(depth) { }; // наличие следующей страницы, или следующих depth страниц; по-умолчанию depth = 1 this.hasPrevPage = function(depth) { }; // наличие предыдущей страницы, или следующих depth страниц; по-умолчанию depth = 1 this.findPages = function() { }; // метод возвр. кол-во страниц this.getCurPage = function() { }; // метод возвр. номер текущей страницы } // ConcreteComponent (реализация Component'а) // самый простой скроллинг function SimpleScroll() { var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this) Scroll.call(this); // агрегируем интерфейс // Scroll уже умеет работать с container и scroll, но ничего более - // теперь нам надо реализовать (перегрузить) все пустые методы var curPage = 0; // текущая страница (0-4) this.init = function() { }; this.nextPage = function() { if (dublicate.hasNextPage()) { this.getScroll().style.left = ++curPage * -100 +"px"; } }; this.prevPage = function() { if (dublicate.hasPrevPage()) { this.getScroll().style.left = --curPage * -100 +"px"; } }; this.hasNextPage = function(depth) { var depth = depth || 1; return curPage + depth < dublicate.findPages(); }; this.hasPrevPage = function(depth) { var depth = depth || 1; return curPage - depth >= 0; }; this.findPages = function() { return this.getScroll().getElementsByTagName("div").length; }; this.getCurPage = function() { return curPage; }; } // ConcreteComponent (реализация Component'а) // другой скроллинг, с анимацией function AnimScroll() { var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this) Scroll.call(this); // агрегируем интерфейс var curPage = 0; var curOffset = 0; // текущее смещение страницы в пикселях this.init = function() { }; this.nextPage = function() { if (dublicate.hasNextPage() && !curOffset) { curPage++; // прибавим сразу, чтобы декораторы работали curOffset = 0; nextPageIterate(); } }; function nextPageIterate() { curOffset -= 10; dublicate.getScroll().style.left = curOffset + (curPage-1)* -100 +"px"; if (curOffset>-100) { window.setTimeout(arguments.callee, 20); } else { curOffset = 0; } } this.prevPage = function() { if (dublicate.hasPrevPage() && !curOffset) { curPage--; curOffset=0; prevPageIterate(); } }; function prevPageIterate() { curOffset += 10; dublicate.getScroll().style.left = curOffset + (curPage+1)* -100 +"px"; if (curOffset<100) { window.setTimeout(arguments.callee, 20); } else { curOffset = 0; } } this.hasNextPage = function(depth) { var depth = depth || 1; return curPage + depth < dublicate.findPages(); }; this.hasPrevPage = function(depth) { var depth = depth || 1; return curPage - depth >= 0; }; this.findPages = function() { return this.getScroll().getElementsByTagName("div").length; }; this.getCurPage = function() { return curPage; }; } // Decorator (интерфейс) // умеет инкапсулировать (сохранять в локальную переменную component) компонент или другой декоратор // и просто передает вызовы методов в component (кроме setComponent и getComponent) function Decorator() { var component; this.setComponent = function(val) { component = val; }; this.getComponent = function() { return component; }; this.setContainer = function(val) { return component.setContainer(val); }; this.setScroll = function(val) { return component.setScroll(val); }; this.getContainer = function() { return component.getContainer(); }; this.getScroll = function() { return component.getScroll(); }; this.init = function() { return component.init(); }; this.nextPage = function() { return component.nextPage(); }; this.prevPage = function() { return component.prevPage(); }; this.hasNextPage = function(depth) { return component.hasNextPage(depth); }; this.hasPrevPage = function(depth) { return component.hasPrevPage(depth); }; this.findPages = function() { return component.findPages(); }; this.getCurPage = function() { return component.getCurPage(); }; } Decorator.prototype = new Scroll(); Decorator.prototype.constructor = Decorator; // ConcreteDecorator (реализация Decorator'а) // кнопки для переключения страниц function Decorator_SwitchPage() { var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this) // подключаем Decorator и записываем ссылки на методы, которые определенны в "интерфейсе" Decorator'а, и которые будут переопределяться здесь Decorator.call(this); var methods = { nextPage: this.nextPage, prevPage: this.prevPage, init: this.init }; var buttonNext, buttonPrev; this.init = function() { dublicate.getContainer().appendChild( buttonPrev = createButton("<", dublicate.prevPage) ); dublicate.getContainer().appendChild( buttonNext = createButton(">", dublicate.nextPage) ); buttonNext.disabled = !dublicate.hasNextPage(); buttonPrev.disabled = !dublicate.hasPrevPage(); return methods.init(); }; this.nextPage = function() { buttonNext.disabled = !dublicate.hasNextPage(2); buttonPrev.disabled = !dublicate.hasPrevPage(-1); return methods.nextPage(); }; this.prevPage = function() { buttonNext.disabled = !dublicate.hasNextPage(-1); buttonPrev.disabled = !dublicate.hasPrevPage(2); return methods.prevPage(); }; function createButton(text, onclick) { var ret = document.createElement("button"); ret.appendChild( document.createTextNode( text ) ); ret.onclick = onclick; return ret; } } // ConcreteDecorator (реализация Decorator'а) // индикатор текущей страницы function Decorator_PageNum() { var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this) Decorator.call(this); var methods = { nextPage: this.nextPage, prevPage: this.prevPage, init: this.init }; var text; this.init = function() { dublicate.getContainer().appendChild( text = document.createTextNode( "..." ) ); var ret = methods.init(); chText(); return ret; }; this.nextPage = function() { var ret = methods.nextPage(); chText(); return ret; }; this.prevPage = function() { var ret = methods.prevPage(); chText(); return ret; }; function chText() { text.nodeValue = " "+ (dublicate.getCurPage()+1) +" / "+ dublicate.findPages(); } }
Пряники
Когда компонентов или декораторов будет много, можно написать удобную функцию:function cr() { var ret, last; for (var i=0, l=arguments.length; i<l; i++) { ret = new arguments[i](); if (!i) { ret.setContainer(document.getElementById("container")); ret.setScroll(document.getElementById("scroll")); } else ( ret.setComponent(last); } last = ret; } return ret; }
Демка
К верстке добавляем:<table cellpadding="5"><tr> <th>workWith</th> <td>=</td> <td> <div> <input type="radio" name="component" value="simple" id="component-simple" checked="checked" /> <label for="component-simple">SimpleScroll</label> </div> <input type="radio" name="component" value="simple" id="component-anim" /> <label for="component-anim">AnimScroll</label> </td> <td>+</td> <td> <input type="checkbox" id="pageNum" checked="checked" /> <label for="pageNum">pageNum</label> </td> <td>+</td> <td> <input type="checkbox" id="switchPage" checked="checked" /> <label for="switchPage">switchPage</label> </td> </tr></table> <button onclick="create();">Пересоздать компонент с декораторами</button> <br /> <button onclick="workWith.prevPage();">workWith.prevPage()</button> <button onclick="workWith.nextPage();">workWith.nextPage()</button> <br />
И вот такой script:
var component; var decorator1; var decorator2; var workWith; function reset() { component = decorator1 = decorator2 = workWith = null; var node = document.getElementById("scroll"); node.style.left = "0px"; while(node.nextSibling) { node.parentNode.removeChild(node.nextSibling); } } function create() { reset(); // сбрасываем предыдущие настройки (если были) // создаем компонент - основу для дальнейшей работы if (document.getElementById("component-simple").checked) { component = new SimpleScroll(); } else { component = new AnimScroll(); } component.setContainer(document.getElementById("container")); component.setScroll(document.getElementById("scroll")); workWith = component; if (document.getElementById("pageNum").checked) { // создаем первый декоратор decorator1 = new Decorator_PageNum(); decorator1.setComponent(component); // заворачиваем компонент в декоратор workWith = decorator1; } if (document.getElementById("switchPage").checked) { // создаем еще один декоратор decorator2 = new Decorator_SwitchPage(); if (decorator1) { // заворачиваем первый декоратор во второй decorator2.setComponent(decorator1); } else { // заворачиваем компонент в декоратор decorator2.setComponent(component); } workWith = decorator2; } workWith.init(); } create();