อันนี้เป็นเรื่องที่เกิดขึันในที่ประชุมวันนี้ครับ คือเรามีบั๊กตัวหนึ่งที่เกิดตอนสร้าง transaction โดยที่อันนี้เป็นตอนที่สร้างบันทึกการซื้อขายหลาย ๆ record พร้อม ๆ กันบน frontend แล้วระบบจะต้องไปหาคำสั่งการโอนเงินมาให้การซื้อขายแต่ละอัน โดยคำสั่งการโอนเงินจะต้องเข้ากับการซื้อขายนั้น ๆ แต่เราพบว่าคำสั่งการโอนเงินอันแรกสุดถูกใช้ในทุก ๆ การซื้อขายเลย
ที่มาเกิดจากโค๊ดหน้าตาแบบนี้ครับ อันนี้เขียนเป็น Java เพื่อให้อ่านเข้าใจง่ายขึ้น
BankTransferInstruction instruction = null;
for(Transaction transaction: transactions) {
if(instruction == null) {
instruction = findBankTransferInstruction(transaction);
}
transaction.setBankTransferInstruction(instruction);
}
เห็นบั๊กหรือยังครับ ? ถ้ายังลองคิดดูสักห้านาทีนะครับ
โค๊ดที่แก้ไขบั๊กนี้จริง ๆ ง่ายมากเลย แค่นี้เอง
for (Transaction transaction : transactions) {
BankTransferInstruction instruction = findBankTransferInstruction(transaction);
transaction.setBankTransferInstruction(instruction);
}
น่าจะเห็นแล้วนะครับว่า โค๊ดที่มีปัญหามีการมองหาคำสั่งการโอนเงินแค่ครั้งแรกครั้งเดียว (คือไม่เป็น null
) ส่วนเราต้องการให้หาทุกครั้ง เราก็แค่สั่งให้มันหาใหม่เท่านั้นเอง
วันนี้มีการคุยกับเรื่องนี้ แล้วก็มีคนเสนอว่า เฮ้ย ทำไมเราไม่ optimize เพิ่มเติมอีกสักหน่อย เอาแบบนี้ …
BankTransferInstruction instruction = null;
for(Transaction transaction: transactions) {
if(instruction == null ||
instruction.isApplicable(transaction) {
instruction = findBankTransferInstruction(transaction);
}
transaction.setBankTransferInstruction(instruction);
}
เพิ่มเงื่อนไขมาอีกอัน ก็น่าจะทำให้เร็วขึ้นนะ เพราะไม่ต้องหา instruction
ใหม่บ่อย ๆ ใช่ไหมครับ ? ถ้าเรามองกันที่ performance อย่างเดียวก็แน่นอนว่ามันเร็วขึ้นอยู่แล้ว (แต่ก็มีกรณีที่ทำให้ช้าลงได้เหมือนกันครับ เช่นถ้า isApplicable()
ดันต้องเรียก database ใหม่ จะกลายเป็นว่าในกรณีที่แย่ที่สุดจะต้องเรียก database สองครั้งต่อหนึ่ง transaction)
คำถามคือ มันจำเป็นต้องทำไหม ?
สิ่งแรกที่เราต้องประเมินกันก่อนที่จะทำ optimization คือ ณ.จุดนั้นมันเป็นปัญหาจริง ๆ หรือเปล่า ? อย่างอันนี้ในกรณีทั่ว ๆ ไปของระบบผม ลูกค้าจะไม่ค่อยสร้างคำสั่งซื้อหรือขายพร้อม ๆ กันมาก ๆ คือ ซื้อพร้อม ๆ กันสักสิบรายการก็แฮ่กแล้วครับ ดังนั้นจำนวนมันจะน้อยมาก ๆ ถ้ามันไม่ได้เป็นคอขวดจริง ๆ เราไม่จำเป็นจะต้องไปแก้ครับ
สิ่งที่สองคือ เราจะใช้ effort มากขนาดไหนในการ optimize อย่างกรณีนี้ถ้าเราเขียนฟังก์ชันใหม่ เราก็ต้องทดสอบ isApplicable()
ว่าทำงานอย่างถูกต้องจริง ๆ เผลอ ๆ เราต้อง refactor หลาย ๆ ฟังก์ชันเพื่อโค๊ดดูแลรักษาได้ง่ายขึ้น (ใคร copy โค๊ดจาก findBankTransferInstruction()
มาใช้ตรง ๆ นี่ผมด่าจริง ๆ นะเออ) ดังนั้นเราก็ต้องทดสอบหลาย ๆ ฟังก์ชันว่าทำงานถูกใหม่ ถ้าโค๊ดใช้ unit test ก็ต้อง refactor ตัว unit test ด้วย อะไรแบบนี้ ซึ่งตรงนี้มันก็ใช้เวลา
ถึงจุดนี้ ถ้าเราตัดสินใจจะทำจริง ๆ สิ่งที่เราต้องทำต่อคือ ลอง benchmark แยกกัน ระหว่างโค๊ดเก่า กับโค๊ดใหม่ ซึ่งในขั้นนี้อาจจะเป็นโค๊ดที่ยังไม่สมบูรณ์ก็ได้ครับ เอามาเทียบกัน แล้วดูว่า ความแตกต่างกันมีมากน้อยแค่ไหน และประเมินต่อว่า เมื่อโค๊ดใหม่เข้าไปในระบบแล้วจะช่วยให้เราประหยัดเวลาไปได้มากน้อยแค่ไหนในระยะเวลาหนึ่ง (สมมติว่าหนึ่งเดือนหรือหนึ่งปีก็ได้) แล้วดูว่าถ้าจะ implement ต่อจนเสร็จ ทดสอบเรียบร้อย ผ่าน Function Testing และ UAT ไปแล้วจะต้องใช้เวลามากน้อยแค่ไหน ใช้งบประมาณเท่าไหร่
ผมอาจจะประเมินคร่าว ๆ ว่า หน้าคำสั่งซื้อขายนี้เนี่ย ผู้ใช้หนึ่งคนอาจจะใช้เวลากับมันประมาณ 1 ชั่วโมงต่อวัน หรือคิดเป็น workload 12.5% ถ้าผมสามารถลดเวลามันลงไป 15 นาที ก็เท่ากับว่าผมสามารถทำให้ efficiency ของผู้ใช้คนนี้เพิ่มขึ้น 25% ของ 1 ชั่วโมงจาก 8 ชั่วโมงต่อวัน ก็จะเป็น 25% * 12.5% = 3.125% ซึ่งผมว่าตัวเลขที่มากกว่า 1% ก็คุ้มค่าที่จะลงทุนนะครับ อันนี้แก้ไปเถอะ
แต่ถ้าแบบ ลดเวลามันลงไปได้ 3 นาที … ก็เท่ากับเพิ่มได้แค่ 0.625% อันนี้ก็ต้องเริ่มดูละว่ามันคุ้มจริง ๆ หรือเปล่า ?
ทั้งนี้อย่าลืมว่าเราต้องเอาตัวเลขนี้มาเทียบกับว่า ทำแล้วทำให้โค๊ดเราดูแลยากขึ้นมากแค่ไหน เพิ่มปัจจัยเสี่ยงให้กับระบบมากแค่ไหน ก่อนจะตัดสินใจว่าจะทำหรือไม่ทำนะครับ
อย่างกรณีผมที่ทำงานกับ Legacy Code เป็นหลัก โค๊ดไม่ได้เขียนเป็น procedure มากนัก ภาษาก็เก่า โอกาสที่พลาดสูง และไม่มี unit test เนี่ย ผมมองว่ามันเป็นการเพิ่มปัจจัยเสี่ยงมากเกินไปและผลตอบแทนก็ไม่คุ้ม ก็เลยแย้งไปว่าอย่าทำเลยดีกว่า …
ทั้งนี้การทำ optimization ก่อนที่จะประเมินเรื่องผลตอบแทนและค่าใช้จ่ายนั้น เป็นการทำ premature-optimization รูปแบบนึงครับ (อีกรูปแบบนึงคือโค๊ดยังไม่เป็นรูปเป็นร่างเลยแต่ทำ optimize แล้ว)
โปรแกรมเมอร์ C++ และผู้นิยมดนตรี