Шаблон Decorator — это способ уменьшить множественные уровни наследования, которые конфликтуют друг с другом; например, если вы хотите создать объекты, которые обращаются к одному или нескольким из обязанностей X, Y или Z, простое наследование приводит вас к созданию следующих классов:
BaseClass X Y Z XY XZ YZ XYZ
в то время как шаблон Decorator позволяет создавать только:
BaseClass XDecorator YDecorator ZDecorator
Вариант шаблона Decorator, который мы увидим реализованным в JavaScript, не соответствует основному принципу оригинала: декорированные объекты расширяют интерфейс BaseClass. Не стесняйтесь комментировать, если вы узнали более знакомый шаблон
в коде вместо Decorator; фокус здесь находится в:
- автоматически наследовать методы от декорированного объекта.
- Возможность переопределять методы декорированных объектов для введения нового поведения.
- Будучи способным вызывать методы декорированных объектов во время переопределения, lik вы бы сделали с super () в Java или parent :: в PHP.
Начальное состояние
Начнем с базового класса, Болл. Названия классов и методов абсолютно наивны, так как они здесь, чтобы показать нам, как связывать объекты в JavaScript больше, чем моделировать реальный дизайн.
Тесты написаны с помощью jsTestDriver, который соответствует xUnit. Поскольку JavaScript не поддерживает классы, мы используем функции конструктора; Более того, я игнорирую разделение функций между экземплярами одного класса для простоты.
TestCase("basic Ball object test", { "test it should tell us what it is" : function() { var ball = new Ball(); assertEquals("I am a ball", ball.what()); } }); function Ball() { this.what = function() { return "I am a ball"; } }
Теперь у нас есть две разные характеристики, которые мы хотим добавить к объектам Ball, ноль или более одновременно:
"test a blinking ball is a ball that can blink" : function () { var ball = new BlinkingBall(); assertEquals("I am a ball", ball.what()); assertTrue(ball.canBlink()); }, "test a jumping ball is a ball that can jump" : function () { var ball = new JumpingBall(); assertEquals("I am a ball", ball.what()); assertTrue(ball.canJump()); }, "test a blinking&jumping ball is a ball that can do both" : function () { var ball = new BlinkingJumpingBall(); assertEquals("I am a ball", ball.what()); assertTrue(ball.canBlink()); assertTrue(ball.canJump()); }
Вы можете видеть, как этот дизайн заставляет вас вводить 2 ^ N (в данном случае 2 ^ 2) классов.
Декорирование
Давайте определим дизайн, основанный на декораторах. Объект, который мы создаем изначально, всегда является Ball; после этого он оборачивается во время выполнения одной или несколькими стратами. Каждый слой представляет собой новый объект, уровень косвенности, на котором можно задать новое поведение и перехватить вызовы метода.
TestCase("basic Ball object test", { "test it should tell us what it is" : function() { var ball = new Ball(); assertEquals("I am a ball", ball.what()); }, "test a blinking ball is a ball that can blink" : function () { var ball = Blinking.decorate(new Ball()); assertEquals("I am a ball", ball.what()); assertTrue(ball.canBlink()); }, "test a jumping ball is a ball that can jump" : function () { var ball = Jumping.decorate(new Ball()); assertEquals("I am a ball", ball.what()); assertTrue(ball.canJump()); }, "test a blinking&jumping ball is a ball that can do both" : function () { var ball = Blinking.decorate(Jumping.decorate(new Ball())); assertEquals("I am a ball", ball.what()); assertTrue(ball.canBlink()); assertTrue(ball.canJump()); } });
Поскольку декорирование выполняется во время выполнения, вам не нужно создавать декоратор BlinkingJumping, а просто объединить существующие декораторы вместе при создании объекта Ball (или вы можете декорировать его задолго до создания).
Это полная реализация класса Ball и двух его декораторов:
function Ball() { this.what = function() { return "I am a ball"; } } Blinking = {}; Blinking.decorate = function(originalBall) { newBallConstructor = function() { this.canBlink = function() { return true; }; }; newBallConstructor.prototype = originalBall; return new newBallConstructor(); } Jumping = {}; Jumping.decorate = function(originalBall) { newBallConstructor = function() { this.canJump = function() { return true; }; }; newBallConstructor.prototype = originalBall; return new newBallConstructor(); }
Функция конструктора newBallConstructor создается на лету во время каждого оформления; это добавляет к этому (объект, который будет создан) новые методы. Более того, прототип этой функции связан с исходным объектом; эффект состоит в том, что вызовы методов, не определенные newBallConstructor, делегируются интерпретируемым в originalBall.
Поскольку код создания декоратора довольно стандартный, мы можем устранить дублирование, распаковав его:
createDecorator = function(newConstructor) { return function(originalObject) { newConstructor.prototype = originalObject; return new newConstructor(); }; }; Blinking = {}; Blinking.decorate = createDecorator(function() { this.canBlink = function() { return true; }; });
Стандарт
Однако стандартный шаблон Decorator основан на перехвате вызовов, а не на расширении интерфейса объекта новыми методами; эта семантика также обусловлена статически типизированными языками, в которых она реализована, что не позволяет использовать динамический интерфейс, когда вызывающая сторона не знает, какие методы доступны во время компиляции.
Мы можем украсить существующий метод тоже. Давайте изменим тесты на:
TestCase("basic Ball object test", { "test it should tell us what it is" : function() { var ball = new Ball(); assertEquals("I am a ball", ball.what()); }, "test a blinking ball is a ball that can blink" : function () { var ball = Blinking.decorate(new Ball()); assertEquals("I am a ball (and I can blink)", ball.what()); assertTrue(ball.canBlink()); }, "test a jumping ball is a ball that can jump" : function () { var ball = Jumping.decorate(new Ball()); assertEquals("I am a ball", ball.what()); assertTrue(ball.canJump()); }, "test a blinking&jumping ball is a ball that can do both" : function () { var ball = Blinking.decorate(Jumping.decorate(new Ball())); assertEquals("I am a ball (and I can blink)", ball.what()); assertTrue(ball.canBlink()); assertTrue(ball.canJump()); } });
Теперь Blinking Decorator должен вызвать оригинальный метод Ball.what () и объединить результат «(и я могу моргнуть)» перед возвратом.
Blinking.decorate = createDecorator(function() { oldWhat = this.what; this.what = function() { return oldWhat.apply(this) + " (and I can blink)"; } this.canBlink = function() { return true; }; });
Внутри функции конструктора это всегда связывается с создаваемым объектом, даже внутри функций. Итак, мы выполняем эти шаги:
- сохранение старого, прежде чем его перезаписать; поскольку объект для украшения назначен прототипу, мы можем сделать это, просто обратившись к полю этого.
- Определение совершенно нового метода , который будет иметь приоритет над методами-прототипами.
- Делегирование части метода исходному с помощью метода apply () для объекта Function oldWhat. apply () гарантирует, что это внутри oldWhat () привязано к текущему объекту.