ตั้งแต่ทำงานผมก็จับแต่ PHP ซึ่งเป็นภาษาโปรแกรมสมัยใหม่ มีการจัดการความผิดพลาดที่ยืดหยุ่นและมีประสิทธิภาพแต่พอกลับมาใช้งาน VBA ที่ใช้เขียนแมโครถึงกับต้องมารำลึกความหลังว่าตัว VBA นั้นมีวิธีการจัดการเรื่องความผิดพลาดอย่างไรบ้าง เพราะยิ่งทำงานโดยเฉพาะงานที่ให้แผนกอื่นใช้พบว่า เราไม่สามารถคาดเดาอะไรได้เลยว่าผู้ใช้จะใส่อะไรเข้ามาในโปรแกรม ที่ผ่านเจอมาหลากหลายรูปแบบมาก
ยังมีอีกเยอะแยะตาแป๊ะมาก ๆ แต่หลัก ๆ ก็เจอประมาณนี้ การป้องกันความผิดพลาดหรือจัดการความผิดพลาดนั้นเป็นเรื่องที่สำคัญมาก ๆ เพราะช่วยให้ผู้ใช้งานไม่มึนตรึ๊บกับกล่องข้อความที่อ่านแล้วไม่เข้าใจเลย ยิ่งถ้าผู้ใช้ไม่ได้อยู่ที่ไซต์งานแล้วแล้วยิ่งปวดหัวไปกันใหญ่เพราะเราก็มึนว่ามัน Error ตรงไหน
ดังนั้นนอกจากโปรแกรมที่เขียนต้องทำงานถูกต้องแล้ว การจัดการความผิดพลาดที่อาจเกิดขึ้นนับว่าเป็นอีกปัจจัยที่สำคัญกับการใช้งานเป็นอย่างมาก ถ้าแยกออกเป็นประเด็นหลัก ๆ ได้แก่
คราวนี้ลองมาดูในรายละเอียดกันว่าแต่ละประเด็นมีอะไรน่าสนใจกันบ้าง
วิธีนี้ต้องใช้ประสบการณ์อันยาวนานและความสามารถเฉพาะตัวในการ "เดา" อย่างมีหลักการตลอดการทำงานของโปรแกรม เช่น
การจัดการโดยการ "ดัก" เหตุการณ์ที่อาจเกิดขึ้นได้ และแจ้งให้ผู้ใช้งานทราบถึงความผิดพลาดดังกล่าวก่อนที่โปรแกรมจะเดี้ยง ยิ่งรองรับมาก ผู้ใช้งานใช้ง่าย ภาระก็จะตกของคนเขียนโปรแกรมเต็ม ๆ
ข้อดีที่โปรแกรมสามารถ "ดัก" ความผิดพลาดเหล่านี้ได้นั้นคือ โปรแกรมจะสามารถทำงานต่อหรือหยุดทำงานในแบบที่หาสาเหตุของปัญหา เพื่อหาทางลงแบบนิ่ม ๆ ได้ และทำให้คนเขียนโปรแกรมสามารถแนะนำวิธีการแก้ไขปัญหากับผู้ใช้งานได้อย่างถูกต้องตรงจุด แต่ก็แลกด้วยการใช้พลังมหาศาลในการเขียนในส่วนนี้ขึ้นมาแต่...ในชีวิตจริงไม่มีใครที่จะเขียนหรือคิดได้ครอบคลุมทุกเงื่อนไขเพราะทรัพยากรมีจำกัด (เวลา พลังชีวิต และ "ค่าจ้าง")
ลองมาดูกันสักตัวอย่างเพื่อให้เห็นภาพว่าถ้าจะทำโปรแกรมให้มันเมพ ๆ สักโปรแกรม ผู้ใช้งานแสนสบาย จะใส่ข้อมูล จะปู้ยี่ปูยำยังไงโปรแกรมก็สามารถรับมือ โดยลองทำเป็นตารางให้ผู้ใช้งานกรอกเพื่อคำนวณราคาสินค้าขายยกลัง ผลลัพธ์ที่ต้องการคือสินค้าที่ราคาต่อหน่วยถูกที่สุด
ลำดับการทำงานของแมโครก็ง่าย ๆ ดังนี้
ลองเขียนเป็น VBA ตามขั้นตอนที่ต้องการ โดยตั้งชื่อว่า Sub Demo1()
Sub Demo1() Dim rTable As Range Set rTable = Names("table_product").RefersToRange 'หาจำนวนแถวที่ใช้งาน Dim maxRow As Long maxRow = rTable.Parent.UsedRange.Rows.Count Dim nRow As Long, nRowResult As Long Dim nLowPrice As Long, nPrice As Long nLowPrice = 2147483647 'กำหนดค่าเริ่มต้นของราคาที่ใช้เปรียบเทียบไว้สูงที่สุด 'อ่านข้อมูลตั้งแต่แถวที่ 2 จนถึงแถวสุดท้าย For nRow = 2 To maxRow With rTable.Rows(nRow) 'คำนวณเฉพาะแถวที่มีชื่อสินค้า If .Cells(1).Value <> "" Then nPrice = .Cells(3).Value / .Cells(2).Value 'คำนวนราคาต่อหน่วยจากคอลัมน์ที่ #3 หาร #2 If nPrice < nLowPrice Then nLowPrice = nPrice nRowResult = nRow End If End If End With Next 'แสดงข้อความผลที่ได้ MsgBox "ราคาสินค้าต่อหน่วยต่ำที่สุดคือ " & rTable.Cells(nRowResult, 1).Value End Sub
ลองมาปู้ยี่ปู้ยำโปรแกรมกันโดยลองใส่ข้อมูลไม่ครบ แถวที่ 4 ไม่ใส่จำนวนชิ้น เมื่อ Run แมโคร ก็จะเจอ
หรือป้อนข้อมูลที่ไม่ใช่ตัวเลข ซึ่งอาจจะเกิดจากความผิดพลาดที่ไม่ตั้งใจของผู้ใช้ เมื่อ Run แมโคร ก็จะเจอ
หรือถ้าผู้ใช้งานดันทะลึ่งไปลบ Name ที่ตั้งเอาไว้ ไม่ว่าจะตั้งใจหรือเพราะรู้เท่าไม่ถึงการณ์ก็แล้วแต่ พอ Run แมโครก็จะ Error อีกแล้ว
เป็นไงบ้างครับ อ่านถึงตรงนี้แล้วแทบลมจับ ขนาดโปรแกรมไม่กี่บรรทัด หากไม่มีการเขียนในส่วนของการตรวจสอบความผิดพลาดที่อาจเกิดขึ้น (ทั้งที่ไม่น่าจะเกิดขึ้น) พอเกิดปัญหาขึ้นมา สุดท้ายเราก็ต้องมาเหนื่อยตามแก้ไขกันอีกหลายตลบเลยทีเดียว
การแก้ไขปัญหาดังกล่าว สามารถใช้วิธีเขียนคำสั่ง If เพื่อตรวจสอบค่าก่อนนำไปใช้งาน เพื่อให้แน่ใจว่าจะไม่เกิด Run-time error มาดูคำสั่งที่เขียนเพิ่มเติมเพื่อดักปัญหาต่าง ๆ ที่ลองยกตัวอย่างมา
Sub Demo2() Dim rTable As Range On Error Resume Next Set rTable = Names("table_product").RefersToRange On Error GoTo 0 If rTable Is Nothing Then MsgBox "ไม่พบ Name 'table_product' ในแผ่นงาน โปรดตรวจสอบ Name Manager", vbCritical Exit Sub End If 'หาจำนวนแถวที่ใช้งาน Dim maxRow As Long maxRow = rTable.Parent.UsedRange.Rows.Count Dim nRow As Long, nRowResult As Long Dim nLowPrice As Long, nPrice As Long nLowPrice = 2147483647 'กำหนดค่าเริ่มต้นของราคาที่ใช้เปรียบเทียบไว้สูงที่สุด 'อ่านข้อมูลตั้งแต่แถวที่ 2 จนถึงแถวสุดท้าย For nRow = 2 To maxRow With rTable.Rows(nRow) 'คำนวณเฉพาะแถวที่มีชื่อสินค้า If .Cells(1).Value <> "" Then If TypeName(.Cells(2).Value) <> "Double" Or TypeName(.Cells(3).Value) <> "Double" Then MsgBox "โปรดระบุค่าเป็นตัวเลขเท่านั้น โปรดตรวจสอบแถวที่ " & nRow, vbCritical Exit Sub End If If .Cells(2).Value <= 0 Then MsgBox "จำนวนชิ้นมีค่าน้อยกว่าหรือเท่ากับ 0 ในแถวที่ " & nRow, vbCritical Exit Sub End If nPrice = .Cells(3).Value / .Cells(2).Value 'คำนวนราคาต่อหน่วยจากคอลัมน์ที่ #3 หาร #2 If nPrice < nLowPrice Then nLowPrice = nPrice nRowResult = nRow End If End If End With Next 'แสดงข้อความผลที่ได้ MsgBox "ราคาสินค้าต่อหน่วยต่ำที่สุดคือ " & rTable.Cells(nRowResult, 1).Value End Sub
จาก Sub Demo2() มีการปรับปรุงโดยการเพิ่มส่วนในการตรวจสอบค่าต่าง ๆ ก่อนนำไปใช้งาน หรือข้อมูลที่ได้อาจมีปัญหาจะทำการขึ้นข้อความเตือนและหยุดการทำงาน เพื่อให้ผู้ใช้กลับไปแก้ไขให้ถูกต้องก่อน จะเห็นว่าจำนวนบรรทัดของเดิม 30 เพิ่มเป็น 50 ก็กะคร่าว ๆ ว่าในส่วนของตรวจสอบความผิดพลาดเกือบ 30-50% ของงานที่ทำเลยก็ได้ ความจำเป็นในการเพิ่มและความถี่ในการตรวจสอบก็อยู่ที่ว่าต้องการให้โปรแกรมมันทนทานต่อความผิดพลาดได้แค่ไหน ถ้าขณะที่ใช้งานโปรแกรมอยู่ใกล้ ๆ สามารถเดินไปดูได้ ก็ไม่จำเป็นต้องลงทุนอะไรมาก แต่ถ้าหากเกิดปัญหาจะแก้ไขหรือตามดูแลลำบากก็ต้องจัดกันเต็ม ๆ
แม้ว่าจะเขียนคำสั่งเพิ่มเข้าไปกันโน้นกันนี่แล้วแต่ก็ใช่ว่าจะกันได้ทุกเงื่อนไข อาจมีเงื่อนไขที่คาดไม่ถึงเกิดขึ้นได้อีกใครจะไปรู้ ดังนั้นจึงต้องมีการรับมือกับปัญหาที่เกินที่จะคาดเดาเอาไว้ด้วย มิฉะนั้นก็จะต้องเจอ Run-time error ที่ผู้ใช้งานอ่านแล้วไม่รู้เรื่องอยู่ดี
ใน VBA จะมีคำสั่ง On Error เพื่อช่วยในการจัดการกับ Error ต่าง ๆ ที่อาจเกิดขึ้น โดยหากเกิด Error ขึ้นแทนที่มันแสดง Run-time error ที่อ่านแล้วไม่เข้าใจ มันจะส่งต่อปัญหาดังกล่าวไปในส่วนที่เขียนขึ้นเพื่อจัดการปัญหาดังกล่าวเอง โครงสร้างโค้ดสำหรับจัดการ Error จะดังนี้
Sub Demo3() On Error GoTo handle_error 'your code here 'your code here line_x: 'your code here Exit Sub handle_error: 'handle error here '................. 'go back to continue work Resume Next 'or Resume line_x 'if don't to continue then Exit Sub End Sub
จากตัวอย่างใน Demo3 หากโปรแกรมเกิด Run-time error ขึ้น มันจะวิ่งไปที่บรรทัดที่ 11 ตามคำสั่ง On Error GoTo handle_error ผู้เขียนโค้ดจำเป็นต้องจัดการกับ Error ที่เกิดขึ้นซึ่งจะระบุไว้ใน object Err เช่น
ก็เขียนจัดการกันไปว่า Err.Number เป็นค่าอะไร แล้วจะทำอย่างไรกับมันดี จะแจ้งปัญหาให้ผู้ใช้ทราบว่าเกิดอะไร และหลังจากนั้นจะให้ทำอะไรต่อ ซึ่งตัวเลือกตรงนี้มี 3 ตัวเลือกคือ
สำหรับวิธีการใช้ On Error ในแมโคร สามารถดูรายละเอียดเพิ่มเติมที่เว็บไซต์ http://support.microsoft.com/kb/141571/EN-US ครับ ส่วนข้อมูลอ้างอิงอื่น ๆ เกี่ยวกับ Error Code
สำหรับเทคนิคที่ผมแนะนำคือ ควรมีตัวแปรแบบ Global สำหรับ module นั้น ๆ เพื่อช่วยบันทึกชื่อ Sub หรือ Function ที่ทำงานอยู่แล้วเกิด Error ขึ้น เพราะว่าใน VBA ค่า Err.Source จะระบุไว้หยาบมากคือบอกที่ระดับ Project ไหนที่เกิด Error
หากผู้ใช้งานเป็นระดับธรรมดา จับภาพหน้าจอไม่เป็น เจอภาษาอังกฤษแล้วรีบปิดหนี เจอแบบนี้แนะนำว่าให้เขียน log file เอาไว้ว่าเกิด Error อะไร แล้วให้ผู้ใช้ส่งไฟล์ดังกล่าวกลับมาให้ดูเพื่อวิเคราะห์ปัญหาที่เกิดขึ้น ตัวอย่างโปรแกรมแสดงใน Sub Demo4()
Sub Demo4() Dim currentModule As String currentModule = "Demo4()" On Error GoTo handle_error Debug.Print 1 / 0 'make some error Exit Sub handle_error: Dim fileNumber As Long fileNumber = FreeFile Open ThisWorkbook.Path & "\error_log.txt" For Append As #fileNumber Print #fileNumber, "Date: ", Now() Print #fileNumber, "Module: ", currentModule Print #fileNumber, "Err.Source: ", Err.Source Print #fileNumber, "Err.Number: ", Err.Number Print #fileNumber, "Err.Description: ", Err.Description Print #fileNumber, "" Close #fileNumber MsgBox "พบความผิดพลาดในโปรแกรม " & _ "โปรดส่งข้อมูลในไฟล์ error_log.txt ให้กับผู้พัฒนาโปรแกรม", vbCritical Shell "notepad.exe """ & ThisWorkbook.Path & "\error_log.txt" & """" Exit Sub End Sub
อีกเทคนิคคือ ในกรณีที่โปรแกรมมีการเรียก Sub หลัก แล้ว Sub หลักไปเรียก Sub ย่อย ๆ ก็ให้วางคำสั่ง On Error ไว้ที่ Sub หลักเท่านั้น เมื่อเกิด Error ที่ Sub ย่อย ๆ ก็จะเด้งกลับมาที่ Sub หลักเพื่อจัดการ Error ทั้งหมดก็ได้ แต่วิธีนี้มีข้อจำกัดคือ พอกลับมาที่ Sub หลักแล้วจะไม่กลับไปทำที่ Sub ย่อยต่อ ดั้งนั้นไม่มีวิธีการตายตัวอยู่ที่การออกแบบ แล้วแต่ทรัพยากรจะอำนวยครับ
สุดท้ายก่อนจบประเด็นนี้ หากใช้ On Error Resume Next อย่าลืมยกเลิกด้วย On Error Goto 0 ด้วยเสมอ มิฉะนั้นหากโปรแกรมผิดพลาดขึ้นก็จะเงียบ และหาสาเหตุของปัญหาไม่เจอ อันตรายมากครับ
ปล.ตอนแรกตั้งใจจะเขียนให้สั้น ๆ แต่คงลดได้สุด ๆ แค่นี้ นี่ก็ยาวมาเกินที่คิดไว้เยอะเลย