SOLID

Михаил Давыдов

SOLID

Михаил Давыдов

Курс по паттернам, 2014

S.O.L.I.D

Нафига S.O.L.I.D?

Single
Responsibility
Principle

Принцип
единственной
ответственности

Single Responsibility

На каждую сущность должна быть возложена одна единственная ответственность.

Пример про Корзину

            function addToCart(e) {
                var $el = $(this);
                cart.push($el.attr('id'), $el.attr('title'));
                var newItem = $('<li></li>')
                    .html($el.attr('title'))
                    .attr('id-cart', $el.attr('id'))
                    .appendTo('#cart');
            }
        

Проблемы в примере

Исправленный пример

            function addToCart(item) {
                cart.add(_.pick(item, ['id', 'title']));
            }
        
            this.bindTo(cart, 'add', (item) => {
                render(item).appendTo(this.$el);
            });
        

Проблемы в примере

Более исправленный пример

            bind('#cart', {
                cart: cart
            });
        
            <ul id="cart">
                <li data-each-item="cart" id="{item.id}">
                    {item.title}
                </li>
            </ul>
        

Пример про Mailer

            class MyMailer {
                send (options, cb) {
                    this.smtpTransport.sendMail(options, () => {
                        cb();
                        console.log('Message sent');
                    });
                }
            }
        

Проблемы в примере

Исправленный пример

            class MyMailer {
                /**
                 * @param {ILogger} logger
                 */
                constructor(logger) {
                    this.logger = logger;
                }
            }
        

Исправленный пример

            class MyMailer {
                send (options, cb) {
                    this.smtpTransport.sendMail(options, => {
                        cb();
                        this.logger.log('Message sent');
                    });
                }
            }
        

Проблемы в примере

Более исправленный пример

            class MyMailer extends EventEmitter {
                send (options, cb) {
                    this.smtpTransport.sendMail(options, => {
                        cb();
                        this.emit('done', 'Message sent');
                    });
                }
            }
        

Стереотипы ролей объектов

Стереотипы ролей объектов

Стереотипы ролей помогают разделять ответственность

Влияние Single Responsibility

Задачи по SRP

Open-
Closed
Principle

Принцип
открытости-
закрытости

Open-Closed

Сущности (классы, модули, функции) должны быть открыты к раширению, но закрыты от модификаций.

Пример с отрисовкой вопросов

            function renderQuestion (q) {
                switch (q.type) {
                    case 'yes-no':
                    case 'multi':
                        return q.title + renderAnswers(q.answers);
                }
            }
        

Проблемы в примере

Исправленный пример

            class QuestionRenderer {
                constructor () {
                    this.methods = {};
                }
                use (methods) {
                    Object.assign(this.methods, methods);
                }
            }
        

Исправленный пример

            class QuestionRenderer {
                render (question) {
                    return this.methods[question.type](question);
                }
            }
        

Опять пример про Mailer

            class MyMailer {
                send (mail, cb) {
                    this.smtpTransport.sendMail(mail, => () {
                        cb();
                        console.log('Message sent');
                    });
                }
            }
        

Проблемы в примере

Исправленный пример

            class MyMailer extends EventEmitter {
                send (mail) {
                    this.emit('before-send', mail);
                    this.smtpTransport.sendMail(mail, => (err, st) {
                        if (err) return this.emit('error', err);
                        this.emit('after-send', st);
                    });
                }
            }
        

Пример использования

            var mailer = new MyMailer(options);
            listenTo(mailer, 'before-send', formatAndSaveLogToDb);
            listenTo(mailer, 'after-send', formatAndPipeToStdout);
            listenTo(mailer, 'after-send', () => res.render('ok'));
            listenTo(mailer, 'error', sendAlertSmsTo(cfg.adminEmail));
            mailer.send(someMail);
        

Пример про робота

            class Robot {
                constructor (weapons) {
                    this.weapons = weapons;
                    this.weapon = weapons[0];
                }
                shoot () {}
            }
        

Пример про робота

            shoot () {
                if (this.weapon instanceof Shotgun) {
                    this.weapon.loadShell(1);
                    this.weapon.pressTrigger();
                }
                if (this.weapon instanceof Grenade) {
                    this.weapon.throw();
                }
            }
        

Проблемы в примере

Исправленный пример

            class Robot {
                constructor (weapons) {
                    this.weapons = weapons;
                    this.weapon = weapons[0];
                }
                shoot () {
                    this.weapon.use();
                }
            }
        

Влияние Open-Closed

Задачи по OCP

Liskov
Substitution
Principle

Принцип
Замещения
Лискова

Liskov Substitution

Сущности, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа не зная об этом.
Подкласс не должен требовать от вызывающего кода больше, чем базовый класс, и не должен предоставлять вызывающему коду меньше, чем базовый класс.

Liskov Substitution

Пример нарушения Предусловий

            var robot = new Robot();
            robot.shot();
            // class SniperRobot extends Robot
            var robot = new SniperRobot();
            robot.reload();
            robot.shot();
        

Проблемы в примере

Пример нарушения Постусловий

            class PointFromIp extends Geolocation {
                getLocation () {
                    return super().point;
                }
            }
        

Проблемы в примере

Пример нарушения Исключений

            function Robot(name) {
                this.name = name;
            }
            function RobotMk2 (name, weapons) {
                Robot.call(this, name);
                if (!weapons) throw new Error();
            }
        

Проблемы в примере

Пример нарушения Инвариантности

            class Vehicle {
                constructor () {
                    this.speed = 0;
                }
                accelerate () { this.speed++; }
                decelerate () { this.speed--; }
            }
        

Пример нарушения Инвариантности

            class FastVehicle extends Vehicle {
                accelerate () { this.speed++; this.speed++; }
            }
            var vehicle = new FastVehicle();
            vehicle.accelerate();
            vehicle.decelerate();
            expect(vehicle.speed).to.equal(0);
        

Пример нарушения Инвариантности

            class Person {
                decelerateBike () {
                    this.bike.decelerate();
                    if (this.bike instanceof FastVehicle) {
                        this.bike.decelerate();
                    }
                }
            }
        

Проблемы в примере

Пример нарушения Исторических ограничений

            class Point {
                center() {
                    return this._center;
                }
            }
        

Пример нарушения Исторических ограничений

            class MutablePoint extends Point {
                center(value) {
                    if (!value) return this._center;
                    this._center = value;
                }
            }
        

Проблемы в примере

Пример соблюдения Исторических ограничений

            class Circle extends Point {
                radius(value) {
                    if (!value) return this._radius;
                    this._radius = value;
                }
            }
        

Пример

            class Square extends Rectangle {
                width(value) {
                    if (!value) return this._width;
                    this._width = value;
                    this._height = value;
                }
                height(value) { return this.width(value); }
            }
        

Проблемы в примере

            var sq = new Square();
            sq.width(1);
            sq.height(2);
            expect(sq.width() * sq.height()).to.equal(2);
        

Влияние Liskov substitution

Задачи по LSP

Interface
Segregation
Principle

Принцип
разделения
интерфейса

Interface Segregation

Наследники не должны имплементировать методы, которыми они не пользуются.

Интерфейсы и JavaScript

Интерфейсы и JavaScript

            // EventEmitter
            {
                on: function(event, cb) {}
                off: function(event, cb) {}
                emit: function(event, data) {}
            }
        
Class: events.EventEmitter

Интерфейсы и JavaScript

            // Promise aka thenable
            {
                // @param {Function} [onFulfilled]
                // @param {Function} [onRejected]
                // @returns {Promise}
                then: function(onFulfilled, onRejected) {}
            }
        
Promise/A+ Specification

Пример

            class IWeapon {
                reload() {
                    throw new AbstractMethodError();
                }
                fire() {
                    throw new AbstractMethodError();
                }
            }
        

Пример

            class Rifle extends IWeapon {
                reload() {
                    this._pullHammer();
                }
                fire() {
                    this._pullTrigger();
                }
            }
        

Пример

            class Grenade extends IWeapon {
                reload() {}
                fire() {
                    this._pullCheck();
                    this._throw();
                }
            }
        

Проблемы в примере

Пример

            class IProduct {
                log(value) {throw new AbstractMethodError();}
                addTo(cart) {throw new AbstractMethodError();}
                destroy() {throw new AbstractMethodError();}
                getLink() {throw new AbstractMethodError();}
                updateImage(img) {throw new AbstractMethodError();}
                loadImage(img, cb) {throw new AbstractMethodError();}
            }
        

Пример

            class IButton {
                getIcon() {throw new AbstractMethodError();}
                getLabel(cart) {throw new AbstractMethodError();}
                getUrl() {throw new AbstractMethodError();}
                onClick(cb) {throw new AbstractMethodError();}
                onLongPress(cb) {throw new AbstractMethodError();}
            }
        

Проблемы в примере

Role Interface

Влияние Interface Segregation

Задачи по ISP

Dependency
Inversion
Principle

Принцип
инверсии
зависимости

Dependency Inversion

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции.
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Пример

            class TrackMap {
                renderMap() {
                    return new google.maps.Map(this.el, this.options);
                }
                addMarker(options) {
                    new google.maps.Marker(options).setMap(this.map);
                }
            }
        

Проблемы в примере

Решение проблем в примере

            var MapProvider = {
                // @param {Object}   options
                // @param {Number[]} options.ll
                // @param {String}   options.title
                // @returns {IMarker}
                Marker(options) {}
                Map(options) {}
            }
        

Решение проблем в примере

            class TrackMap {
                constructor(mapProvider) {
                    this.api = mapProvider;
                }
                addMarker(options) {
                    var marker = new this.api.Marker(options);
                    marker.addTo(this.map);
                }
            }
        

Решение проблем в примере

Влияние Dependency Inversion

Попытка реиспользовать модуль в новом проекте не должна привести к копипасту половины модулей из старого проекта.

Задачи по DIP

Заключение

Заключение

SOLID

Михаил Давыдов

clck.ru/98btn