JS Decorators — приоденьте свой код

Никита Дубко, iTechArt Group

JS Decorators
приоденьте свой код


Никита Дубко, iTechArt Group

Iron Man Pop-art
iamherecozidraw
Никита Дубко

Никита Дубко

Хотелки

Как пройти в библиотеку?

Императивный подход Декларативный подход
  1. Выйти из здания, повернуть направо.
  2. Дойти до станции метро "Первомайская".
  3. Сесть на поезд до станции "Купаловской" и доехать до нее.
  4. Перейти на Московскую ветку.
  5. Сесть на поезд до станции "Восток" и доехать до нее.
  6. Выйти из метро направо по ходу движения.
  7. Идти к огромному ромбокубооктаэдру.
Адрес: город Минск, проспект Независимости 116
Thumb Up
Underscore.js
var fibonacci = _.memoize(function(n) {
    return n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2);
});

var throttled = _.throttle(updatePosition, 100);
window.addEventListener('scroll', throttled);
Cane
Lodash
var abc = function(a, b, c) {
    return [a, b, c];
};
var curried = _.curry(abc);
curried(1)(2)(3); // => [1, 2, 3]

_.delay(function(text) {
    console.log(text);
}, 1000, 'later');
Cool Cane
ES6
var handler = {
    get: function(target, name) {
        return (name in target) ? target[name] : 42;
    }
};
var p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42
Cool Cane
Iron Man
Wikipedia

Декоратор (англ. Decorator) — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту. Шаблон Декоратор предоставляет гибкую альтернативу практике создания подклассов с целью расширения функциональности.

C#: атрибуты

public class Person
{
    [Display(Name = "Имя")]
    [Required()]
    public string FirstName { get; set; }

    [Display(Name = "Отчество")]
    public string Patronym { get; set; }
}

Java: аннотации

@Service("testBean")
@Scope("singleton")
public class TestBean {
    private String data = "I am a singleton!!";

    public String getData() {
        return data;
    }
}

Python: декораторы

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

@makebold
def hello():
    return "hello world"

Спецификации

Паттерн «Декоратор»




class TonyStark {
    constructor() {
        this.isBillionaire = true;
        this.isPlayboy = true;
        this.isPhilanthropist = true;
    }
}
Tony Stark

Паттерн «Декоратор»

@Avenger
@Armor('Mark III')
@Assistant('J.A.R.V.I.S')
class TonyStark {
    constructor() {
        this.isBillionaire = true;
        this.isPlayboy = true;
        this.isPhilanthropist = true;
    }
}
Iron Man animation

Паттерн «Декоратор»

@Avenger
@Armor('Iron Captain')
class StevenRogers {
    constructor() {
        this.isLeader = true;
        this.equipment = [
            new VibraniumShield(),
            new VibraniumHelmet()
        ];
    }
}

Паттерн «Декоратор»

@Avenger
@Armor('Iron Hulk')
class BruceBanner {
    constructor() {
        this.isAngry = false;
    }
    toggle() {
        this.isAngry = !this.isAngry;
    }
    get isSmart() {
        return !this.isAngry;
    }
}

Stage 0

wycats / javascript-decorators

function decorator(...args) {
    return function(target, name, descriptor) {
        // do something
        return descriptor;
    }
}

Object.defineProperty()

Object.defineProperty(target, name, {
    enumerable: true,
    configurable: true,
    writable: true,
    value: 42,
    // get: function() {},
    // set: function(value) {}
});
const meetup = {
    name: 'MinskJS',
    number: 3
};
const meetup = Object.defineProperties({}, {
    name: {
        value: 'MinskJS',
        writable: true,
        enumerable: true,
        configurable: true
    },
    number: {
        value: 3,
        writable: true,
        enumerable: true,
        configurable: true
    }
});

Object.getOwnPropertyDescriptor()

const obj = {
    get getter() { return 42; },
    property: true,
    method: function() {
        return "I am the method";
    }
};

['getter', 'property', 'method'].forEach(p => {
    console.log(
        Object.getOwnPropertyDescriptor(obj, p)
    );
});

Виды декораторов

@classDecorator
class MyClass {
    @propertyDecorator
    property = 42

    @methodDecorator
    someMethod() {}
}

Декораторы

/* https://jsfiddle.net/dark_mefody/gL1j34sw/9/ */
function decorator(id) {
    console.log(id + ' declaration');
    return function(target, name, descriptor) {
        console.log(id + ' calling');
    };
}

@decorator('1st')
@decorator('2nd')
class MyClass {
    constructor() {
        console.log('new object created');
    }
}

const c = new MyClass();
Decorators calling order

Stage 2

tc39 / proposal-decorators

{
    kind: "method" or "field",
    key: String, Symbol or Private Name,
    placement: "static", "prototype" or "own",
    descriptor: Property Descriptor
}

ESnext

class MyClass {
    @First("test")
    @Second
    method() { }
}

ES5

var MyClass = (function () {
    function MyClass() {}
    MyClass.prototype.method = function () { }
    var _temp;
    _temp = First("test")(MyClass.prototype, "method",
        _temp = Second(MyClass.prototype, "method",
            _temp = Object.getOwnPropertyDescriptor(
                MyClass.prototype,
                "method"
            )) || _temp)
    || _temp;
    if (_temp) Object.defineProperty(MyClass.prototype, "method", _temp);
    return Foo;
})();

Где использовать?

React
<form className='new-item' onSubmit={this.addItem.bind(this)}>
class AddNewItemComponent extends React.Component {
    @autobind
    addItem(event) { ... }
    render() {
        return ( ...
            <form className='new-item' onSubmit={this.addItem}>
        ... );
    }
}

Проверка доступа

function checkPermission(target, name, descriptor) {
    const oldValue = descriptor.value;
    const newDescriptor = {
        enumerable: false,
        configurable: false,
        get: function() {
            return isAdmin() ? oldValue : (() => {});
        },
        set: function() {}
    };
    return newDescriptor;
}

Fluent interface

function fluent(target, name, descriptor) {
    const fn = descriptor.value;
    descriptor.value = function(...args) {
        fn.apply(target, args);
        return target;
    }
}

class Person {
    @fluent
    setName(name) { this.name = name; }
    sayName() { console.log(this.name); }
}

const p = new Person();
p.setName('Bruce').sayName();

Angular-подобный компонент

@Component({
    selector: 'minsk-js',
    template: `

Hello, MinskJS #<%= number %>!

` }) class MinskJsComponent { number = 3 } bootstrap();

Angular-подобный компонент

import _ from 'lodash';

const COMPONENTS = [];

function Component(config) {
    config.template = _.template(config.template);
    return function(target) {
        config.class = target;
        COMPONENTS.push(config);
    }
}

Angular-подобный компонент

const bootstrap = function() {
    COMPONENTS.forEach(component => {
        document
            .querySelectorAll(component.selector)
            .forEach(node => {
                const instance = new component.class();
                node.innerHTML = component.template(instance);
            });
    });
};

Angular-like component demo

Как начать использовать?

tsconfig.json

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

babel-plugin-transform-decorators-legacy

.babelrc

npm install --save-dev babel-plugin-transform-decorators-legacy
{
    "presets": ["env"],
    "plugins": ["transform-decorators-legacy"]
}

core-decorators

import { autobind } from 'core-decorators';

class Person {
    @autobind
    getPerson() {
        return this;
    }
}

const person = new Person();
const { getPerson } = person;
getPerson() === person; // true

lodash-decorators

import { Debounce, Memoize } from 'lodash-decorators';
class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Debounce(100)
    save(date) {
        return this.httpService.post(data);
    }
    @Memoize(item => item.id)
    doSomeHeavyProcessing(arg1, arg2) {}
}
import { Mixin } from 'lodash-decorators';
const MyOtherApi = {
    method() {
        // Do something cool
    }
};

@Mixin(MyOtherApi)
class Person {}

Person.prototype.method === MyOtherApi.method; // => true
Lego Iron Man

Браузерная поддержка

kangax.github.io/compat-table/

tc39/proposals

T39 Proposals

babel/babel

Babel7.next

JS Object Explorer — Sarah Drasner

Object Explorer

Итого

1. Думайте о других

2. Используйте фабричный метод для гибкости

3. Разбивайте на мелкие компоненты

4. Следите за развитием стандарта

Материалы

Видео

Спасибо за внимание!

mefody.github.io/talks/js-decorators/
@dark_mefody
n.a.dubko@gmail.com

QR code — ссылка на презентацию Iron Man