Backward Compatibility เป็นฝันร้ายจริงไหม ?

Tae Keerati Jearjindarat
5 min readOct 31, 2021

กรุยทาง

เคยได้ยินคำว่า “Backward Compatibility เป็นฝันร้ายสำหรับ Developer ไหมครับ ?”

คำถามคือ มันคืออะไร และทำไมถึงฝันร้าย แต่นั่นแหละครับเรื่องนี้ผมหาไม่ได้ในตอนเรียน คือในช่วงที่ผมยังเรียน ก็ได้ทำงานเยอะทั้งในวิชา นอกวิชา และงานนอก Freelance ซึ่งมันไม่เคยเจอจริงๆ เพราะการที่จะทำเรื่องพวกนี้ได้ มันต้องเกิดการเปลี่ยนแปลง Business Requirement อะไรสักอย่างแล้วมีการแก้โค้ดที่ไปกระทบกับระบบเก่า ถึงจะได้เรียนรู้จริงๆจังๆ

ตัวอย่างเช่น

ตอนเรียนทำ Senior Project จบ แล้วก็จบเลิกลากันไป บางคนมันอาจจะไม่ได้ทำแล้วทิ้ง แต่ขอเป็นกรณีที่น้อยคนที่จะเอามาทำต่อ ทีนี้ประเด็นมันอยู่ที่ตอนทำ Senior Project จบมันไม่ได้มีการเปลี่ยนแปลงอะไรเยอะมาก สิ่งที่เจอ Business Requirement ที่ค่อนข้างตายตัว คือทำให้ไม่ได้แก้อะไรมากมายตอนที่ Deploy หรือส่งงานไปแล้วน่ะครับ ถึงจะมีแก้ไปสุดท้ายก็น้อยมากที่จะได้มีคนใช้จริงๆ ซึ่งพอไม่มีคนใช้ (หรือใช้แล้วทิ้ง) เราก็ไม่ต้องมาแคร์เรื่องเวอร์ชั่นเก่ากับใหม่กันมากมายอะไรนัก

หรือการทำ Freelance ของผมก็เช่นกัน งานที่ทำก็เป็น Project based สิ่งที่ทำอาจจะเป็นเพิ่ม Requirement อย่างเดียว เช่น เพิ่มคำหน่อย เพิ่มระบบจัดการสินค้าหน่อย เพิ่มระบบจับคู่หน่อย แต่มันไม่ได้เกิดการแก้ของเก่าที่ต้องคำนึงถึง Compatibility เลย

ทีนี้พอมาทำงานในบริษัทค่อนข้างใหญ่ มีผู้ใช้หลายแสนคน ทำให้เราต้องเรียนรู้เรื่องนี้ให้เข้าใจ และทำให้เราเองต้องสนใจเรื่องนี้สำคัญเป็นอันดับต้นๆ ในการออกแบบซอฟต์แวร์เลยครับ อยากให้ทุกคนเข้าใจและได้อะไรกลับไปบ้าง วันนี้เลยจะมาแชร์ให้ฟังกัน !

ทำไมเราต้องสนใจล่ะ ?

เวลาเราทำ Web Application หรือ Software ใดก็ตามที่อาจจะเพิ่งเริ่มหรือทำมานานแล้ว สิ่งที่เราจะเจอเหมือนกันก็คือเรามักจะมีคนที่เรียกใช้กับคนที่คอยให้บริการ ลักษณะคือมี Consumer ที่มีการเรียกใช้ Provider นั่นเอง เวลาที่ Consumer เรียกใช้ Provider มันจะเกิดสิ่งที่ควรเป็นข้อผูกมัดกัน (Contract) และข้อผูกมัดนี้ ไม่ควรจะเปลี่ยนแปลง โดยไม่บอกกล่าวกันก่อน ซึ่งควรจะมีแผนและวิธีการที่รอบคอบหากต้องการเปลี่ยนแปลงหรือแก้ไขจริงๆ เพราะถ้าหากแก้ไขโดยไม่สนใจข้อผูกมัดเดิม อาจจะทำให้ระบบฝั่งคนเรียกใช้ พังเป็นวงกว้างได้

ตัวอย่างเช่น

  • ถ้าเรามี Server และมี Mobile Application มาเรียกขอข้อมูลสินค้าไปแสดง Flash sale ทำให้เรามีรายได้จากการสามารถส่งมอบ Software 1M บาท ซึ่งเราปล่อยไปแล้ว 5 เดือน แต่วันหนึ่งมี Engineer ที่ทำ Server ดันเปลี่ยนชื่อ Field จาก productName เป็น product_name ทำให้ Mobile ไม่แสดงชือสินค้าซะงั้น สุดท้ายเกิดบัคที่แสดงผลผิดพลาด
  • ถ้าเรามีระบบที่คอย Sync File CSV ที่มี 5 Column เพื่ออัพเดทข้อมูลสินค้า โดยไฟล์นี้ Sync มาจาก Partner ของเราเอง แต่อยู่ดีๆ มีคนของฝั่ง Partner เพิ่ม Column มา หรือสลับข้อมูลในแต่ละ Column ก่อนวันหยุด ส่งผลให้ระบบของเราที่เคยมีอยู่ดันทำงานกับ File ไม่ได้ไป 3 วันเพราะส่งข้อมูลมาเกินหรือแอบสลับข้อมูลมา ทำให้เสียหายเป็นวงกว้างเช่นกัน
  • อีกกรณีที่อยากพูดถึงคือเราจะทำยังไงถ้าปกติแอปเราสามารถรันได้แค่บน CPU Intel แต่เราต้องใช้ M1 ในการพัฒนาเพราะ Technology ที่ใช้รัน Instruction ไม่เหมือนกัน และต้องจำใจใช้เพราะบริษัทซื้อคอมให้ใหม่นั่นเอง
  • ลองเอาตัวอย่างในชีวิตประจำวันก็ได้ครับ ปกติบ้านเรามีปลั้กไฟอยู่แล้ว แต่ปลั้กบ้านเราเป็น รูแบบ US แต่ไอโคมไฟที่เพิ่งซื้อมามีหัวแบบ EU ซึ่งมันเสียบกันไม่ได้ เราจะทำไงกันดีครับ ฮ่าๆ อันนี้ขอเก็บไว้ก่อนว่าจะแก้ยังไง แต่ขอบ่นของที่เพิ่งซื้อแต่งบ้านหน่อย ซึ่งถ้าเรามองปัญหาออก จริง ๆ มันไม่จำเป็นต้องเจอในตอนเรียนก็ได้

ซึ่งจริงๆ แล้ว เรื่อง Backward Compatibility เราจะเจอได้ไม่ยาก และ มันมีปัญหาอยู่ในทุกที่ถ้าเรามองเห็นมันซึ่งปัญหามันจะเกิดขึ้นตอนที่เราพยายามแก้อะไรบางอย่าง โดยที่เราไม่ได้สนใจสิ่งที่เคยมีอยู่ว่ามันจะกระทบยังไง หรือ มันจะใช้ร่วมกันได้หรือไม่ มันจะส่งผลอะไรหรือเปล่า

ซึ่งเวลาที่เราผลิตซอต์ฟแวร์ก็เหมือนกัน เราควรจะ Aware ไว้ให้มากๆ ให้ความสนใจและให้ความสำคัญหน่อยว่าสิ่งที่เรากำลังจะทำไป ณ วันนี้ มันอาจจะใช้กับของเก่าที่เคยมีอยู่ไม่ได้นะ มันจะกระทบอะไรบ้าง เราจะต้องแก้ไขยังไง ให้ถูกต้องและราบรื่น หรือ ถ้าให้ดีคือเราจะป้องกันยังไง และทำยังไงให้สามารถรับรู้ถึงปัญหาได้ ถ้าเกิดข้อผิดพลาดจริงๆ เพราะมันจะง่ายขึ้นเวลาเราทำ Software ต่อไปเรื่อยๆ และไม่เสียเวลากับของเก่า ซึ่งมันจะส่งผลต่อ pace ในการ Delivery งานของเราครับ

Backward Compatibility คืออะไร ?

ให้นึกถึงตอนที่เรามีระบบเดิมอยู่แล้ว และเราพยายามจะเพิ่ม Feature ใหม่ หรือ สร้าง Service ใหม่เพื่อต่อกับ Architecture เดิม สิ่งที่เราต้องทำงานต่อคือดูว่าสิ่งที่กำลังจะทำจะกระทบระบบไหนบ้าง ถ้าหากกระทบแล้วมันส่งผลเสียหรือไม่ ผลเสียในที่นี้ก็คือระบบเก่าพังไปเลย ไม่สามารถใช้งานได้ ซึ่งสามารถเกิดได้จากหลายสาเหตุ เช่น

  • เปลี่ยนชื่อ Field JSON
  • ทำของที่เป็น Optional กลายเป็น Mandatory (จากที่เคยมีบ้างไม่มีบ้างกลายเป็นมีตลอด)
  • มีการ Validate บางอย่างและโยน Exception เพิ่ม
  • เปลี่ยน Constraint หรือ Data Type
  • เปลี่ยนชื่อ Endpoints
  • เพิ่ม Security หรือเปลี่ยน Standard

ทั้งหมดนี้ลองคิดสนุกๆครับ ว่าทำไมถึง Breakchange 😃

ตัวอย่าง Real world ก็มีเช่น Mobile App Crash ไม่สามารถเปิดและใช้งานได้เพราะ API Spec ที่ Server โดนเปลี่ยน โดยที่ Mobile App ไม่ได้ Handle เอาไว้ ซึ่งทำให้ User ที่ใช้เวอร์ชั่นเก่าและไม่ได้ Update พังกันไป สุดท้ายเราก็ต้อง rollback การ deploy นั้นทิ้งและไปใช้ API Spec Version เดิม ส่วน API Spec ที่พยายามจะเปลี่ยนก็ต้องแก้ให้ใช้ได้เฉพาะ App Version ถัดไปแทน

เปลี่ยนชื่อ field ทำให้ breakchange app พัง
เปลี่ยนชื่อ field โดยที่ใช้เวอร์ชั่นเป็นตัวกำหนด ทำให้แอปเก่ายังใช้งานได้

จากตัวอย่างผมจึงสรุปได้ว่า Backward Compatibility ก็คือการทำยังไงก็ได้ให้ระบบเก่าของเดิมที่เรามีไม่พังและทำงานได้ปกติ ไม่ว่าเราจะเพิ่มอะไรเข้าไปใหม่ในระบบเดิม หรือ เราสร้าง Component หรือ Service ใหม่ เพื่อทำการต่อกับระบบเดิม

สิ่งที่จะทำให้พังก็คือ Contract ที่ตกลงกันไว้ ไม่เหมือนเดิม โดย Contract ก็คือสิ่งที่ได้จากการ Interaction กันระหว่าง Provider ที่เป็นผู้ให้ กับ Consumer ที่เป็นคนเอาไปใช้ เหมือนตัวอย่างในภาพก็คือมี Provider เป็น Server มี Version ต่าง ๆ และ Consumer ก็คือ Mobile App นั่นเอง และ Contract ก็คือ API Spec ที่เป็น JSON ซึ่งจริงๆ Contract จะเป็นอะไรก็ได้ เช่น XML, Payload ของ Message Queue, Proto File, Parameter, ชื่อ Endpoints หรือแม้กระทั่ง CSV Format

ในมุม Engineer ควรทำยังไงล่ะ ?

1. Aware เรื่องสำคัญดังนี้

  • นึกเรื่องนี้เอาไว้เสมอ คิดให้รอบคอบ ถ้ามีการเปลี่ยนแปลง Contract ต้องจำให้ขึ้นใจและเห็นความสำคัญ
  • ต้อง Test ดีๆ ทั้ง Manual Test และ Automated Test โดยเราควร Test ที่หลาย ๆ Layer เพื่อให้มั่นใจว่าจะไม่พัง และ บางจุดป้องกันได้ในเบื้องต้นหากมี Break change ซึ่งเดี๋ยวอธิบายเพิ่มเติมด้านล่างครับ
  • ต้อง Communicate ออกไปกับผู้ที่เกี่ยวข้อง เพราะ Contract ไม่ใช่เรื่องของคน ๆ เดียว แต่เป็นเรื่องของผู้ให้และผู้รับ ซึ่งผู้รับอาจจะไม่ได้มีคนเดียวก็ได้ เช่น Message Queue ที่อาจมีคน Subscribe 3 คน เช่นคนทำ Report, คนทำ Data Pipeline ไป Data Lake หรือ คนเอา Data ไปใช้คำนวน ซึ่งอาจจะเป็นคนละทีมที่ดูแลเลยก็ได้
  • ต้องมีแผนการ Migrate หากมีความจำเป็นต้อง Break change จริง ๆ เพราะ Business ไม่ได้นิ่งเสมอไป ต้องแก้ได้ ต้องทำให้รอบคอบ และ รัดกุม มีลำดับชัดเจนว่า Service ไหนต้อง Deploy ก่อน-หลัง เช่น Server ต้อง Deploy ก่อนเพื่อให้สามารถใช้งาน API ที่ทำขึ้นใหม่ได้ และค่อยปล่อย Mobile App เวอร์ชั่นใหม่ให้ไปเรียก API ที่ Server ทำขึ้นใหม่ เพราะถ้าหาก Mobile App ปล่อยก่อนและเรียกใช้ API เส้นใหม่ที่ยังไม่ Deploy จะทำให้ Mobile App ใช้งานไม่ได้เลย

2. ป้องกันตั้งแต่ตอนออกแบบระบบ

วิธีป้องกันไม่ยากขอแค่เราคิดไว้ตั้งแต่แรกครับว่าจะทำยังไงให้ระบบเราสามารถพัฒนาต่อไปได้อย่างราบรื่น สิ่งสำคัญก็คือเราต้องควบคุม Consistency ของ Contract โดยทำให้ Contract ทนทานต่อความเปลี่ยนแปลง หรือ ถึงแม้ Contract จะมีการเปลี่ยนก็ต้องออกแบบให้สามารถปรับเปลี่ยนกันได้ไม่ยาก ซึ่งโชคดีที่โลกนี้มีคนคิด Solution เอาไว้ให้เราทำตามได้ไม่ยาก ผมขอแบ่งเป็น Pattern ดังนี้ครับ

Adapter Pattern

  • นึกถึง กรณีปลั้กบ้านเป็นแบบ US แต่โคมไฟเป็นแบบ EU ครับ การที่เราจะใช้ได้คือมันต้องมีแปลงหัว US เป็น EU และเสียบคั่นกลาง ถึงทำให้ใช้ไฟได้ ซึ่ง Adapter Pattern เป็นแบบนี้เลย เพราะการแปลงจากสิ่งหนึ่งไปเป็นอีกสิ่งหนึ่งที่สามารถใช้กับระบบเราได้อย่างดี เราอาจจะพบกันในชื่อแนวๆ DTO, Mapper ซึ่งเป็นวิธี่ที่คล้ายหรืออาจจะเหมือนกันเลย แล้วแต่คนมอง เพราะเป็นการแปลง Contract ให้สามารถ Interaction ระหว่างระบบได้ ข้อเสียคือจะทำให้โค้ดเราซับซ้อนขึ้น แต่ที่แน่ๆ คือเราได้เรื่อง Single Responsibility หน้าที่ชัดเจนและ Open/Close Principle ที่ต่อยอดง่ายซึ่งเป็นเรื่องดีครับ
Thanks picture from https://th.aliexpress.com/item/1000001087105.html
  • โดยปกติเวลาเรายิง HTTP Request และใช้ Contract เป็น JSON เราจะต้องเอา JSON ไปทำ Business Logic ต่อ เช่น ใช้ Javascript ยิง API และใช้ Response ไปแสดงบนหน้าเว็บ ปัญหามันจะเกิดขึ้นถ้าเราเรียกใช้ JSON ตรงๆ แล้ว API มีการ Break change โดยไม่ได้ทำตามกระบวนการที่สมควร สุดท้ายพอเรียกใช้ตรงๆ แต่หา Field นั้นไม่เจอ มันก็จะขึ้นว่า undefined หรืออาจจะ Crash ไปเลยนั่นเอง สิ่งที่เราแก้และป้องกันได้คือ เราควรแปลง JSON ให้เป็น Object หรือ Data Type ตามภาษานั้นๆ เพื่อให้เอาไปใช้ใน Business Logic กันต่อไป โดยควรจะ Handle กรณี null หรือ undefined ให้ครอบคลุม เพื่อไม่ให้เกิดบัคแปลกๆ นั่นเอง
  • ซึ่งกรณีนี้รวมไปถึง Message Queue ด้วยเพราะ Payload จะถูก Serialized และส่งเป็น byte แล้วเราต้องมา Convert เอง แต่จริง ๆ ส่วนใหญ่มี Library ทำให้ ซึ่งต้องดูดีๆ ว่าพวก Library ฉลาดแค่ไหนเพราะอาจจะแปลงให้เฉยๆ แต่ไม่ได้เขียนป้องกันบัคเอาไว้ก็ได้ซึ่งถ้าเราไม่เข้าใจก็มีโอกาสหลุดรอดครับ

Versioning Strategy

Concept คือให้มีเลขเวอร์ชั่นกำหนดให้ชัดเจน ซึ่งเวอร์ชั่นต้องมี Pattern ที่ชัดเจนแจ่มแจ้ง โดยเราสามารถใช้ได้ทั้ง Request Params, Query String หรือ HTTP Header ที่เป็น Key กับ Value เฉพาะก็ได้ ตัวอย่างเช่น

  • รันตัวเลขไปเรื่อย ๆ
    localhost/api/v1/user/me
    localhost/api/v2/user/me
  • ใช้ semantic version ที่ประกอบไปด้วย major, minor และ patch version
    localhost/api/1.0.0/user/me
    localhost/api/1.5.0/user/me
  • ใช้วันที่ localhost/api/20211031/user/me
  • ใช้ Semantic ผสมกับวันที่ให้ส่งบน HTTP Header
GET localhost/api/user/me
Headers X-API-VERSION : 7.0.20211031

วิธีนี้ไม่ยากและได้ Consistency ค่อนข้างดีจากที่เคยทำมาครับ แต่อย่าสับสนกับ Version ของ Application นะครับ คิดแยกกัน วิธีก็แล้วแต่บริบทว่าเรา Interaction ระหว่างระบบแบบไหน

ส่วนกรณีของ Message Queue ก็สามารถใช้ Versioning ได้เช่นเดียวกันโดยเราสามารถใส่ Field ที่กำหนด Version เอาไว้ใน Payload แล้วเขียนโค้ด Check Version ก่อนว่าเป็น Version อะไร หลังจากนั้นค่อยใช้คู่กับ Adapter Pattern ในการแปลง Payload เป็น Object Version ที่ถูกต้อง

ต้อง Test ระดับไหนบ้าง ?

  • เราสามารถเขียน Unit Test หากใช้ Adapter Pattern ในโค้ดที่เป็นตัวเชื่อม Contract กับระบบเรา เนื่องจากตัว Design Pattern เราเขียน จะอยู่ในโค้ดอยู่แล้ว เราสามารถเขียน Unit Test เพื่อทดสอบ Logic การแปลง Contract ให้เป็นสิ่งที่เราต้องการได้ เช่น ถ้า Contract เป็น JSON หรือ XML แล้วเราต้องการแปลงเป็น Java Object เช่นบาง Field ที่เป็น Nullable เราอาจจะแปลงให้เป็น primitive ก็เขียน Test ไปตามที่ต้องการเลย ซึ่งจุดนี้ไม่ควรจะยากมาก เพราะเป็นวิธีที่ง่ายที่สุด
  • เราสามารถใช้ Integration Test ได้เช่นกัน แต่ไม่ควรทำท่า Mock เพราะเวลา Mock ไม่ได้เรียกใช้ของจริง จะมีโอกาสที่ Engineer จะลืมแก้ Spec ที่ Mock ซึ่งก็อยู่กับวินัยของทีมนั้นด้วย แต่ถ้าลืมจะทำจะจับบัคไม่ได้ เพราะไม่รู้ว่า Contract หรือ Spec ที่มีการเปลี่ยนแปลงทำให้พังหรือเปล่า ซึ่งถ้าไม่ Mock ส่งผลให้ Test ยากขึ้น แต่ถ้า Mock ก็จะ Test ง่ายแต่ต้องแก้วินัยให้ไม่เกิด Human Error ซึ่งก็ยากเช่นกัน
  • หรือจะใช้ Contract Testing ก็ได้ เนื่องจากเป็นการทดสอบตัว Contract ที่ Interaction กัน แบบ High Level ว่าตอนนี้ Contract ที่ใช้อยู่เป็นอย่างไร และตัวใหม่ที่จะเปลี่ยนมีผลทำให้ระบบพังหรือไม่ เช่น ดู JSON Schema หรือ CSV Column จะทำให้เราตรวจสอบได้ว่ามี Break อะไรหรือไม่ สามารถทำได้เร็วและง่ายครับ
  • สุดท้ายคือ End to End Testing (E2E) ก็คือไปทดสอบกันบนมุม User เลยว่า App พังหรือไม่พังตาม Testcases จริงๆ แล้วแต่ว่าจะแบ่ง Environment dev/beta ก็แล้วแต่จะทำ ซึ่งอันนี้ก็น่าจะทำกันอยู่แล้ว เพราะเวลาเราส่งมอบ Software แล้วเราก็ควรจะไปลองเล่นดู และ Monitor ต่อว่ามีอะไรแปลกไปหรือไม่ แต่ข้อเสียคือเป็นวิธีที่ค่อนข้างใช้เวลาเยอะ(มาก) เมื่อเทียบกับอันอื่นซึ่งทำได้ง่ายและเร็วกว่า
  • ทั้งหมดนี้เราทำ Layer ไหนบ้างแล้วแต่บริบทเลย ถ้าอยากมั่นใจแบบทดสอบกันแน่นๆ ก็ทำทุกอัน แต่ถ้าไม่เคร่งอะไรก็ทำแค่ Unit Test และ End to End ก็ไม่ได้ผิดอะไรแค่ความเสี่ยงเยอะขึ้นต้อง Trade off เอาเองครับ

สุดท้าย

  • ส่วนตัวอย่างที่บอก ผมคิดว่ามันไม่ได้เป็นฝันร้ายขนาดนั้น วิธีการอาจจะยากเพราะต้องคิดหลายตลบ คิดหน้าคิดหลังให้ดี และแก้ปัญหาอย่างรอบคอบ ผมนับว่าเป็น Challenge นึงที่ดีเลยทีเดียว
  • สำคัญที่เราห้ามลืมระบบเก่าเด็ดขาด เพราะถ้าลืมก็พังนั่นเองไม่ได้ยาก แต่ถ้าเรานึกได้ เดี๋ยววิธีแก้ไขก็จะมาเองครับ ขอให้มีสติและรู้ทันท่วงทีเวลาทำ Feature ใหม่ที่ทำต่อกับ Legacy นะครับ
  • ถ้าอ่านแล้วยังไม่อินในวันนี้ ก็ขอฝากเก็บไว้ในใจก่อน ผมเชื่อว่าสักวันจะต้องนึกถึงหรือต้องเจอแน่นอน ขอให้มีวันที่ดีและคิดถึงบทความนี้ในวันข้างหน้าครับ

เพิ่มเติม

--

--

Tae Keerati Jearjindarat

Hi, I'm Tae, Associate Engineering Manager at LINEMAN Wongnai. Thanks for following <3