РСС

Ношу шлем, тяжело дышу…

Меня зовут Антон Шувалов. Я работаю в Lazada. Кроме программирования я пишу музыку и иногда занимаюсь дизайном интерфейсов. Я есть в Twitter, Facebook, и на GitHub. Вы можете написать мне email.

Если вы задумали порадовать меня небольшим подарком (не может быть!) — вот список моих мещанских мечт.

7 паттернов для рефакторинга JavaScript-приложений:

Паттерн «объект-представление»

фото: shuttermanic

17 октября 2012 года Bryan Helmkamp, основатель Code Climate написал пост описывающий 7 паттернов для рефакторинга толстых ActiveRecord-моделей в Ruby on Rails. Здесь, в Crush & Lovely, у всех Rails-разработчиков этот пост является основным ориентиром для написания модульного, лаконичного, выразительного и тестируемого кода.

В этой серии статей мы расскажем о подобных концепциях в JavaScript. Как и работа Bryan Helmkamp, эта серия так же применима к моделям данных, и не менее полезна. В этом посте обсудим паттерн «объект-форму.

Паттерны рефакторинга JavaScript-приложений

Объект-представление

Лучше всего храненить логику и атрибуты необходимые только для формирования представления (HTML в обычных сайтах или JSON в API) вне моделей. Хранение специфичной информации непосредственно в модели создает путаницу в том, что является «настоящими» данными модели (и хранится в базе), а что требуется только для представления. Объект-представление работает как адаптер между «настоящими» данными модели и представлением этих данных.

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

Объект-представление «одевает» данные модели, трансформируя, добавляя или удаляя значения, и возвращая новый объект для использования в слое представления. Этот подход создает хорошее место для логики и атрибутов, связанных с представлением и не смешивает их со свойствами модели.

Я хочу отметить, что существуют различные мнения о том, что именно подразумевается под этим паттерном. Helmkamp упоминает об этом в оригинальной статье, да и у нас в Crush & Lovely это частая тема для споров — наши инженеры предпочитают не употреблять название «объект-представление», в основном, потому что обычно под представлением имеют в виду HTML, а этот паттерн может использоваться и для формирования ответа API, и для передачи данных внешним сервисам и любыми другим способами. Мы предпочитаем название «Presenter», в первую очередь потому что это имя хорошо отражает суть паттерна — формирование представления данных, вне зависимости от их конечной формы.

Пример

К примеру, возмем конец года, когда преподаватели печатают отчеты по каждому студенту. Кроме различной информации, отчет содержит среднюю оценку студента, информацию о том, переведен ли он на следующий курс, и его номер телефона.

Скрипт для генерации отчетов находит каждого студента и оценки которые он получал в течение года, создавая «настоящее» представление объекта:

{
  "id": "123456",
  "firstName": "Susan",
  "lastName": "Smith",
  "gender": "f",
  "phone": "5551234567",
  "assignments": [
    {
      "grade": 0.65
    },
    {
      "grade": 0.83
    },
    {
      "grade": 0.90
    },
    ...
  ]
}

PDF-верстка отчета «глупа» и ничего не знает о форматировании данных:

...
<p class="average-grade">Average grade across all assignments: </p>
<p class="passing-status">Passing: </p>
...
<p>
  For any questions, please call the teacher at .
</p>
...

Формирование данных студента для представления станет проще если мы воспользуемся объектом-представлением, который, после создания, передадим прямо в шаблон. Вот пример того, как должен выглядеть объект-представление «одевающий» модели наших студентов для использования в шаблоне:

var _ = require('underscore');
var DetermineStudentPassingStatus = require('./determineStudentPassingStatus');
var GetAverageGradeFromAssignments = require('./getAverageGradeFromAssignments');

var StudentGradeReportPresenter = function( students ) {
  // ensure students variable is array
  this.students = ( students instanceof Array ) ? students : [students];
};

StudentGradeReportPresenter.whitelistKeys = [
  'firstName',
  'lastName',
  'isPassing',
  'averageGrade',
  'phone'
];

StudentGradeReportPresenter.prototype = _.extend( StudentGradeReportPresenter.prototype, {

  present: function() {
    var process = _.compose(
      this.sanitizeAttributes.bind( this ),
      this.getAverageGrade.bind( this ),
      this.getPassingStatus.bind( this ),
      this.formatPhoneNumber.bind( this )
    );

    this.result = _( this.students ).map( process );

    // return same type of primitive that was passed in
    // either Array or single object
    return ( this.students.length > 1 ) ? this.result : this.result[0];
  },

  sanitizeAttributes: function( student ) {
    student = _.pick.apply( null, [student].concat( StudentGradeReportPresenter.whitelistKeys ));
    return student;
  },

  getAverageGrade: function( student ) {
    student.isPassing = new DetermineStudentPassingStatus( student.id ).fromAssignments( student.assignments );
    return student;
  },

  getPassingStatus: function( student ) {
    student.averageGrade = new GetAverageGradeFromAssignments( student.assignments ).run();
    return student;
  },

  formatPhoneNumber: function( student ) {
    student.phone = student.phone.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3");
    return student;
  }

});

module.exports = StudentGradeReportPresenter;

Так что, все, что нужно сделать — взять данные студентов и передать их в представление для рендеринга:

new CurrentStudentsWithAssignmentsQuery().run()
  .then(function( students ) {
    return new StudentGradeReportPresenter( students ).present();
  })
  .then(function( students ) {
    res.render('reportCard', students);
  });

Нужно отметить отилчную возможность извлечь часто используемые метобы объекта-представления, такие как форматирование телефонного номера и вынести их в модули или хелперы, которые могут быть использованы в других объектах-представления, делая ваше приложение более модульным. Возможности для организации кода при использовании объектов-представлений открыты и гибки, так что каждый инженер должен взглянуть на свой собственный стиль, для того, чтобы получить лучший результат.

Тестирование

Юнит-тесты для проверки изменений данных на удивление прямолинейны, так как все, что вам нужно сделаеть — передать сквозь объект-представление один объект или массив а затем получить другой на выходе. Остается только проверить корректрость обработанных данных.

var expect = require('chai').expect;
var StudentGradeReportPresenter = require('./studentGradeReportPresenter');
var Grade = require('./grade');

describe('StudentGradeReportPresenter', function(){
  var student;
  var presentedStudent;

  before(function(){
    student = {
      id: '123456',
      firstName: 'Susan',
      lastName: 'Smith',
      gender: 'f',
      phone: "5551234567",
      assignments: [
        {
          grade: new Grade(0.65)
        },
        {
          grade: new Grade(0.83)
        },
        {
          grade: new Grade(0.90)
        }
      ]
    };

    presentedStudent = new StudentGradeReportPresenter( student ).present();
  });

  it('returns only the specified properties', function(){
    expect( presentedStudent ).to.have.keys('firstName', 'lastName', 'phone', 'averageGrade', 'isPassing');
  });

  describe('.phone', function(){

    it('returns the correct value', function() {
      expect( presentedStudent.phone ).to.equal('555-123-4567');
    });

  });

  describe('.isPassing', function(){

    it('returns the correct value', function() {
      expect( presentedStudent.isPassing ).to.equal(true);
    });

  });

  describe('.averageGrade', function(){

    it('returns the correct value', function(){
      expect( presentedStudent.averageGrade ).to.equal(0.79);
    });

  });

});

В следующем посте мы рассмотрим объекты-политики, которые представляют отличные инструменты для инкапсуляции бизнес-логики.

фото: shuttermanic

Подписывайтесь на РСС. Всем добра!

«Как рушатся комплексные системы», Ричард И. Кук
О фундаментальных проблемах больших запутанных систем
7 паттернов для рефакторинга JavaScript-приложений
Перевод отличной серии статей о проектировании и рефакторинге проектов
Музыка для работы
Мои плейлисты: теплый glitch, нежные девичьи голоса, интересная электроника и chillwave
Ссылколог
Коллекционирую полезные ссылки