NodeJS ตอนที่ 8 [การเขียน Asynchronous ด้วย Promise]

จากปัญหา Callback Hell ก็มีไลบรารี่ที่แก้ปัญหาความสวยงามของการเขียนโค้ดแบบ callback ของ JavaScript ออกมาหลายตัว ตัวที่น่าสนใจอีกตัวที่ผมชอบใช้ก็คือ async ซึ่งก็มีความสามารถมากมายสามารถกำหนดลำดับการทำงานแบบต่างๆได้มากมายไม่ว่าจะเป็น waterfall,series ,parallel,etc  แต่ก็ยังไม่ตอบโจทย์เรื่องความสวยงามของโค้ด ก็มีการสร้างไลบรารี่ Promise ขึ้นมา เพื่อให้เขียนโค้ด asynchronous แต่ synchronous เป็นลำดับได้ เอ่ะ งง รึเปล่า เอาเป็นว่า Promise จะช่วยให้เราเข้าใจลำดับการทำงานของ asynchronous ที่มีความต่อเนื่องกันได้ง่ายขึ้น จริงรึเปล่าต้องมาลองกันดู

พื้นฐาน Promise

การเขียนฟังก์ชั่นแบบ Promise จะมี สถานะพื้นฐานอยู่ 3 สถานะคือ

  • Pending > สถานะเริ่มต้นของการใช้ Promise
  • Fulfilled > สถานะที่บอกถึงการได้ผลลัพธ์ที่ถูกต้อง
  • Rejected > สถานะที่บอกถึงข้อผิดพลาด

* Promise ก็มีหลายคนพัฒนาออกมา ในแต่ละตัวก็อาจจะมีชื่อเรียกหรือ State ที่ต่างกันไป แต่โดยมาตรฐานวางโครงสร้างไว้ประมาณที่กล่าวมาข้างต้น

การเขียน Promise จะวางโครงสร้างโค้ด ดังนี้

function testPromise(a){
var deferred = Promise.pending();
if a == 1
  deferred.resolve(a);
else
  deferred.reject(a);
return defered.promise;
}

ใน Angular ก็มีไลบรารี่ Promise มาให้ด้วย ลักษณะการใช้งานก็จะคล้ายๆกันกับตัวอย่างโค้ดด้านบน แต่สถานะ pending จะเรียกชื่อต่างกัน คือ

var deferred = $q.defer();

ดังนั้นการเลือกใช้ Promise แต่ละตัวก็มีชื่อเรียกต่างๆกันไป ในตัวอย่างของ nodejs จะใช้ไลบรารี่ ชื่อ bluebird

หากยังไม่มีให้ทำการติดตั้ง

npm install bluebird

ขั้นตอน

  1. สร้างไฟล์ชื่อ ch8_promise.js
  2. ป้อนโค้ด ดังนี้
var Promise = require('bluebird');

function car(){
	var isStarted = false;

  this.start = function(){
		var deferred = Promise.pending();
		if(isStarted){
	        deferred.reject('Error:Already Started!');
	    }
	    else{
	    	isStarted = true;
	    	console.log('Started Engine');
	    	deferred.resolve('Started Engine');
	    }
	    return deferred.promise;
	}

	this.stop = function(){
		var deferred = Promise.pending();
		if(!isStarted){
	        deferred.reject('Error:Not Start yet!');
	    }
	    else{
	    	isStarted = false;
	    	console.log('Stopped Engine');
	    	deferred.resolve('Stopped Engine');
	    }
	    return deferred.promise;
	}

	this.forward=function(a){
    console.log('forward',a);
		var deferred = Promise.pending();

		if(!isStarted){
	        deferred.reject('Error:Not Start yet!');
	    }
	    else{
	    	isStarted = true;
	    	console.log('Go forward');
	    	deferred.resolve('Go forward');
	    }
	    return deferred.promise;
	}

	this.backward =function(){
		var deferred = Promise.pending();
		if(!isStarted){
	        deferred.reject('Error:Not Start yet!');
	    }
	    else{
	    	isStarted = true;
	    	console.log('Go backward');
	    	deferred.resolve('Go backward');
	    }
	    return deferred.promise;
	}

	this.turnleft=function(){
		var deferred = Promise.pending();
		if(!isStarted){
	        deferred.reject('Error:Not Start yet!');
	    }
	    else{
	    	isStarted = true;
	    	console.log('Turn left');
	    	deferred.resolve('Turn left');
	    }
	    return deferred.promise;
	}

	this.turnright=function(){
		var deferred = Promise.pending();
		if(!isStarted){
	        deferred.reject('Error:Not Start yet!');
	    }
	    else{
	    	isStarted = true;
	    	console.log('Turn right');
	    	deferred.resolve('Turn right');
	    }
	    return deferred.promise;
	}

}

var mycar = new car();
mycar.start()
.then(mycar.turnleft)
.then(mycar.turnright)
.then(mycar.backward)
.then(mycar.start).catch(function(e){
  console.log(e);
});

3. รันโปรแกรม

node ch8_promise.js
Started Engine
Turn left
Turn right
Go backward
Error:Already Started!

อธิบายโค้ด

ในโค้ดตัวอย่างนี้จะสร้าง ออบเจค car ซึ่งมีการเขียนโค้ดโดยใช้หลักการ OOP (Object Oriented Programming) คือจำลองโค้ดให้เหมือนวัตถุในโลกนี้ เพื่อใช้ในการสื่อสารความหมายให้เข้าใจพฤติกรรมการใช้งานโค้ดได้ง่ายขึ้น โดยส่วนตัวแล้วจะใช้เทคนิคออกแบบเหมือน grammar ภาษาอังกฤษ คือ

  • ชื่อคลาส = คำนาม
  • ฟังก์ชั่น = คำกริยา
  • property  = คำคุณศัพท์

ซึ่งในตัวอย่างจะมีฟังก์ชั่น ดังนี้

car
  .start
  .stop
  .forward
  .backward
  .turnleft
  .turnright

เป็นการจำลองว่า รถมีฟังก์ชั่น start เครื่อง ดับเครื่อง เลี่ยวซ้าย เลี้ยวขวา เดินหน้า ถอยหลัง เหมือนการทำงานทั่วไปของรถจริงๆ ถ้าสังเกตในโค้ดจะมีการวางลำดับเหมือนกันทุกฟังก์ชั่น คือ

var deferred = Promise.pending();
deferred.resolve(result);
deferred.reject(result);

ทั้งสามบรรทัดนั้นคือ 3 สถานะที่กล่าวมาแล้วข้างต้น

สำหรับ bluebird

  • resolve ก็คือ fulfilled จะคืนค่าได้ตัวแปรเดียว จะเป็น json หรือ value ใดก็ได้
  • reject ก็คือ rejected จะคืนค่าได้ตัวแปรเดียว จะเป็น json หรือ value ใดก็ได้

เมื่อนำออบเจค car ไปใช้งาน

var mycar = new car();
mycar.start()
.then(mycar.turnleft)
.then(mycar.turnright)
.then(mycar.backward)
.then(mycar.start).catch(function(e){
  console.log(e);
});

เมื่อ start แล้ว ต้องการรันฟังก์ชั่นอื่นต่อ ให้ใช้ then ก็จะเห็นว่าโค้ดดูมีลำดับในการทำงาน ทำให้สื่อสารให้โปรแกรมเมอร์คนอื่นๆทำความเข้าใจได้ง่ายขึ้น ค่อยเข้าไปอ่านรายละเอียดปลีกย่อยในโค้ดอีกทีภายหลังได้ ถ้าเข้าใจขั้นตอนการทำงานชัดเจน
ค่าที่คืนมาจาก resolve จะคืนมากับฟังก์ชั่น then ส่วนค่าที่คืนมาจาก reject จะคืนมาที่ฟังก์ชั่น catch
ในตัวอย่างจะเกิด error ขึ้นหากมีการสั่ง start อีกครั้งเนื่องจาก รถได้ทำการ start ขึ้นมาแล้ว จึงเกิด error
และทำการ log ออกมาที่ฟังก์ชั่น catch

mycar.start()
.then(mycar.turnleft)

หลังจาก then ของ start จะคืนค่า ‘Started Engine’ ใน then หากเขียนฟังก์ชั่น mycar.turnleft ก็จะได้รับค่า ‘Started Engine’ เข้าไป หากลองใส่โค้ดใน ฟังก์ชั่น turnleft ดังนี้

console.log(arguments);

ก็จะเห็นค่าของ ‘Started Engine’ ส่งเข้ามา

พบกับตอนที่ 9 การทำ Cluster