JavaScript Clean Coding Best Practices

บทความนี้สรุปจากเว็บ //blog.risingstack.com/javascript-clean-coding-best-practices-node-js-at-scale จะแปลคราวๆไว้นะครับ เผื่อให้ตัวเองอ่านและหวังว่าจะเป็นประโยชน์ต่อคนอื่นนะครับ โดยเราอาจจะเคยได้ยินเรื่องการเขียนโค้ดที่ดีหรือว่า clean code แต่มันคืออะไรกันล่ะ ? เราไปดูกันว่าเขาแนะนำอย่างไรกันครับ

อย่างแรกเลยคือ clean code คืออะไร ?

คือโค้ดที่คุณเขียนครั้งล่าสุดเพื่อตัวคุณเองในอนาคตและเพื่อนร่วมทีมคุณมาอ่านแล้วเข้าใจได้ง่าย ไม่ใช่ให้คอมอ่าน เพราะฉะนั้นเราควรจะทำให้มันอ่านง่ายทุกครั้งที่เรากำลังทำงานกับมัน ในบทความมีติดตลกว่า เกณฑ์ในการวัดว่า code เราอ่านง่ายขึ้นอยู่กับการสถบ WTF ต่อนาที ถ้าเป็นในไทยเราคงจะบ่นประมาณ

“เขียนเชี้ยอะไรวะเนี้ย !!!”

“โค้ดตรงนี้ทำไมต้องเขียนแบบนี้ฟ่ะ” หรือ

“ทำไมต้องเขียนแบบนี้ทั้งๆที่แม่งเหมือนกัน”

คราวนี้เรารู้แหละว่า clean code คืออะไรเราไปดูกันว่าเขาทำยังไงกันบ้างให้มัน clean code

ตั้งชื่อตัวแปรยังไงดี ?

ปัญหานี้เป็นกันทุกคนตั้งแต่เริ่มต้นยันแก่ตาย จะตั้งชื่อตัวแปรยังไงดี เขาบอกว่าให้ เอาสิ่งที่มันเป็นหรือความหมายตรงๆของมันเลย ไม่ต้องกังวลว่ามันจะยาวไหม ถ้าคุณทำตามนี้คุณจะค้นหามันง่ายด้วย และช่วยเวลาคุณต้อง refactor ในบางครั้งเรามาดูตัวอย่างกัน

// DON'T
let d
let elapsed
const ages = arr.map((i) => i.age)

// DO
let daysSinceModification
const agesOfUsers = users.map((user) => user.age)

สังเกตุตัวอย่างที่ไม่ควรทำนะครับ เขาประกาสตัวแปรชื่อ d เฉยๆเลยซึ่งมันทำให้เราต้องไปหาหรือเลื่อนจอไปหาอีกว่า d มันแทนอะไร Datetime หรือเปล่าหรือว่า decimal ที่เป็นค่าทศนิยม เห็นไหมครับว่ามันแปลได้หลายอย่างมากๆหรืออาจจะแค่เป็นตัวแปรลอยๆเพื่อมารับค่าเฉยๆ

แล้วมาดู ages ที่รับค่าอันนี้ไม่แย่แต่ก็ยังไม่ดีเท่าไร เขาเขียน ages เติม s ในสากลการเขียนโปรแกรมจะหมายความว่าเป็น array ซึ่ง ages นี้เราไม่รู้ว่ามันคืออายุของอะไร อาจจะอายุของบางอย่างหรืออายุของ user

ในตัวอย่างที่ดีนั้นเขาบอกเลยว่า daysSinceModification อันนี้อ่านแล้วเข้าใจได้ทันว่าคือตัวแปรสำหรับเก็บวันที่มีการเปลี่ยนแปลงล่าสุด ดูถัดมา agesOfUsers ก็ความหมายตรงตัว

เอาจริงๆมันยากที่จะคิดคำเพราะบางทีเราไม่ชินกับการใช้ภาษาอังกฤษอันนนี้ผมก็เข้าใจเพราะผมเองก็มีปัญหาบ่อยๆ อาจจะใช้คำยาวหน่อยแต่ถ้าเพื่อนในทีมอ่านแล้วเข้าใจก็โอเคครับ

เขาให้คำแนะนำต่ออีกว่า ควรให้ควาหมายแตกต่างอย่างชัดเจน และต้องไม่เติมคำนามอื่นๆที่ไม่สำคัญ ตัวอย่างเช่น

// DON'T
let nameString
let theUsers

// DO
let name
let users

อย่างตัวอย่างที่ไม่ควรทำเขาใช้คำว่า nameString ซึ่งเราจะรู้อยู่แล้วว่า name มันก็ควรจะเป็น String type หรืออีกตัวคือ theUsers อันนี้ก็ไม่ดีเพราะว่าไม่จำเป็นต้องเติม the เข้าไปครับ

ทำให้อ่านง่าย เพราะว่าในสมองเราจะใช้เวลาประมวลผลน้อยกว่าถ้ามันอ่านได้เลย

// DON'T
let fName, lName
let cntr

let full = false
if (cart.size > 100) {
  full = true
}

// DO
let firstName, lastName
let counter

const MAX_CART_SIZE = 100
// ...
const isFull = cart.size > MAX_CART_SIZE

fName มันก็ไม่แย่แต่ถ้าเป็นคำ firstName เลยมันก็ตรงๆตัวเลย อย่างคำว่า cntr อย่างนี้เราจะนึกไม่ออกต้องมาคิดอีกว่ามันอ่านว่าอะไร (วะ) แต่อันที่ควรทำก็ประกาศเลยว่า counter และเขาประกาศ MAX_CART_SIZE เป็นตัวใหญ่หมดอันนี้ก็สากลจะเข้าใจว่ามันคือค่า constant หรือค่าที่เราจะไม่เปลี่ยนแปลงเป็นค่าคงที่ตลอดนั่นเอง คือเราไม่ต้องเดาว่า ไอ้ 100 เนี้ยคืออะไร (วะ) ง่ายต่อการอ่านและค้นหาภายหลังครับ

ควรตั้งชื่อ function อย่างไร ?

function ควรทำงานเพียง 1 สิ่งก็พออย่ามากกว่านั้น เรามาดูตัวอย่างกันครับ

// DON'T
function getUserRouteHandler (req, res) {
  const { userId } = req.params
  // inline SQL query
  knex('user')
    .where({ id: userId })
    .first()
    .then((user) => res.json(user))
}

// DO
// User model (eg. models/user.js)
const tableName = 'user'
const User = {
  getOne (userId) {
    return knex(tableName)
      .where({ id: userId })
      .first()
  }
}


// route handler (eg. server/routes/user/get.js)
function getUserRouteHandler (req, res) {
  const { userId } = req.params
  User.getOne(userId)
    .then((user) => res.json(user))
}

ในตัวอย่างที่ไม่ควรทำ ใน function getUserRouteHandler รับค่า req, res ซึ่งหลายคนก็ทำแบบนี้ผมเองก็ทำบอกกันตรงๆ ซึ่งมันไม่ได้แย่ แต่เขาแนะนำว่าให้แยกการทำงานเป็นอย่างๆ โดยสร้าง file หรือ directory เก็บให้เรียบร้อยครับเช่น สร้าง folder models และสร้างไฟล์ user.js ข้างในก็ประกาศแยกกันไปว่าชื่อ tableName คืออะไร และประกาศ User ไว้สำหรับ export ออกไปใช้งานเวลาเรียกใช้เราก็เรียกใช้ User.getOne(id) ได้เลย

 ใช้คำยาวได้และอธิบายด้วยชื่อของมัน

ชื่อ function ควรจะเป็นคำกิริยา และต้องสื่อถึงสิ่งที่มันทำ รวมถึงคำสั่งภายในและพวกค่าที่รับเข้ามาด้วย ชื่อ function ที่ยาวดีกว่าสั้นแล้วเข้าใจยากหรือพวก comment ยาวๆ

// DON'T
/**
 * Invite a new user with its email address
 * @param {String} user email address
 */
function inv (user) { /* implementation */ }

// DO
function inviteUser (emailAddress) { /* implementation */ }

จะเห็นว่าเขาเขียน comment อธิบายยาวๆ เอาจริงๆคนส่วนใหญ่ก็ไม่ได้อ่าน ถ้าเรามองที่ชื่อ function จะเป็น inv คือมันสั้นมากๆ และตัวแปรที่รับเข้ามาเป็น user ซึ่งเราจะไม่ทราบว่า user เนี้ยต้องรับแบบไหนทั้ง object เลยหรือเปล่าถ้าเป็น object แล้วในนั้นมีอะไรบ้าง เห็นไหมครับว่ามันมีจุดให้เราได้สงสัยถ้าเรามาทำโค้ดต่อ

สิ่งที่ควรทำก็จะเห็นว่าเขียนอธิบายเลยว่า inviteUser และค่าที่รับคือ emailAddress อันนี้อ่านแล้วเข้าใจทันที

หลีกเลี่ยงการรับค่า arguments เยอะๆใน function

ความหมายคือหากว่าเราจำเป็นต้องรับค่าเยอะๆ ให้ใช้การรับค่าเป็น object แทนแล้วเวลาโยนค่าจะง่ายไปดูตัวอย่าง

// DON'T
function getRegisteredUsers (fields, include, fromDate, toDate) { /* implementation */ }
getRegisteredUsers(['firstName', 'lastName', 'email'], ['invitedUsers'], '2016-09-26', '2016-12-13')

// DO
function getRegisteredUsers ({ fields, include, fromDate, toDate }) { /* implementation */ }
getRegisteredUsers({
  fields: ['firstName', 'lastName', 'email'],
  include: ['invitedUsers'],
  fromDate: '2016-09-26',
  toDate: '2016-12-13'
})

จะสังเกตุในตัวอย่างที่ไม่ดีว่าเขารับค่า 4 ค่าแต่ในตัวอย่างที่ดีเขาจะรับมาเป็น object เดียวแล้วเอาค่าจากใน object ออกมาเลยเราเรียกการดึงค่าออกจาก object อย่างนี้ว่า destructuring assignment

Reduce side effects

อันนี้ไม่รู้จะหาชื่อไทยยังไง เอาเป็นว่าทุกๆ function เราควรจะ return เป็นแบบ pure function หมายความว่าเราจะ return ค่าใหม่ออกไปเสมอเราจะไม่แก้ไขค่าเก่าที่โยนเข้ามาเพราะว่า object มันเป็นเก็บเป็นแบบ reference อธิบายง่ายๆคือ ถ้าสมมติ function a (user)  รับค่า user โดยข้างในเราอาจจะเปลี่ยนแปลงค่าบางอย่าง แล้วตอน return ออกไปนั้นค่า user ได้ถูกเปลี่ยนแล้วตัวอย่างเช่น

function a (user) {
 user.name = 'aaaa'
 return user
}

function b(user) {
  if (user.name == 'tong') {
    return true
  } else {
    return false
  }
}

const user = {
 name: 'tong'
}

console.log(a(user))
// ผลลัพธ์ข้างบน { name: 'aaaa' }
console.log(b(user))
// ผลลัพธ์ข้างบน false

ซึ่งจริงๆแล้วเราคิดว่า function b ควรจะ return true อันนี้ให้มองเห็นง่ายๆนะว่ามันถูกเปลี่ยนแปลงค่า ไปดูตัวอย่างที่ในบทความเขาทำให้ดูครับ

// DON'T
function addItemToCart (cart, item, quantity = 1) {
  const alreadyInCart = cart.get(item.id) || 0
  cart.set(item.id, alreadyInCart + quantity)
  return cart
}

// DO
// not modifying the original cart
function addItemToCart (cart, item, quantity = 1) {
  const cartCopy = new Map(cart)
  const alreadyInCart = cartCopy.get(item.id) || 0
  cartCopy.set(item.id, alreadyInCart + quantity)
  return cartCopy
}

// or by invert the method location
// you can expect that the original object will be mutated
// addItemToCart(cart, item, quantity) -> cart.addItem(item, quantity)
const cart = new Map()
Object.assign(cart, {
  addItem (item, quantity = 1) {
    const alreadyInCart = this.get(item.id) || 0
    this.set(item.id, alreadyInCart + quantity)
    return this
  }
})

จัดการการเรียง function โดยความสำคัญก่อนหลัง

function ที่สำคัญเป็นคนเริ่มเรียกก่อน function อื่นให้วางไว้ข้างบนไปดูตัวอย่าง

// DON'T
// "I need the full name for something..."
function getFullName (user) {
  return `${user.firstName} ${user.lastName}`
}

function renderEmailTemplate (user) {
  // "oh, here"
  const fullName = getFullName(user)
  return `Dear ${fullName}, ...`
}

// DO
function renderEmailTemplate (user) {
  // "I need the full name of the user"
  const fullName = getFullName(user)
  return `Dear ${fullName}, ...`
}

// "I use this for the email template rendering"
function getFullName (user) {
  return `${user.firstName} ${user.lastName}`
}

จะสังเกตุว่า function renderEmailTemplate เป็นคนเรียกใช้ getFullName อีกทีเราจะวาง function นี้ก่อนครับเพราะถ้าหากเราจะหา function ที่ใช้งานต่อเราจะได้เลื่อนมาดูเลยเหมือนจัดการไว้แล้วว่าต้องวางระบบประมาณนี้เพื่อให้หาง่าย

Query or Modify

ให้ function นั้นๆทำอย่างใดอย่างหนึ่งแต่ไม่ใช่ทั้งคู่ คือให้หาอะไรก็ทำเพื่อหาอย่างเดียว ให้เปลี่ยนแปลงค่าอะไรก็ให้ทำแบบนั้นอย่างเดียว

ทุกคนเขียน javascript แตกต่างกันทำอย่างไรดี ?

เนื่องจากเจ้า js เนี้ยมัน dynamic type จึงทำให้เกิด error ได้ง่ายเราจึงต้องหาวิธีแก้ไขให้คนทำงานร่วมกันพลาดน้อยสุดครับ

ใช้ข้อตกลงร่วมกันและใช้ linter

linter คือเครื่องในการดูว่า format ที่คุณเขียนตรงกับกฎที่ตั้งไว้ไหม เช่นให้มี ; ทุกครั้งที่ปิดบรรทัดหรือว่าต้อง if แล้วปีกกาต้องวางไว้ข้างหลังเสมอ ตรงนี้จะลดความผิดพลาดลงได้ครับ หากสนใจเชิญ linter คืออะไร

เขียน async ยังไงดี

ให้ใช้ promise ทุกครั้ง ถ้าคุณใช้ node 4 ขึ้นไปและใช้ async/await สำหรับ node 7 ขึ้นไปครับใช้ยังไงไปดูกัน

// AVOID
asyncFunc1((err, result1) => {
  asyncFunc2(result1, (err, result2) => {
    asyncFunc3(result2, (err, result3) => {
      console.lor(result3)
    })
  })
})

// PREFER
asyncFuncPromise1()
  .then(asyncFuncPromise2)
  .then(asyncFuncPromise3)
  .then((result) => console.log(result))
  .catch((err) => console.error(err))

คุณอาจจะใช้ bluebird ก็ได้ครับใช้ง่ายดี อันนี้ผมก็ใช้อยู่บาง project แต่ปัจจุบันก็ใช้ async / await นะครับ ใครไม่เข้าใจเชิญอ่านตรงนี้เลย

async/await เรามารู้จัก syntax ที่จะมาเปลี่ยนโลกของ javascript กัน

ฉันจะเขียนโค้ดให้มีประสิทธิภาพได้อย่างไร ?

ก็ฝึกเขียน clean code และก็ต่อด้วยบทความนี้ //blog.risingstack.com/node-js-tutorial-debugging-async-memory-leaks-cpu-profiling/ เพื่อหาปัญหาคอขวดว่าตรงไหนช้าเอาไว้จะมาเขียนต่อเรื่องนี้นะ

มีคำถามอะไร comment ได้เลยครับ

ฝากข้อคิดเห็น

This site uses Akismet to reduce spam. Learn how your comment data is processed.