Принципы и приемы написания эффективного кода

Александр Завьялов, Евгений Мокеев

Сервис «Заметки»

blocks/
bundles/
controllers/
└── notes.js
└── pages.js
models/
└── note.js
app.js
routes.js

    GET  /
    GET  /notes
    POST /notes
    GET  /notes/:name

Задача

Аутентификация пользователя

  • Проверка логина/пароля
  • Страница с формой
  • Редирект при успешной проверке
  • Вывод ошибки при неудачной попытке

Как это работает

Страница аутентификации

controllers/
└── user.js


exports.login = (req, res) => {
    res.render('login/login');
};
routes.js


const user = require('./controllers/user');

module.exports = function (app) {
    app.get('/login', user.login);
}

GET /login
bundles/
└── login
    └── login.hbs


Модель пользователя

models/
└── user.js


const users = [
    {id: 1, username: 'admin', password: 'admin'}
];

class User {
    static find(username, password) {}

    static findById(id) {}
}

module.exports = User;

Сессии

Способность сохранять и восстанавливать окружение посетителя между запросами к сайту.

Сессии

package.json


"dependencies": {
    "express-session": "^1.13.0",
}

app.js


app.use(require('express-session')({}));

req.session

Библиотека passport

lib/
└── passport
    └── index.js


const User = require('../../models/user');

exports.authenticate = (req, res) => {};

exports.onlyAuth = (req, res) => {};

exports.authenticate = (req, res) => {
    const user = User.find(
        req.body.username,
        req.body.password
    );

    if (!user) {
        req.session.authFailMessage =
            'Пользователь не найден';
        res.redirect('/login');
        return;
    }

    req.session.auth = { id: user.id };

    res.redirect('/');
};

exports.onlyAuth = (req, res, next) => {
    const auth = req.session.auth;

    if (auth && auth.id) {
        if (User.findById(auth.id)) {
            return next();
        }
    }

    res.sendStatus(401);
};
controllers/
└── user.js


exports.login = (req, res) => {
    res.render('login/login');
};

exports.login = (req, res) => {
    const data = {};

    if (req.session.authFailMessage) {
        data.error = req.session.authFailMessage;
    }

    res.render('login/login', data);
};

exports.profile = (req, res) => {
    res.send('Страница профиля');
};
routes.js


app.post('/login', passport.authenticate);

POST /login


app.get('/profile', passport.onlyAuth, user.profile);

GET  /profile

Задание выполнено

Новые фичи

  • Данные пользователя на странице профиля
  • Аутентифицировать через социальные сети
  • Увеличить надежность проверки пароля
  • Редиректить на другую страницу
  • Показать другое сообщение об ошибке
  • Прикрутить к трем других проектам

Переписывать весь код?

Почему?

Проблемы нашей бибиотеки

  • Много обязанностей
  • Много знаний о внешних объектах
  • Нет точек расширения/эволюции

Ароматы плохого модуля

  • Жесткий дизайн
  • Хрупкость
  • Монолитность
  • Сложность

SOLID

S - Single responsibility principle

O - Open/closed principle

L - Liskov substitution principle

I - Interface segregation principle

D - Dependency inversion principle

Зачем?

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

God Object

Результат

  • Маленькие модули
  • Простое тестирование
  • Четкая структура приложения
  • Реиспользование модулей
  • Меньше проблем при доработках

Middleware

routes.js


app.get(
    '/profile',
    passport.onlyAuth,
    user.profile
);


Chain of Responsibility

Делит ответственность за обработку между несколькими обработчиками
lib/
└── passport
    └── index.js


    exports.onlyAuth = (req, res, next) => {
    const auth = req.session.auth;

    if (auth && auth.id) {
        if (User.findById(auth.id)) {
            return next();
        }
    }

    res.sendStatus(401);
};
lib/
└── passport
    └── index.js


exports.initUser = (req, res, next) => {
    const auth = req.session.auth;

    if (auth && auth.id) {
        req.user = User.findById(auth.id);
    }

    next();
};

exports.onlyAuth = (req, res, next) => {
    if (!req.user) {
        res.sendStatus(401);
        return;
    }

    next();
};
app.js


const passport = require('./lib/passport');

app.use(passport.initUser);
controllers/
└── user.js


exports.profile = (req, res) => {
    res.send('Страница профиля');
};

exports.profile = (req, res) => {
    res.send(`Привет, ${req.user.username}!`);
};
Сущности (классы, модули, функции) должны быть открыты к раширению, но закрыты от модификаций.

Результат

  • Гибкие сущности
  • Простое тестирование
  • Меньше сильных связей
  • Нет правок в базовых сущностях
lib/
└── passport
    └── index.js


exports.authenticate = (req, res) => {
    const user = User.find(
        req.body.username,
        req.body.password
    );

    if (!user) {
        req.session.authFailMessage =
            'Пользователь не найден';
        res.redirect('/login');
        return;
    }

    req.session.auth = { id: user.id };

    res.redirect('/');
};
routes.js


app.post('/login', passport.authenticate);


app.post('/login', passport.authenticate({
    successRedirect: '/',
    failureRedirect: '/login',
    failureMessage: 'Неправильный логин или пароль'
}));
lib/
└── passport
    └── index.js


exports.authenticate = (req, res) => {};


exports.authenticate = options => {
    return (req, res) => {};
};

exports.authenticate = options => {
    return (req, res) => {
        const user = User.find(
            req.body.username,
            req.body.password
        );

        if (!user) {
            req.session.authFailMessage =
                options.failureMessage'Пользователь не найден';
            res.redirect(options.failureRedirect'/login');
            return;
        }

        req.session.auth = { id: user.id };

        res.redirect(options.successRedirect'/');
    }
};


Factory

Создает однотипные объекты
lib/
└── passport
    └── index.js


    exports.authenticate = options => {
    return (req, res) => {
        const user = User.find(
            req.body.username,
            req.body.password
        );

        if (!user) {
            req.session.authFailMessage =
                options.failureMessage ;
            res.redirect( options.failureRedirect );
            return;
        }

        req.session.auth = { id: user.id };

        res.redirect(options.successRedirect);
    }
};
lib/
└── passport
    └── index.js


exports.initUser = (req, res, next) => {
    const auth = req.session.auth;

    if (auth && auth.id) {
        req.user = User.findById(auth.id);
    }

    next();
};

Сильные связи

Умные вещи


// Сериализация пользователя
auth = { id: user.id }


// Десериализация пользователя
user = User.findById(auth.id)
lib/
└── passport
    └── index.js


let serialize = () => {};
let deserialize = () => {};

exports.authenticate = options => {
    return (req, res) => {
        const user = User.find(
            req.body.username,
            req.body.password
        );

        if (!user) {
            req.session.authFailMessage =
                options.failureMessage ;
            res.redirect( options.failureRedirect );
            return;
        }

        req.session.auth = { id: serialize(user)user.id }

        res.redirect(options.successRedirect);
    }
};
lib/
└── passport
    └── index.js


exports.initUser = (req, res, next) => {
    const auth = req.session.auth;

    if (auth && auth.id) {
        req.user = deserialize(auth.id)User.findById(auth.id)
    }

    next();
};
lib/
└── passport
    └── index.js


exports.registerSerializer = fn => {
    serialize = fn;
};

exports.registerDeserializer = fn => {
    deserialize = fn;
};

Кто знает о пользователе?

models/
└── user.js


class User {
    static getSerializator() {
        return user => user.id;
    }

    static getDeserializator() {
        return id => User.findById(id);
    }
}

Что осталось?

app.js


const User = require('./models/user');
const passport = require('./lib/passport/index');

passport.registerSerializer(User.getSerializator());
passport.registerDeserializer(User.getDeserializator());
lib/
└── passport
    └── index.js
    passport.js


const User = require('../models/user');
const passport = require('./passport/index');

passport.registerSerializer(User.getSerializator());
passport.registerDeserializer(User.getDeserializator());


Decorator

Расширение базовой функциональности без наследования

require кеширует результат



Singleton

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

Результат

  • Предсказуемое поведение потомков

exports.authenticate = options => {
    return (req, res) => {
        const user = User.find(
            req.body.username,
            req.body.password
        );

        if (!user) {
            req.session.authFailMessage =
                options.failureMessage ;
            res.redirect( options.failureRedirect );
            return;
        }

        req.session.auth = { id: serialize(user) }

        res.redirect(options.successRedirect);
    }
};
lib/
└── passport
    └── index.js


const strategies = {};

exports.registerStrategy = (name, strategy) => {
    strategies[name] = strategy;
};

Что делает стратегия?


exports.authenticate = options => {
    return (req, res) => {
        /* поиск пользователя */const user = User.find(
            req.body.username,
            req.body.password
        );

        if (!user) { /* проверка пользователя */
            /* обработка ошибки */req.session.authFailMessage =
                options.failureMessage;
            res.redirect( options.failureRedirect );
            return;
        }

        /* обработка успешного результата */req.session.auth = { id: serialize(user) }

        res.redirect(options.successRedirect);
    }
};

exports.authenticate = (name, options) => {
    return (req, res) => {
        const strategy = strategies[name];

        /* проверка пользователя */
        strategy.authenticate(req, (err, user) => {
            if (err) {
                /* обработка ошибки */
            }

            /* обработка успешного результата */
        });
    }
};
lib/
└── passport
    └── strategies
        └── strategy.js


class Strategy {
    constructor(verify) {
        this._verify = verify;
    }

    authenticate(req, done) {
        const username = req.body.username;
        const password = req.body.password;

        this._verify(username, password, done);
    }
}

module.exports = Strategy;
lib/
└── passport.js


const Strategy = require('./passport/strategies/strategy');

passport.registerStrategy(
    'local',
    new Strategy((username, password, done) => {
        const user = User.find(username, password);

        const err = user ? null : new Error('...');

        done(err, user);
}));

Задаем стратегию при вызове

routes.js


app.post('/login', passport.authenticate('local', {
    successRedirect: '/',
    failureRedirect: '/login'
}));


Strategy

Выбор алгоритма поведения независимо от клиентов, которые его используют

Нужна еще одна стратегия?

lib/
└── passport
    └── strategies
        └── anotherStrategy.js


var Strategy = require('./strategy');

class AnotherStrategy extends Strategy  {
    constructor(verify) {}

    authenticate(req, done) {}
}

Passport зависит от Strategy

AnotherStrategy зависит от Strategy

Плохо

lib/
└── passport
    └── strategies
        └── BaseStrategy.js


class BaseStrategy {
    constructor(verify) {
        this._verify = verifgoog
    }

    authenticate(req, done) {
        done(new Error('Такого пользователя нет'));
    }
}

module.exports = BaseStrategy;
lib/
└── passport
    └── strategies
        └── LocalStrategy.js


var BaseStrategy = require('./base');

class LocalStrategy extends BaseStrategy {
    authenticate(req, done) {
        const username = req.body.username;
        const password = req.body.password;

        this._verify(username, password, done);
    }
}

module.exports = LocalStrategy;
lib/
└── passport
    └── strategies
        └── CookieStrategy.js


var BaseStrategy = require('./base');

class CookieStrategy extends BaseStrategy {
    authenticate(req, done) {
        userId = req.cookies.userId;

        this._verify(userId, done);
    }
}

module.exports = CookieStrategy;
lib/
└── passport
    └── strategies
        └── AnotherStrategy.js


    var BaseStrategy = require('./base');

class BadStrategy extends BaseStrategy {
    authenticate(req, done) {
        userId = req.cookies.userId;

        if (userId === 9) {
            throw new Error('...');
        }

        this._verify(userId, done);
    }
}
Маленькие интерфесы лучше больших

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

Результат

  • Один интерфейс = одна роль
  • Маленькие интерфейсы
models
    └── user.js


class User {
    static findByName(username) {}

    static checkPassword(password){}static find(username, password) {}
}
lib
└── passport.js


passport.registerStrategy('local', new Strategy((username, password, done) => {
    const user = User.findByName(username);

    if (!user) {
        done(new Error('Пользователя не существует'));
        return;
    }

    if (!user.checkPassword(password)) {
        done(new Error('Неправильный пароль'));
        return;
    }

    done(null, user);
}));
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции.

Результат

  • Модули не имеют жестких связей
  • Изменения локализованны
  • Изменения не ломают систему
lib/
└── passport.js


// Инъекция через метод
passport.registerSerializer(...);
passport.registerDeserializer(...);
passport.registerStrategy(...);

// Инъекция через конструктор
passport.registerStrategy('local', new LocalStartegy(...));

// Инъекция через свойство
passport.strategy = ...;

Что мы получили?

  • Данные пользователя на странице профиля
  • Аутентифицировать через социальные сети
  • Увеличить надежность проверки пароля
  • Редиректить на другую страницу
  • Показать другое сообщение об ошибке
  • Прикрутить к трем других проектам

Без фанатизма!

Авторизация в «Заметках»

Книги

Паттерны проектирования

Node.js Design Patterns

Learning JavaScript Design Patterns

JavaScript. Шаблоны

Приемы объектно-ориентированного
проектирования