การรับค่าแบบ Synchronous บน Node.js

ในบทของการรับค่า คุณได้เรียนรู้การรับค่าผ่านทางคีย์บอร์ดบน Node.js โดยการใช้เมธอดจากโมดูล readline ซึ่งมีการทำงานเป็นแบบ Asynchronous ที่ต้องการฟังก์ชัน Callback เพื่อทำงานเมื่อการรับค่าเสร็จสิ้น

ในบทนี้ เราจะพูดถึงเทคนิคการเขียนโปรแกรมเพื่อให้รับค่าแบบ Synchronous บน Node.js โดยการแปลงการทำงานของเมธอดให้เป็น Promise และใช้มันร่วมกับคำสั่ง Async/await ซึ่งจะสามารถช่วยให้การรับค่าในการเขียนโปรแกรมทำได้ง่ายขึ้น นี่เป็นเนื้อหาในบทนี้

  • การใช้ Callback function
  • การใช้ Promise
  • การใช้คำสั่ง Async/Await
  • การรับค่าด้วยแพ็จเกจ readline-sync

การใช้ Callback function

แม้ว่า Callback จะสามารถทำงานได้อย่างไม่มีปัญหา แต่การซ้อนกันของฟังก์ชัน Callback เป็นจำนวนมากอาจไม่ใช่วิธีที่ดีในการเขียนโปรแกรม เนื่องจากนั่นจะทำให้โค้ดของไม่เป็นระเบียบและยากต่อการอ่าน ซึ่งนี่สามารถเกิดขึ้นได้ ในกรณีที่เราต้องการรับค่าอื่นหลังจากที่การรับค่าก่อนหน้าเสร็จสิ้นแล้ว ลองดูตัวอย่างต่อไปนี้

getting_input.js
const readline = require('readline');

const readInterface = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

readInterface.question('Enter first number: ', first => {
    console.log(`You entered ${first}`);

    readInterface.question('Enter second number: ', second => {
        console.log(`You entered ${second}`);
        let sum = Number(first) + Number(second);
        console.log(`${sum} = ${first} + ${second}`);
        readInterface.close();
    });
});

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

Enter first number: 3
You entered 3
Enter second number: 2
You entered 2
5 = 3 + 2

ในตัวอย่าง เป็นโปรแกรมสำหรับรับค่าตัวเลขสองตัวผ่านทางคีย์บอร์ดเพื่อนำมาหาผลรวมและแสดงผลรวมออกทางหน้าจอ จะเห็นว่าเพื่อรับค่าเอาตัวเลขตัวที่สอง เราจะต้องทำมันหลังจากการรับค่าตัวแรกเสร็จก่อน เพื่อทำเช่นนี้เราใช้เมธอด question ในฟังก์ชัน Callback ของการรับค่าครั้งแรก

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

readInterface.question('Enter first number: ', first => {
    readInterface.question('Enter second number: ', second => {
        readInterface.question('Enter third number: ', third => {
            ...
        });
    });
});

การซ้อนกันของฟังก์ชัน Callback ในลักษณะนี้เรียกว่า Callback hell หรือ Pyramid of Doom แม้ว่ามันจะสามารถทำงานได้อย่างไม่มีปัญหา แต่การเขียนในลักษณะนี้อาจทำให้โค้ดซับซ้อนและยากต่อการทำความเข้าใจ

และเพื่อแก้ไขปัญหาการซ้อนกันของฟังก์ชัน Callback เราสามารถแปลงเมธอดของการรับค่าให้เป็น Promise และจากนั้นใช้มันร่วมกับคำสั่ง Async/Await เพื่อทำให้การรับค่าของโปรแกรมเป็นแบบ Synchronous หรือ Blocking

การใช้ Promise

จากตัวอย่างก่อนหน้า เพื่อลดการซ้อนกันของฟังก์ชัน Callback เราสามารถแปลงโค้ดให้ทำในรูปแบบของ Promise ซึ่งนี่เรียกว่า Promisifying โดยเราจะต้องสร้างฟังก์ชันที่ส่งกลับออบเจ็คของ Promise ที่ resolve ค่าที่ได้รับจากคีย์บอร์ดเมื่อผู้ใช้งานกดปุ่ม Enter นี่เป็นวิธีการแปลงการทำงานของเมธอด question ให้เป็น Promise

getting_input_promise.js
const readline = require('readline');

const readInterface = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

function readLine(text) {
    return new Promise((resolve, reject) => {
        readInterface.question(text, input => {
            resolve(input);
        });
    })
}

let first, second, third;

readLine('Enter first number: ').then((value) => {
    first = value;
    return readLine('Enter second number: ');
}).then((value) => {
    second = value;
    return readLine('Enter third number: ');
}).then((value) => {
    third = value;
    readInterface.close();
    console.log(
        'You have entered three numbers: %s',
        [first, second, third].join(', ')
    );
});

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

Enter first number: 1
Enter second number: 2
Enter third number: 3
You have entered three numbers: 1, 2, 3

ในตัวอย่างนี้ เป็นการแปลงการทำงานของเมธอด question ให้เป็น Promise โดยการสร้างฟังก์ชันใหม่ที่ชื่อว่า readLine ที่ส่งค่ากลับเป็นออบเจ็คของ Promise ที่ resolve ค่าที่รับได้เมื่อการรับค่าเสร็จสิ้น

Promise เป็นอีกรูปแบบหนึ่งของการเขียนโปรแกรมที่ให้ทำงานแบบ Asynchronous ที่สามารถช่วยลดปัญหาการซ้อนกันของฟังก์ชัน Callback ได้ เนื่องจากกว่า Promise สามารถเชื่อมต่อกันได้หรือเรียกว่า Promise chaining

function readLine(text) {
    return new Promise((resolve, reject) => {
        readInterface.question(text, input => {
            resolve(input);
        });
    })
}

ในการสร้างออบเจ็ค Promise เราจะเขียนโค้ดที่ทำงานแบบ Asynchronous ในฟังก์ชัน Callback ของคลาสคอนสตรัคเตอร์ที่ส่งสองพารามิเตอร์ฟังก์ชัน resolve และ reject เพื่อให้เราสามารถนำมาใช้งานสำหรับเสร็จสิ้นการทำงานของ Promise

resolve(input);

นั่นหมายความว่าเมื่อฟังก์ชัน resolve ถูกเรียกใช้งาน หมายถึง Promise เสร็จสิ้นการทำงานแบบสำเร็จ (resolve) และในกรณีที่การทำงานล้มเหลว (reject) เราสามารถเรียกฟังก์ชัน reject ได้ แต่เราไม่ได้ใช้มันในตัวอย่างนี้

readLine('Enter first number: ').then((value) => {
    first = value;
    return readLine('Enter second number: ');
}).then((value) => {
    second = value;
    return readLine('Enter third number: ');
}).then((value) => {
    third = value;
    readInterface.close();
    console.log(
        'You have entered three numbers: %s',
        [first, second, third].join(', ')
    );
});

จากนั้นในการใช้งานฟังก์ชัน readLine ซึ่งมีการทำงานเป็นแบบ Promise สังเกตว่าเราไม่ต้องส่งฟังก์ชัน Callback ไปในตอนเรียกฟังก์ชันแล้ว แต่เราเรียกใช้งานเมธอด then ของ Promise เพิื่อรับค่าที่ส่งกลับ (resolve) มาจาก Promise แทน

ในเมธอด then เราสามารถส่งค่ากลับเป็นออบเจ็คของ Promise เพื่อใช้ในการเชื่อมต่อการทำงานของ Promise หรือที่เรียกกันว่า Promise chaining ได้ จะเห็นว่าในตอนนี้ โค้ดของเราไม่มีการซ้อนกันของฟังก์ชัน Callback แล้ว และทำให้มันสามารถอ่านได้ง่ายขึ้น

มากไปกว่านั้น เนื่องจากฟังก์ชัน readLine มีการทำงานในรูปแบบของ Promise เราสามารถใช้มันร่วมกับคำสั่ง Async/Await เพื่อทำให้การรับค่าเป็นแบบ Synchronous หรือ Blocking ได้ นี่จะทำให้เราสามารถหลีกเลี่ยงการใช้ฟังก์ชัน Callback ได้อย่างสมบูรณ์ ดังนั้นเราลองมาปรับปรุงโค้ดอีกครั้ง

การใช้คำสั่ง Async/await

หลังจากที่เราสร้างฟังก์ชัน readLine จากในตัวอย่างก่อนหน้าที่ทำงานในรูปแบบของ Promise แล้ว เราสามารถนำมันมาใช้ร่วมกับคำสั่ง Async/awaitเพื่อทำให้การรับค่าเป็นแบบ Synchronous ได้ นี่เป็นตัวอย่างที่มีการรับค่าเป็นลำดับเหมือนกับในตัวอย่างก่อนหน้า แต่ในครั้งนี้เราใช้คำสั่ง Async/await แทน

synchronous_getting_input.js
const readline = require('readline');

const readInterface = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

function readLine(text) {
    return new Promise((resolve, reject) => {
        readInterface.question(text, input => {
            resolve(input);
        });
    })
}

async function main() {
    let first = await readLine('Enter first number: ');
    console.log(`You entered ${first}`);

    let second = await readLine('Enter second number :');
    console.log(`You entered ${second}`);

    let third = await readLine('Enter third number: ');
    console.log(`You entered ${third}`);

    console.log(
        'You have entered three numbers: %s',
        [first, second, third].join(', ')
    );
    readInterface.close();
}

main();

นี่เป็นผลลัพธ์การทำงานของโปรแกรม เมื่อเรากรอกค่านำเข้าเป็น 1, 2 และ 3 ตามลำดับ

Enter first number: 1
You entered 1
Enter second number :2
You entered 2
Enter third number: 3
You entered 3
You have entered three numbers: 1, 2, 3

ในตัวอย่าง เป็นการใช้งานฟังก์ชัน readLine สำหรับการรับค่าแบบ Synchronous ร่วมกับคำสั่ง Async/Await สิ่งแรกที่คุณส่ังเกตเห็นในตัวอย่างก็คือเราได้สร้างฟังก์ชัน main ซึ่งเป็นฟังก์ชันแบบ async เพื่อให้สามารถใช้งานคำสั่ง await ได้

หรือกล่าวอีกนัยหนึ่งเพื่อใช้งานคำสั่ง await มันจะต้องถูกใช้ในขอบเขตของฟังก์ชันแบบ async เท่านั้น ในกรณีนี้คือฟังก์ชัน main

let first = await readLine('Enter first number: ');

หลังจากนั้นในการรับค่าด้วยฟังก์ชัน readLine เราสามารถใช้คำสั่ง await นำหน้าฟังก์ชันได้ โดยการใช้คำสั่ง await หน้าฟังก์ชันที่ส่งค่ากลับเป็น Promise ออบเจ็ค นั่นจะทำให้การทำงานของคำสั่งนั้นเป็นแบบ Synchronous และบล็อคจนกว่า Promise จะส่งค่ากลับ (resolve)

let first = await readLine('Enter first number: ');
console.log(`You entered ${first}`);

let second = await readLine('Enter second number :');
console.log(`You entered ${second}`);

let third = await readLine('Enter third number: ');
console.log(`You entered ${third}`);

นี่จะทำให้เราสามารถรับค่าแบบหลายค่าเป็นแบบตามลำดับได้โดยที่ไม่ต้องซ้อนฟังก์ชัน Callback หรือ Promise chaining เหมือนกับในตัวอย่างก่อนหน้า ซึ่งทำให้โค้ดของเราอ่านและทำความเข้าใจได้ง่าย เพราะการรับค่าทั้งหมดอยู่ระดับเดียวกัน

readInterface.close();

เมื่อสิ้นสุดการรับค่าแล้ว คุณต้องไม่ลืมที่จะปิด I/O Stream สำหรับการรับค่าและการแสดงผลโดยการเรียกใช้เมธอด readInterface.close

การรับค่าด้วยแพ็จเกจ readline-sync

ในตัวอย่างที่ผ่านมาคุณได้เรียนรู้การใช้งานเมธอดจากโมดูล readline ที่ทำงานในรูปแบบ Asynchronous และเราเปลี่ยนการทำงานของมันให้เป็น Promise และ Synchronous ด้วยการใช้คำสั่ง Async/Await ในภาษา JavaScript นี่เป็นวิธีที่ดีในการเรียนรู้การทำงานของฟังก์ชันสำหรับรับค่าบน Node.js

อย่างไรก็ตาม สำหรับการรับค่าแบบ Synchronous คุณสามารถใช้โมดูล readline-sync จาก NPM ได้ ซึ่งมันใช้สำหรับรับค่าผ่านทาง Command line เช่นเดียวกันกับโมดูล readline ของ Node.js แต่มันทำงานเป็นแบบ Synchronous แทน เพื่อใช้งานโมดูล นี้คุณจะต้องติดตั้งมันผ่าน npm ด้วยคำสั่ง

npm install readline-sync

จากนั้นสามารถนำเข้าโมดูลเพื่อใช้มันในโปรแกรมของคุณ นี่เป็นตัวอย่าง

readline_sync.js
var readlineSync = require('readline-sync');

// Wait for user's response.
var userName = readlineSync.question('May I have your name? ');
console.log('Hi ' + userName + '!');

สำหรับรายละเอียดและวิธีการใช้งานเพิ่มเติมของโมดูลนี้ คุณสามารถอ่านได้ที่ https://www.npmjs.com/package/readline-sync

ในบทนี้ คุณได้เรียนรู้เกี่ยวกับการรับค่าผ่านทาง Command line บน Node.js ในรูปแบบ Synchronous ด้วยโมดูลมาตรฐาน readline โดยการประยุกต์ใช้ร่วมกับ Promise และคำสั่ง Async/Await เพื่อหลีกเลี่ยงการใช้ฟังก์ชัน Callback ซ้อนกันเป็นจำนวนมาก และนี่จะทำให้โค้ดเป็นระเบียนและอ่านง่ายมากขึ้น