Шаблон 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 () привязано к текущему объекту.