ความล้มเหลวในการเข้าสู่ระบบที่มีสาเหตุมาจากการจัดเก็บหมายเลขโทรศัพท์ที่ไม่ตรงกันได้เผยให้เห็นถึงข้อผิดพลาดหนึ่ง บัญชีของผู้ใช้รายหนึ่งถูกระงับเนื่องจากฐานข้อมูลเก็บหมายเลขโทรศัพท์มือถือของเยอรมนีเบอร์เดียวกันไว้ในสองรูปแบบ คือ 0171 5550134 ในแถวหนึ่ง และ +49 171 5550134 ในอีกแถวหนึ่ง ทำให้ระบบมองว่าทั้งสองเป็นข้อมูลคนละชุดกัน ผลลัพธ์คือ OTP ไม่เคยส่งมาถึง และปุ่ม "ส่งรหัส" ก็กลายเป็นเรื่องของการเสี่ยงดวง

ทำไมหมายเลขโทรศัพท์ถึงจัดการยากกว่าวันที่

นักพัฒนามักจะเชื่อใจ regex ในการควบคุมรูปแบบการกรอกหมายเลขโทรศัพท์ แต่ความเชื่อใจนั้นจะพังทลายลงทันทีเมื่อหมายเลขนั้นข้ามพรมแดนหรือมีการเปลี่ยนแปลงแผนการโทรระดับประเทศ วันที่ดำเนินไปตามปฏิทินที่คาดเดาได้ แต่หมายเลขโทรศัพท์เปลี่ยนแปลงไปตามผู้ให้บริการ กฎระเบียบ และธรรมเนียมปฏิบัติทางวัฒนธรรม

ต้นทุนที่ซ่อนอยู่ของการเก็บข้อมูลแบบสตริงดิบ

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

ปัญหายิ่งแย่ลงเมื่อหมายเลขถูกเก็บไว้ในฟิลด์ที่เป็นตัวเลข คอลัมน์ประเภท BIGINT จะตัดเครื่องหมาย + และเลขศูนย์นำหน้าออกทั้งหมด เปลี่ยนจาก +49 171 5550134 เป็น 491715550134 และหากไม่มีเครื่องหมายบวก การจะกู้คืนรูปแบบเดิมกลับมาก็ทำได้เพียงแค่การคาดเดา

ข้อตกลง E.164

แผนการกำหนดหมายเลขโทรศัพท์ระหว่างประเทศ หรือ E.164 ได้กำหนดรูปแบบที่เป็นมาตรฐานเดียวและนำไปใช้ได้ทุกที่:

  • เริ่มต้นด้วย +
  • ตามด้วยรหัสประเทศ 1 ถึง 3 หลัก
  • ตามด้วยหมายเลขผู้ใช้บริการ
  • รวมแล้วต้องไม่เกิน 15 หลัก
  • ไม่มีช่องว่าง จุด หรือขีดกลาง

E.164 ไม่ได้ รับประกันว่าหมายเลขนั้นใช้งานได้จริง แต่มันรับประกันว่าสตริงนั้นเป็นไปตามรูปแบบโครงสร้างที่ถูกต้อง ให้มองว่ามันคือข้อตกลงด้านรูปแบบ (format contract) ไม่ใช่เครื่องมือยืนยันการติดต่อได้ (reachability oracle)

ข้อผิดพลาดทั่วไปที่ regex แก้ไม่ได้

  • การจัดเก็บแบบตัวเลข (Numeric storage) – BIGINT จะตัดเครื่องหมาย + และเลขศูนย์นำหน้าออก ควรใช้คอลัมน์ประเภทข้อความ (TEXT หรือ VARCHAR) แทน
  • การเขียน regex แบบตายตัว (Hard-coded regexes) – แผนการโทรระดับประเทศมีการเปลี่ยนแปลงเสมอ เช่น เม็กซิโกยกเลิกรหัส Trunk prefix ในปี 2019 หรืออาร์เจนตินาที่ปัจจุบันกำหนดให้ต้องมีเลข 9 หลังรหัสประเทศสำหรับเบอร์มือถือ รูปแบบที่เขียนไว้ตายตัวจึงล้าสมัยได้อย่างรวดเร็ว
  • การตัดเลขศูนย์นำหน้าโดยไม่พิจารณาเงื่อนไข (Blind zero stripping) – เบอร์บ้านในอิตาลีต้องมีเลขศูนย์นำหน้า แต่เบอร์ในเยอรมนีไม่ต้องมี การใช้กฎ "ลบเลขศูนย์นำหน้า" แบบเหมาเข่งจะทำให้ข้อมูลของอิตาลีผิดเพี้ยน ในขณะที่เบอร์เยอรมันไม่ได้รับผลกระทบ
  • การทึกทักว่ารูปแบบที่ถูกต้องหมายถึงการส่งถึงผู้รับได้จริงlibphonenumber ช่วยตรวจสอบโครงสร้างได้ แต่ไม่สามารถบอกได้ว่าโทรศัพท์เปิดอยู่หรือไม่ หรือหมายเลขนั้นถูกย้ายค่ายไปแล้วหรือยัง

การสร้าง Pipeline ที่เชื่อถือได้

  1. ให้ระบุประเทศ – เพิ่มตัวเลือกประเทศในฟอร์มลงทะเบียนและส่งข้อมูลภูมิภาคนั้นไปยังตัว Parser
  2. แสดงรูปแบบขณะพิมพ์ (Live formatting) – ใช้การจัดรูปแบบแบบ “AsYouType” เพื่อให้ผู้ใช้เห็นรูปแบบที่ถูกต้องในขณะที่กำลังพิมพ์
  3. ตรวจสอบเมื่อออกจากฟิลด์ (Validate on blur) – ทำการตรวจสอบหลังจากผู้ใช้เลิกโฟกัสที่ฟิลด์นั้น แทนที่จะตรวจสอบในทุกๆ การกดแป้นพิมพ์ เพื่อลดความรำคาญของผู้ใช้
  4. บันทึกเฉพาะสตริงรูปแบบ E.164 – เก็บหมายเลขที่ผ่านการ Normalize และมีเครื่องหมาย + นำหน้าไว้ในฐานข้อมูล
  5. จัดรูปแบบที่ส่วนแสดงผล (Format at the edge) – ค่อยแปลงกลับเป็นรูปแบบที่อ่านง่ายสำหรับมนุษย์เฉพาะในส่วนของ UI หรือเทมเพลตอีเมลเท่านั้น

เมื่อต้องย้ายข้อมูลเก่า (legacy data) ให้เก็บแถวที่ตรวจสอบไม่ผ่านไว้ ให้ทำการ Parse ข้อมูลที่มีอยู่เดิมลงในคอลัมน์ใหม่ ทำเครื่องหมายรายการที่ล้มเหลว และสร้างรายงานขึ้นมา การลบข้อมูลทิ้งไปเงียบๆ จะสร้างตั๋วแจ้งปัญหา (support tickets) ที่กลายเป็นการแก้ไขที่ต้องใช้ต้นทุนสูงในภายหลัง

รายการตรวจสอบด่วนสำหรับนักพัฒนา

  • เก็บหมายเลขเป็น TEXT/VARCHAR ในรูปแบบ E.164
  • ใช้ไลบรารี libphonenumber ของ Google ซึ่งจัดการกับความซับซ้อนของแผนการโทรทั่วโลกได้ดี
  • กำหนดภูมิภาคเริ่มต้น (default region) สำหรับผู้ใช้ที่ไม่ได้ระบุรหัสประเทศ
  • เรียกใช้ is_valid_number สำหรับการลงทะเบียนใหม่ เพื่อตรวจสอบว่าหมายเลขเป็นไปตามกฎของภูมิภาคนั้นๆ
  • ใช้ is_possible_number เมื่อต้องทำความสะอาดข้อมูลจำนวนมาก เพื่อดักจับข้อมูลที่ผิดรูปแบบอย่างชัดเจนโดยไม่ปฏิเสธกรณีที่ก้ำกึ่ง

การจัดการหมายเลขโทรศัพท์อย่างเหมาะสมไม่ใช่แค่ "มีก็ดี" แต่มันคือ "สิ่งที่จำเป็นต้องมี" สำหรับระบบใดก็ตามที่ต้องพึ่งพาการติดต่อผู้ใช้ที่เชื่อถือได้ การทำ Normalization เป็น E.164 และส่งต่อหน้าที่การ Parse ให้กับไลบรารีที่ผ่านการทดสอบมาอย่างดี จะช่วยให้นักพัฒนาขจัดบั๊กประเภทที่บั่นทอนความเชื่อมั่นและรายได้โดยไม่รู้ตัว จงปฏิบัติกับหมายเลขโทรศัพท์ในฐานะข้อมูลที่มีโครงสร้าง ไม่ใช่ข้อความอิสระ และปล่อยให้มาตรฐานสากลเป็นตัวจัดการงานหนักแทนคุณ