РСС

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

Меня зовут Антон Шувалов. Я работаю в 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-приложений

Объект-значение

В статье Brayan’а объект-значение описывается как «простой объект, который сравнивается по значению, а не по идентификатору». Поскольку в JavaScript все объекты передаются по ссылке, подобных примеров, за исключением примитивов, нет ни в ECMAScript 5, ни даже в Harmony. Например:

var foo = new Number( 2 );
  var bar = new Number( 2 );
  foo === bar; // => false

В этом примере примитивы сохраняются в переменные foo и bar, которые равны по значению, но, технически, эти примитивы являются объектами. Конструктор Number, не смотря на то, что он создает примитивный элемент, создаст «простой JavaScript-Object в старом стиле» (POJO), потому переменные сравниваются по ссылке, а не по значению и не будут равны между собой, хотя имеют одинаковое значение.

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

Пример

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

var _ = require('underscore');
 
var Grade = function( percentage ) {
  this.percentage = percentage;
  this.grade = this.grade( percentage );
};
 
Grade.prototype = _.extend( Grade.prototype, {
 
  grades: [
    { letter: 'A', minimumPercentage: 0.9, passing: true },
    { letter: 'B', minimumPercentage: 0.8, passing: true },
    { letter: 'C', minimumPercentage: 0.7, passing: true },
    { letter: 'D', minimumPercentage: 0.6, passing: true },
    { letter: 'F', minimumPercentage: 0,   passing: false }
  ],
 
  passingGradeLetters: function() {
    return _.chain( this.grades ).where({ passing: true }).pluck('letter').value();
  },
 
  grade: function( percentage ) {
    return _.find( this.grades, function( grade ) { return percentage >= grade.minimumPercentage; });
  },
 
  letterGrade: function() {
    return this.grade.letter;
  },
 
  isPassing: function() {
    return this.grade.passing
  },
 
  isImprovementFrom: function( grade ) {
    return this.isBetterThan( grade );
  },
 
  isBetterThan: function( grade ) {
    return this.percentage > grade.percentage;
  },
 
  valueOf: function() {
    return this.percentage;
  }
 
});
 
module.exports = Grade;

Подобная структура дает дополнительное преимущество, делая ваш код более выразительным. К примеру, теперь можно пистать так:

var firstStudent = { grade: new Grade(0.45) };
var secondStudent = { grade: new Grade(0.70) };
 
firstStudent.grade.isPassing() //=> false
firstStudent.grade.isBetterThan( secondStudent.grade ); //=> false

Перед тем как интегрировать объекты-значения в приложение, стоит отметить несколько вещей:

var myGrade = new Grade(0.65);
alert('My Grade is ' + myGrade + '!'); // alerts, 'My Grade is 0.65!'
 
var myOtherGrade = new Grade(0.75);
myGrade < myOtherGrade; // true

К сожалению, даже если два различных объекта возвращают одинаковое значение с помощью valueOf, они все равно не будут равны. - При конвертировании объекта-значения с помощью JSON.stringify, согласно конвенции, используется toJSON-метод, возвращающий значение, которое будет использовано при конвертировании в строку. Если же метод toJSON не определен, то будет использован valueOf. Если valueOf нет — объект будет конвертирован как Object, что, зачастую, не желательно.

Grade.equal = function( grade1, grade2 ) {
		  return grade1.valueOf() === grade2.valueOf();
		}
 
		var myFirstGrade = new Grade( 0.7 );
		var mySecondGrade = new Grade( 0.7 );
		Grade.equal( myFirstGrade, mySecondGrade ) // => true

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

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

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

var Grade = require('./grade');
var grade1;
var grade2;
 
describe('Grade', function() {
 
  describe('#isPassing', function() {
 
    it('returns true if grade is passing', function() {
      grade1 = new Grade(0.8);
      expect(grade1.isPassing()).to.be.true;
    });
 
    it('returns false if grade is not passing', function() {
      grade1 = new Grade(0.58);
      expect(grade1.isPassing()).to.be.false;
    })
 
  });
 
  describe('#letterGrade', function() {
 
    it('returns correct letter for percentage', function() {
      grade1 = new Grade(0.8);
      expect(grade1.letterGrade()).to.equal('B');
    });
 
    it('returns A for 100 percent', function() {
      grade1 = new Grade(1);
      expect(grade1.letterGrade()).to.equal('A');
    });
 
    it('returns F for 0 percent', function() {
      grade1 = new Grade(0);
      expect(grade1.letterGrade()).to.equal('F');
    });
 
    it('returns F for anything lower than 0.6', function() {
      grade1 = new Grade(0.4);
      expect(grade1.letterGrade()).to.equal('F');
    });
 
  });
 
  describe('#passingGradeLetters', function() {
 
    it('returns all passing letters', function() {
      grade1 = new Grade(0.8);
      expect(grade1.passingGradeLetters()).to.have.members(['A', 'B', 'C', 'D']);
    });
 
  });
 
  describe('#isImprovementFrom', function() {
 
    it('returns true if grade is better than comparison grade', function() {
      grade1 = new Grade(0.8);
      grade2 = new Grade(0.7);
      expect(grade1.isImprovementFrom( grade2 )).to.be.true;
    });
 
    it('returns false if grades are equal', function() {
      grade1 = new Grade(0.7);
      grade2 = new Grade(0.7);
      expect(grade1.isImprovementFrom( grade2 )).to.be.false;
    });
 
  });
 
  describe('#isBetterThan', function(){
 
    it('returns true if grade is better than comparison grade', function() {
      grade1 = new Grade(0.8);
      grade2 = new Grade(0.7);
      expect(grade1.isImprovementFrom( grade2 )).to.be.true;
    });
 
    it('returns false if grades are equal', function() {
      grade1 = new Grade(0.7);
      grade2 = new Grade(0.7);
      expect(grade1.isImprovementFrom( grade2 )).to.be.false;
    });
 
  });
 
});

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


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

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

фото: shuttermanic

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