/ JavaScript

7種重構JavaScript程式的樣式: VALUE OBJECTS

Original article: 7 PATTERNS TO REFACTOR JAVASCRIPT APPLICATIONS: VALUE OBJECTS By Michael Phillips.


2013年10月17日,Code Climate創辦人Bryan Helmkamp寫了一篇文章,描述了7種樣式,用來重構Ruby on Rails裡肥胖的ActiveRecord模型。在Crush & Lovely裡,這是給所有Rails開發者的一篇核心參考文章,學習如何切割關係並撰寫模組化,簡明及具表達性的程式碼,也讓測試變得非常簡單。

本系列文章會在JavaScript環境中展示這些概念;這些概念同樣適用於JavaScript中的資料模型,也相同重要。每一周會介紹一種樣式,總共會介紹七種。這周,我們將談談Value Objects。

樣式

  • Value Objects
  • Service Objects
  • Form Objects
  • Query Objects
  • View Objects
  • Policy Objects
  • Decorators

Value Objects

Bryan的文章對Value Objects的描述是“...一個簡單的物件,相等是依賴其值,而不是本身的識別”,因為在JavaScript所有的物件都附著在“參考傳遞”原則之上,所以在ECMAScript 5或者Harmony都沒有原生範例可參考。舉例如下 (valueObjects1.js):

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

第一個範例裡,指定基本的整數到foobar這兩個變數之中,兩者的值是相同的,即使在JavaScript技術上來說,整數也是個物件。Number建構子,即使包裝了基本整數,也屬於一種Plain Old JavaScript Object,在參考上可以說是相等的。因此,在這個範例中foobar並不相等,即使這兩個Number的實例,呈現相同的整數值。

Value Objects提供了一個屬於領域邏輯的好地方。在你的應用程式裡,幾乎每個值都有相關連的邏輯,例如等號,邏輯的最好地方在value object的實例。

舉例

考慮一種學生分級的應用程式,學生集合成績的比例,用來指定分級字母並決定是否合格或需要再進步。
(grade.js)

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.letterGrade();
  }
 
});
 
module.exports = Grade;

這加入了讓你的程式碼更具表達性的好處,讓你可以撰寫如下的程式碼 (valueObjects2.js):

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

整合Value Objects到程式中,幾件事情提醒:

  • ECMAScript規格中,valueOftoString方法具有特殊意義,建議所有自訂的Value Objects都實作這兩個方法。使用上述的Grade物件,我們讓它符合ECMAScript語法標準,定義了valueOf方法,給定下述範例 (valueObjects3.js):
var myGrade = new Grade(0.65);
alert('My Grade is ' + myGrade + '!'); // alerts, 'My Grade is D!'
 
var myOtherGrade = new Grade(0.75);
myGrade < myOtherGrade; // true

兩個分開的物件,就算valueOf方法傳回相同的值,使用===評估時兩者仍然是不相等的。
(valueObjects4.js )

var myGrade = new Grade(0.65);
var myOtherGrade = new Grade(0.65);
myGrade === myOtherGrade; // false
  • 為了讓你的Value Object能夠透過JSON.stringify進行轉換,慣用方式是實作toJSON方法,傳回你想要轉換的字串。如果沒有指明toJSON方法,JSON.stringify將會評估valueOf方法。如果沒有定義valueOf方法,會視為物件進行評估,結果通常不會是我們想要的。
  • 實作valueOf方法,傳回與物件初始化時相同的值,是一種好的榜樣,這樣你就可以在別的地方重建物件。當應用程式同時有客戶端及伺服端並共用Value Objects時相當有用,你可以在伺服端使用Value Object,然後利用valueOf將值傳送到客戶端,接著在客戶端重建它。
  • 如果你比較喜歡函數編程的Value Objects,你可以在建構子增加方法,來取代在protrtype中增加。考慮下列範例 (valueObjects5.js):
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

物件導向方法或函數編程方法都有效,取決於你個人風格。

測試

因為將邏輯集中在一個物件之上,測試變得更加容易且快速,讓一個小的測試案例就能夠涵蓋到許多程式邏輯。考慮下列測試範例:
(grade.test.js)

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;
    });
 
  });
 
});

測試Value Object的一個好處是測試的配置非常簡單。測試多種組合非常快速且有效,讓你避免於建構假的模型或撰寫複雜的邏輯。另外,在任何模型測試中整個邏輯是孤立的,所以測試案例更小且更專注。


接下來的文章,我們將談到Service Objects,隔離程序上程式碼的好工具。

特別感謝Justin Reidy給予寫作及程式碼複審。