Статьи

Шаблон Decorator, или его двоюродный брат, в JavaScript


Шаблон 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;
    };
});

Внутри функции конструктора это всегда связывается с создаваемым объектом, даже внутри функций. Итак, мы выполняем эти шаги:

  1. сохранение старого, прежде чем его перезаписать; поскольку объект для украшения назначен прототипу, мы можем сделать это, просто обратившись к полю этого.
  2. Определение совершенно нового метода , который будет иметь приоритет над методами-прототипами.
  3. Делегирование части метода исходному с помощью метода apply () для объекта Function oldWhat. apply () гарантирует, что это внутри oldWhat () привязано к текущему объекту.