/ JavaScript

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

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


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

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

樣式

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

Service Objects

Service Objects是一種物件,執行個別的操作或動作。當一個程式變得複雜,難以測試,或接觸超過一個以上的模型型別,Service Objects可以巧妙的整理你的程式碼庫。

Service Objects的目的是隔離個別的操作,應該具有以下幾項原則:

  • 嚴格輸入及輸出. Service Objects 應該設計成處理一種特定的程序,讓我們可以先行 穩健性原則,有利於建立一種工具,用於特別個別的意圖。
  • 徹底的文件化. 這個模組在執行後完全被取出,因此,物件更需要明確的意圖及良好的說明。
  • 操作完成後終結. 此樣式不可與工作程序融合,操作可能會被設置間隔、持續地監聽Web Socket訊息、或其它不會立即結束的動作。Service Objects應該被調用、執行其直接操作(無論同步或非同步)並結束。

舉例

撰寫一支程式,用來給老師在年末時能夠對學生進行評分,決定是否能夠通過。程式取得所有作業進行評估,找出百分比平均值,接著給予學生分級。
(determineStudentPassingStatus.js)

var _ = require('underscore');
 
var DetermineStudentPassingStatus = function( student ) {
  this.student = student;
}
 
DetermineStudentPassingStatus.prototype = _.extend( DetermineStudentPassingStatus.prototype, {
 
  minimumPassingPercentage: 0.6,
 
  fromAssignments: function( assignments ) {
    return _.compose(
      this.determinePassingStatus.bind( this ),
      this.averageAssignmentGrade,
      this.extractAssignmentGrades
    )( assignments );
  },
 
  extractAssignmentGrades: function( assignments ) {
    return _.pluck( assignments, 'grade' );
  },
 
  averageAssignmentGrade: function( grades ) {
    return grades.reduce( function( memo, grade ) {
      return memo + grade.percentage;
    }, 0) / grades.length;
  },
 
  determinePassingStatus: function( averageGrade ) {
    return averageGrade >= this.minimumPassingPercentage;
  }
 
});
 
module.exports = DetermineStudentPassingStatus;

透過取出邏輯到單一模組中,我們提供一個集中化的地方,讓未來很容易的與操作掛上掛勾。例如,如果當學生沒有通過,發送一封email給家長,我們可以很容易加到物件的工作流程中,透過增加另一個方法或者,更好的方式,使用另一個Service Object。

測試

即使操作發展的越來越複雜,測試案例仍然可以保持專注在單一操作上,避免肥大的測試檔案及笨重的環境準備。
(determineStudentPassingStatus.test.js)

var expect = require('chai').expect;
var DetermineStudentPassingStatus = require('./determineStudentPassingStatus');
var Grade = require('./grade');
 
describe('DetermineStudentPassingStatus', function(){
  var student = {};
  var assignments = [
    {grade: new Grade(0.5)},
    {grade: new Grade(0.8)},
    {grade: new Grade(0.9)},
    {grade: new Grade(0.6)},
  ];
  var determineStudentPassingStatus = new DetermineStudentPassingStatus( student );
 
  describe('#extractAssignmentGrades', function(){
    
    it('returns an array of grade value objects', function(){
      var grades = determineStudentPassingStatus.extractAssignmentGrades( assignments );
      expect( grades[0] ).to.be.an.instanceof( Grade );
    });
 
  });
 
  describe('#averageAssignmentGrade', function(){
    
    it('returns the average of all of the grades', function(){
      var grades = determineStudentPassingStatus.extractAssignmentGrades( assignments );
      var averageGrade = determineStudentPassingStatus.averageAssignmentGrade( grades );
      expect( averageGrade ).to.equal( ( (0.5 + 0.8 + 0.9 + 0.6) / 4 ) );
    });
 
  });
 
  describe('#determinePassingStatus', function(){
    
    it('returns the average of all of the grades', function(){
      var grades = determineStudentPassingStatus.extractAssignmentGrades( assignments );
      var averageGrade = determineStudentPassingStatus.averageAssignmentGrade( grades );
      var passing = determineStudentPassingStatus.determinePassingStatus( averageGrade );
      expect( passing ).to.be.true;
    });
 
  });
 
  describe('#fromAssignments', function(){
    var passing;
 
    it('returns the correct passing state', function(){
      passing = determineStudentPassingStatus.fromAssignments( assignments );
      expect( passing ).to.be.true;
 
      // overwrite to test false return
      assignments = [
        {grade: new Grade(0.5)},
        {grade: new Grade(0.4)},
        {grade: new Grade(0.8)},
        {grade: new Grade(0.6)},
      ];
      passing = determineStudentPassingStatus.fromAssignments( assignments );
      expect( passing ).to.be.false;
    });
    
  });
 
});

Service Objects 可以是一種極致寶貴的工具,用來整理並重構你的程式碼。隔離操作讓邏輯保持整潔、乾淨、容易測試及最終帶領到成為更加容易維護的程式庫。


接下來的文章,我們將看看Form Objects,一個能夠達到表單驗證、更輕鬆的持續性及特定文本等特性。

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