scope ว่าง แปลว่าใช้กับทุกอย่าง
- เผยแพร่
- ระบบ
- Klang Ya Bhaesaj
- เวลาอ่าน
- 6 นาที
บั๊กที่แย่ที่สุดสองตัวที่ผมเคยปล่อยขึ้นระบบมาจากไฟล์เดียวกัน และไม่มีตัวไหน crash เลย ทั้งคู่คือการคำนวณที่ดูสมเหตุสมผลสุด ๆ
เครื่องมือโปรโมชั่นเป็นตัวตัดสินว่าลูกค้าจ่ายเท่าไหร่ มันรันตอนดูตะกร้า ตอนสร้างใบเสนอราคา ตอนเจ้าหน้าที่แก้ทุกครั้ง แล้วรันอีกทีตอนอนุมัติ ถ้าสี่จังหวะนี้ตอบไม่ตรงกันแม้แต่คู่เดียว จะมีคนถูกเก็บเงินผิด
pure function ให้ผู้เรียกจัดการ transaction
ตัดสินใจข้อแรก evaluatePromotions เป็น pure อ่านโปรโมชั่นที่เข้าข่าย คืนผลราคา ไม่เขียนอะไรเลย ส่วนการบันทึกว่าโปรโมชั่นถูกใช้ ลดโควตา เติม order id ย้อนกลับ เป็นงานของผู้เรียก ใน transaction ของผู้เรียกเอง
นั่นแหละที่ทำให้รันเครื่องมือเดียวกันได้ 4 จังหวะอย่างปลอดภัย ดูตะกร้าคำนวณกี่รอบก็ได้ไม่มีผลข้างเคียง ส่วนการอนุมัติคำนวณอยู่ใน transaction เดียวกับที่ตัดสต็อก ราคากับสต็อกเลย commit หรือ fail ไปด้วยกัน
ถ้าเครื่องมือเขียนบันทึกการใช้งานเอง แค่ เปิดดู ตะกร้าก็กินโควตาโปรโมชั่นแล้ว
scope ว่าง แปลว่าทุกอย่าง
โปรโมชั่นกำหนดขอบเขตได้แบบระบุสินค้า ระบุหมวดหมู่ หรือไม่ระบุเลย อันสุดท้ายคือจุดที่คม
ไม่ระบุสินค้า และ ไม่ระบุหมวดหมู่ แปลว่า สินค้าทุกตัวเข้าข่าย ค่าว่างไม่ได้แปลว่า "ไม่มีอะไร" แต่แปลว่า "ทุกอย่าง"
เรื่องนี้ทำฟีเจอร์พังมาสองรอบ ทั้งสองรอบเพราะคนที่คิดอย่างมีเหตุผลอ่านลิสต์ว่างเป็นผลลัพธ์ว่าง มันคือโปรลด 10% ทั้งร้าน แน่นอนว่าต้องใช้กับทุกอย่าง เพราะนั่นคือความหมายของ "ไม่จำกัด"
กับดักคือมีค่าว่างสองแบบ และห้ามสับสน
- ไม่มีแถว scope เลย → ไม่จำกัด → ทุกตัวเข้าข่าย
- มีแถว scope แต่ขยายแล้วได้ศูนย์รายการ → จำกัดจริง แต่ตอนนี้ไม่ตรงกับอะไร → ไม่มีอะไรเข้าข่าย
โปรที่ผูกกับหมวดที่สินค้าถูกปิดการขายไปหมดแล้ว ไม่ใช่ การลดราคาทั้งร้าน ยุบสองสถานะนี้เข้าด้วยกันเมื่อไหร่ ก็เท่ากับเพิ่งลดราคาเต็มจำนวนให้ทั้งแคตตาล็อก
แปลว่าการตรวจต้องเกิด ก่อน การขยาย ตัวคำนวณ flash sale เลยหา hasScopeRows ก่อน แล้วค่อยขยาย โค้ดไหนที่ขยายก่อนแล้วค่อยถามว่า "ลิสต์ว่างไหม" คือโค้ดที่ทิ้งความต่างนี้ไปแล้ว
clamp ที่มีค่า ฿150
ส่วนลดแบบระบุบาทมีสองแบบ และต่างกันไม่ใช่แค่ผิวเผิน
| แบบ | ความหมาย | สูตร |
|---|---|---|
| ต่อบิล | ลด ฿50 จากออเดอร์นี้ | min(value, qualifyingSubtotal) |
| ต่อชิ้น | ลด ฿1 ทุกชิ้น | Σ min(value, item.unitPrice) × quantity |
ดูสูตรต่อชิ้นดี ๆ ตัว min อยู่ ข้างใน ผลรวม ทำทีละรายการ ก่อนคูณจำนวน
เวอร์ชันก่อนหน้า clamp แค่ตอนท้ายเทียบกับยอดรวมที่เข้าข่าย ดูเหมือนเท่ากัน แต่ไม่เท่า โปรลด ฿1 ต่อชิ้นบนตะกร้าที่มีของถูกเยอะ ๆ ให้ผลเป็น ลด ฿220 แทนที่จะเป็น ฿70 ส่วนลดใหญ่กว่าของที่กำลังลดอยู่ เพราะไม่มีอะไรกันไม่ให้ส่วนลดต่อชิ้นใหญ่กว่าตัวชิ้นเอง
กฎที่ได้คือ ส่วนลดต่อชิ้นห้ามเกินราคาต่อหน่วยของชิ้นนั้น และต้องบังคับทีละชิ้น ตรงจุดที่คูณ การ clamp ยอดรวมทีหลังเป็นข้อความที่อ่อนกว่า และมันบังเอิญถูกกับตะกร้าส่วนใหญ่เฉย ๆ
เพราะงั้นการย่อให้ง่ายแบบ value × totalQuantity มันผิด และต้องผิดต่อไป
หน้าจอก็ต้องพูดความจริง
สองแบบนี้ต้องแสดงต่างกันบนหน้าร้าน และนี่คือความถูกต้อง ไม่ใช่รสนิยม
แบบต่อชิ้นเปลี่ยนราคาต่อหน่วยจริง เลยได้ราคาขีดฆ่ากับป้าย /ชิ้น ส่วนแบบต่อบิล ไม่ได้ เปลี่ยนราคาต่อหน่วยเลย มันหักยอดคงที่ออกจากยอดรวม เอาราคาขีดฆ่าไปแปะตรงนั้นคือการโกหก ลูกค้าเอาราคาที่เห็นคูณจำนวน ได้เลขคนละตัวกับยอดรวม แล้วก็เลิกเชื่อตะกร้าทั้งใบ
แบบต่อบิลเลยได้ป้ายว่าลด ฿X ต่อบิล แล้วปล่อยราคาต่อหน่วยไว้เฉย ๆ
โค้ด กับอันไหนที่ใช้อัตโนมัติ
สามสถานะจากสองคอลัมน์
| promo_code | is_code_required | พฤติกรรม |
|---|---|---|
| null | — | ใช้อัตโนมัติอย่างเดียว |
| มีค่า | false | ใช้อัตโนมัติ และพิมพ์เองก็ได้ |
| มีค่า | true | ไม่ใช้อัตโนมัติเด็ดขาด ต้องพิมพ์ |
แถวสามคือแถวที่คุ้มที่สุด โค้ดที่ให้ลูกค้าคนเดียวทาง LINE ต้องไม่ไปลดให้ตะกร้าคนอื่นเงียบ ๆ
โค้ดที่ไม่ตรงกับอะไรเลยคืน 422 พร้อมลิสต์โค้ดที่ผิด ไม่ใช่ทิ้งเงียบ คนที่พิมพ์โค้ดแล้วไม่เห็น error ไม่เห็นส่วนลด จะคิดว่าเว็บพัง โค้ดจะถูกตัดช่องว่างและแปลงเป็นตัวใหญ่ก่อนเทียบ เพราะสาเหตุเกือบทุกครั้งคือช่องว่างท้ายบรรทัด
ได้อะไรจากเรื่องนี้
บั๊กทั้งสองตัวไม่ใช่ crash ไม่ใช่ type error ไม่ใช่อะไรที่เทสต์จับได้ฟรี ๆ ทั้งคู่คือกฎที่พออธิบายเป็นภาษาคนแล้วกำกวม คำว่า "scope ว่าง" กับ "ลด ฿1" แล้วความกำกวมนั้นก็เดินเข้าไปอยู่ในโค้ดตรง ๆ
ตอนนี้กฎสองข้อนั้นอยู่เป็นคอมเมนต์ในไฟล์ schema เขียนเป็น ประโยคที่เคยถูกอ่านผิด ไม่ใช่คำอธิบายว่าโค้ดทำอะไร เป็นเอกสารชิ้นเดียวที่ผมเขียนแล้วพิสูจน์ได้ว่าหยุด regression ได้จริง