"คอลัมน์ไม่พอครับ" - โค้ดเบสที่ทั้งดีที่สุดและแย่ที่สุด
(jimmyhmiller.github.io)"ตาราง
merchants2เหรอ? ใช่ เราสร้างmerchants2เพราะคอลัมน์ของmerchantsไม่พอ"
- ตอนเริ่มเขียนโปรแกรมใหม่ๆ ผมไม่รู้เลยว่าคนสามารถหาเงินจากการเขียนโปรแกรมได้
- ผมได้เรียนรู้อะไรมากมายจากงานซอฟต์แวร์แห่งแรก แต่โค้ดเบสของที่นั่นทั้งแย่ที่สุดและดีที่สุดในเวลาเดียวกัน
ฐานข้อมูลอยู่ยงคงกระพัน
- ในระบบ legacy ฐานข้อมูลทำหน้าที่มากกว่าแค่ที่เก็บข้อมูล มันกำหนดข้อจำกัดของทั้งระบบและเป็นจุดที่โค้ดทั้งหมดมาบรรจบกัน
- SQL Server มีข้อจำกัดเรื่องจำนวนคอลัมน์ของตาราง ตอนนั้นคือ 1024 คอลัมน์ ปัจจุบันคือ 4096 คอลัมน์ คอลัมน์ของตาราง Merchants ไม่พอจึงสร้างตาราง Merchants2 ขึ้นมา (มีมากกว่า 500 คอลัมน์)
- Merchants และ Merchants2 คือแกนกลางของระบบ ทุกอย่างเชื่อมกับ Merchants มีตารางที่ normalize แล้วอื่นๆ อยู่ด้วย แต่ก็มีความสัมพันธ์แบบ foreign key กับ Merchants
SequenceKey
- SequenceKey เป็นตารางเรียบง่ายที่มีเพียงหนึ่งคอลัมน์และหนึ่งค่า
- มันถูกใช้เพื่อสร้าง ID คาดว่าน่าจะถูกทำขึ้นเพราะ SQL Server ในตอนนั้นยังไม่รองรับ auto-increment ID
- ใน stored procedure ทุกตัวจะดึงคีย์จาก SequenceKey แล้วเพิ่มค่า จากนั้นนำไปใช้เป็น ID ของหลายตาราง ทำหน้าที่เป็น implicit join ไปในตัว
Calendar
- Calendar เป็นตารางปฏิทินที่กรอกด้วยมือ ถ้า Calendar หมดอายุจะไม่สามารถล็อกอินเข้าระบบได้
- เรื่องนี้เคยเกิดขึ้นเมื่อไม่กี่ปีก่อน แล้วเด็กฝึกงานคนหนึ่งก็เติมข้อมูลเพิ่มให้อีก 5 ปี ไม่มีใครรู้ว่าระบบไหนใช้มันอยู่บ้าง
Employees
- ทุกเช้าเวลา 7:15 ตาราง employees จะถูกลบทิ้งแล้วเติมใหม่จาก CSV ที่ได้รับจาก ADP
- ระหว่างกระบวนการนี้จะไม่สามารถล็อกอินเข้าระบบได้ และบางครั้งงานนี้ก็ล้มเหลว
- เพราะข้อมูลต้องถูก replicate ไปยังสำนักงานใหญ่ จึงมีการส่งอีเมลไปให้คนคนหนึ่งเพื่อกดปุ่มคัดลอกข้อมูลทุกวัน
ฐานข้อมูลตัวทดแทน
- คุณอาจคิดว่าน่าจะจัดระเบียบฐานข้อมูลได้ และบริษัทก็คิดแบบนั้นเหมือนกัน
- มีสำเนาฐานข้อมูลอยู่ชุดหนึ่ง แต่ข้อมูลจะช้ากว่าของจริงประมาณ 10 นาที และซิงก์กันได้ทางเดียวเท่านั้น
- ฐานข้อมูลชุดนี้ถูก normalize ไว้ ดังนั้นถ้าจะหาเบอร์โทรศัพท์จาก merchants ต้อง join ถึง 7 ตาราง
ยอดขาย
- พนักงานขายทุกคนมีโควตารายเดือนที่เรียกว่า "win" ซึ่งต้องทำให้ได้
- ตารางที่ใช้จัดการเรื่องนี้ซับซ้อนมาก ทุกวันจะมีงานรันเพื่อหาว่ามีแถวไหนถูกเพิ่มหรือแก้ไข แล้วซิงก์กับระบบของสำนักงานใหญ่
- ปัญหาเริ่มขึ้นเมื่อพนักงานขายคนหนึ่งขอให้แก้เรคคอร์ดด้วยมือ
- เขาทำ win ได้ครบแล้ว และยังปิดการขายก้อนใหญ่อีกในเดือนนั้น เขาเลยอยากย้ายยอดนั้นไปเดือนถัดไป
- เด็กฝึกงานคนหนึ่งรับงานนี้ไปทำ ข่าวลือนี้แพร่กระจาย และตลอด 3 ปีหลังจากนั้นคำขอแบบนี้ก็เพิ่มขึ้นแบบทวีคูณ
- ช่วงหนึ่งมีเด็กฝึกงาน 3 คนที่ทำแค่เขียน SQL statement สำหรับเรื่องนี้ การสร้างแอปไว้จัดการโดยเฉพาะถูกมองว่ายากเกินไป
โค้ดเบส
- โค้ดเบสแรกที่ผมเจออยู่บน Team Foundation Server ซึ่งเป็นระบบ version control แบบรวมศูนย์
- โค้ดเบสที่ผมทำงานด้วยเป็น VB ครึ่งหนึ่ง และ C# อีกครึ่งหนึ่ง รันอยู่บน IIS และใช้ session state ไปทั่วทั้งระบบ
- นั่นหมายความว่า ถ้าคุณเข้าหน้าเดียวกันผ่าน Path A หรือ Path B คุณอาจเห็นสิ่งที่ต่างกันมากบนหน้านั้น
- JavaScript framework แทบทุกตัวที่มีอยู่ในยุคนั้นถูก check in ไว้ใน repo นี้ พร้อมกับการแก้ไขแบบ custom ที่ผู้เขียนคิดว่าจำเป็น โดยเฉพาะ knockout, backbone, marionette รวมถึง jquery และปลั๊กอินของ jquery
- นอกจากโค้ดเบสนี้แล้ว ยังมี SOAP service ราว 12 ตัว และ Windows application แบบ native อีกหลายตัว
ฮาร์ดไดรฟ์ของ Gilfoyle
- Gilfoyle เป็นที่รู้จักว่าเป็นโปรแกรมเมอร์ที่เร็วมาก ผมไม่เคยเจอเขาเป็นการส่วนตัว แต่ผมรู้จักเขาผ่านโค้ดและสิ่งที่เหลืออยู่บนฮาร์ดไดรฟ์ของเขา
- Munch เก็บฮาร์ดไดรฟ์ของ Gilfoyle ไว้บนโต๊ะในชุด RAID แม้เขาจะลาออกจากบริษัทไปหลายปีแล้วก็ตาม
- เพราะ Gilfoyle มีชื่อเสียงเรื่องไม่ check in โค้ด และชอบทำ Windows application ใช้ครั้งเดียวแบบสุ่มสำหรับผู้ใช้คนเดียว
- ไม่ใช่เรื่องแปลกที่ผู้ใช้จะนำ bug report ของแอปที่มีอยู่แค่บนฮาร์ดไดรฟ์ของ Gilfoyle มาให้
บั๊กการจัดส่ง
- งานส่วนใหญ่ของผมคือไล่ตามบั๊กที่ทีมไม่อยากหยิบไปทำ
- มีบั๊กตัวหนึ่งที่น่ารำคาญมากและเกิดขึ้นทุกๆ สองสามเดือน เป็นกรณีที่มีออเดอร์ค้างอยู่ในคิวจัดส่งหลังจากที่ถูกส่งไปแล้ว ทำให้มันดูเหมือนทั้งจัดส่งแล้วและยังไม่ได้จัดส่งพร้อมกัน
- เพื่อแก้ปัญหานี้ เราลองมาหลายวิธี ทั้ง SQL script และ Windows application ผมถูกแนะนำว่าอย่าไปไล่หาสาเหตุราก แต่ผมทนไม่ได้
- ระหว่างทางผมได้เรียนรู้วิธีคิดของ Gilfoyle แอปจัดส่งจะดึงทั้งฐานข้อมูลมา แล้วค่อยกรองตามวันที่เพื่อเก็บออเดอร์ทั้งหมดตั้งแต่วันที่เริ่มต้นของแอป
- แอปนี้พึ่งพา SOAP service แต่ไม่ใช่เพื่อทำงานแบบ service มันเป็นเพียง pure function โดยที่ฝั่ง client เป็นตัวก่อให้เกิด side effect ทั้งหมด
- ใน client ตัวนั้น ผมเจอโครงสร้าง class hierarchy ขนาดมหึมา มี 120 คลาส แต่ละคลาสมีเมธอดหลายตัว และมีการสืบทอดลึกลงไปถึง 10 ชั้น
- ปัญหาอย่างเดียวคือ เมธอดทั้งหมดว่างเปล่า
- สิ่งนี้ถูกสร้างขึ้นเพื่อทำโครงสำหรับใช้ reflection โดย reflection นั้นจะสร้างสตริงคั่นด้วย pipe (โครงสร้างอิงจากฐานข้อมูลแต่เป็นแบบ static ทั้งหมด) แล้วส่งออกไปทาง socket
- สุดท้ายมันถูกส่งไปยัง Kewill ซึ่งเป็น service ที่ใช้สื่อสารกับผู้ให้บริการขนส่ง สาเหตุของบั๊กคือ Kewill นำเลข 9 หลักกลับมาใช้ซ้ำทุกเดือน และมีใครบางคนไปปิด cron job ที่ใช้ลบออเดอร์เก่า
ความโกลาหลที่งดงาม
- ยังมีเรื่องให้เล่าเกี่ยวกับโค้ดเบสนี้อีกมาก ทั้งทีม senior developer ที่ใช้เวลา 5 ปี rewrite ทั้งระบบโดยไม่ปล่อยโค้ดเลย หรือที่ปรึกษาจาก Red Hat ที่จะมาสร้างฐานข้อมูลหนึ่งเดียวเพื่อควบคุมทุกอย่าง
- โค้ดเบสนี้มีมุมเพี้ยนๆ เต็มไปหมด และมีเหตุผลมากพอที่จะตั้งทีมเฉพาะขึ้นมาเพื่อเริ่มฟีเจอร์เดี่ยวๆ ใหม่ตั้งแต่ต้น
- แต่เรื่องที่สำคัญที่สุดคือ Justin ปรับปรุงหน้า Merchants Search ซึ่งเป็นจุดเริ่มต้นของทั้งแอปพลิเคชัน
- เจ้าหน้าที่บริการลูกค้าทุกคนจะคุยโทรศัพท์กับร้านค้าไปพร้อมกับพิมพ์ ID หรือชื่อเพื่อค้นหาข้อมูล แล้วระบบจะพาไปยังหน้าขนาดใหญ่ที่มีข้อมูลทุกอย่างอยู่ในนั้น
- หน้านี้อัดแน่นไปด้วยข้อมูลที่จำเป็นทั้งหมดและลิงก์ทุกจุดที่อยากเข้า ถือว่าแน่นข้อมูลในแบบที่ดีที่สุด แต่ก็ช้ามาก
- Justin เป็น senior developer เพียงคนเดียวในกลุ่มของผม เขาฉลาด พูดจาเสียดสี และไม่ค่อยสนใจธุรกิจเท่าไร
- เขาพูดตรงๆ ไม่อ้อมค้อม และมักแก้ปัญหาคนเดียวได้เร็วกว่าทีมรอบข้างเสมอ
- วันหนึ่ง Justin เบื่อที่จะต้องฟังว่าหน้า merchant search ช้าแค่ไหน เขาเลยไปแก้มัน
- กล่องข้อมูลทุกกล่องบนหน้าจอถูกแยกออกเป็น endpoint ของตัวเอง ตอนโหลดหน้า ข้อมูลทั้งหมดที่อยู่เหนือ fold จะเริ่มถูกดึงก่อน และเมื่ออันหนึ่งโหลดเสร็จก็จะมี request เพิ่มเติมตามมา
- เวลาโหลดหน้าลดจากหลาย分钟เหลือไม่ถึง 1 วินาที
การ decouple สองรูปแบบ
- ที่ Justin ทำแบบนี้ได้ก็เพราะโค้ดเบสนี้ไม่มี master plan
- ไม่มีพิมพ์เขียวคร่าวๆ ที่ระบบต้องเข้ารูป ไม่มีรูปแบบ API ที่คาดหวังไว้ ไม่มี design system ที่มีเอกสาร และไม่มีคณะกรรมการตรวจสถาปัตยกรรมที่คอยรับประกันความสม่ำเสมอ
- แอปนี้เละเทะโดยสมบูรณ์ ไม่มีใครซ่อมมันได้ จึงไม่มีใครพยายามจะซ่อมมัน แทนที่จะทำแบบนั้น เราสร้างโลกเล็กๆ ที่มีเหตุมีผลของตัวเองขึ้นมา
- แอปแบบ monolithic นี้ค่อยๆ เติบโตกลายเป็นจักรวาลย่อยของแอปเล็กๆ ที่ดีตามขอบนอก เพียงเพราะความจำเป็นล้วนๆ
- ใครก็ตามที่ได้รับมอบหมายให้ปรับปรุงส่วนหนึ่งของแอป สุดท้ายก็มักเลิกพยายามแก้ใยแมงมุมเดิม แล้วไปหามุมเล็กๆ ดีๆ เพื่อสร้างของใหม่ จากนั้นค่อยๆ อัปเดตลิงก์ให้ชี้ไปยังของใหม่ที่ดีกว่า และปล่อยของเก่าให้กลายเป็นกำพร้า
- ฟังดูอาจจะยุ่งเหยิง แต่กลับทำงานด้วยแล้วสนุกอย่างน่าประหลาด ความกังวลเรื่องโค้ดซ้ำหายไป ความกังวลเรื่องความสม่ำเสมอก็หายไป ความกังวลเรื่อง scalability ก็หายไป
- โค้ดถูกเขียนมาเพื่อการใช้งาน เขียนโดยแตะพื้นที่รอบข้างให้น้อยที่สุด และเขียนให้เปลี่ยนทดแทนได้ง่าย โค้ดของเราถูก decouple เพราะการ couple มันยากกว่าเสียอีก
หลังจากนั้น
- หลังจากนั้นมา ในเส้นทางอาชีพของผม ผมไม่เคยมีโอกาสได้ทำงานกับโค้ดเบสที่ทั้งน่าเกลียดและน่าทึ่งขนาดนั้นอีกเลย
- โค้ดเบสน่าเกลียดทุกตัวที่ผมเจอหลังจากนั้น ไม่เคยก้าวข้ามความต้องการเรื่องความสม่ำเสมอได้
- บางทีอาจเป็นเพราะนักพัฒนาที่ "จริงจัง" ทิ้งโค้ดเบสนั้นไปนานแล้ว เหลือไว้แค่เด็กฝึกงานกับ junior developer ที่ค่อนข้างสะเปะสะปะ
- หรืออาจเป็นเพราะไม่มีชั้นกลางคั่นระหว่างนักพัฒนากับผู้ใช้ ไม่มีการแปลความ ไม่มีการเก็บ requirement ไม่มีการ์ดงาน มีเพียงคุณยืนอยู่หน้าโต๊ะของเจ้าหน้าที่บริการลูกค้าแล้วถามว่าจะทำให้ชีวิตพวกเขาดีขึ้นได้อย่างไร
- ผมคิดถึงความเชื่อมโยงโดยตรงแบบนั้น คิดถึง feedback ที่รวดเร็ว การไม่จำเป็นต้องวางแผนใหญ่โต และความเชื่อมต่อที่เรียบง่ายระหว่างปัญหากับโค้ด
- บางทีมันอาจเป็นแค่ nostalgia แบบไร้เดียงสา แต่เหมือนกับที่ผมอยากย้อนกลับไปยังช่วงเวลาที่แย่ที่สุดบางปีในวัยเด็ก ทุกครั้งที่ต้องเผชิญกับ "enterprise design pattern" ใจของผมก็จะย้อนกลับไปหาโค้ดเบสที่ทั้งงดงามและน่าสยดสยองนั้นเสมอ
ความเห็นของ GN⁺
- บทความนี้อาจให้ทั้งความรู้สึกร่วมและความสบายใจกับนักพัฒนาที่ต้องรับมือกับระบบ legacy เพราะมันแสดงให้เห็นว่าแม้โค้ดเบสจะไม่สมบูรณ์แบบ ก็ยังมีคุณค่าและมีสิ่งให้เรียนรู้อีกมาก
- อย่างไรก็ตาม ปัญหาในโค้ดเบสนี้ไม่ควรถูกทำให้ดูโรแมนติก เมื่อ technical debt สะสมมากขึ้น ความเร็วในการพัฒนาจะลดลงอย่างมากและการบำรุงรักษาจะยากขึ้น ในระยะยาวมันมีต้นทุนสูงกว่าเดิมมาก
- การยอมแพ้ต่อความพยายามในการปรับปรุงโค้ดเบสแล้วเดินหน้าสะสมวิธีแก้เฉพาะหน้าไปเรื่อยๆ ไม่ใช่คำตอบ จำเป็นต้องมีกลยุทธ์ในการค่อยๆ ปรับปรุงหรือย้ายระบบ legacy
- วัฒนธรรมและกระบวนการพัฒนาก็สำคัญ การทำ code review การออกแบบสถาปัตยกรรม และการเขียนเอกสารตามแนวปฏิบัติทางวิศวกรรมที่ดี ล้วนช่วยให้สร้างโค้ดเบสที่ดีกว่าได้
- การสื่อสารอย่างใกล้ชิดกับผู้ใช้และ feedback ที่รวดเร็วเป็นข้อดี สิ่งที่เหมาะที่สุดคือส่งเสริมสิ่งนี้ผ่านวิธีการอย่าง Agile ควบคู่ไปกับการดูแลคุณภาพโค้ด
- ท้ายที่สุดแล้ว ทุกอย่างคือเรื่องของความสมดุล แทนที่จะไล่ตามความสมบูรณ์แบบ สิ่งสำคัญคือการตอบสนองความต้องการของผู้ใช้อย่างยั่งยืน พร้อมกับจัดการ technical debt ไปด้วย
3 ความคิดเห็น
งานที่ได้รับในบริษัทแรกตอนทำงานเป็นนักพัฒนาเฟิร์มแวร์ก็คือ วิเคราะห์แอสเซมบลีโค้ดที่ดึงออกมาจากไฟล์ hex ของ 8051 MCU ของผลิตภัณฑ์ที่ไม่มีทั้งนักพัฒนาและซอร์สโค้ด แล้วนำกลับมาเขียนใหม่เป็นภาษา C นี่แหละครับ…
อย่างน้อยก็ยังมีผลิตภัณฑ์ที่ใช้งานได้อยู่ เลยค่อย ๆ ดูโค้ดแล้วทดสอบกับตัวผลิตภัณฑ์ไปด้วย จนพอทำมันสำเร็จได้แบบทุลักทุเล…
เคยโดนขู่ว่าถ้าไปทริปทำงานต่างจังหวัดแล้วซ่อมไม่ได้ก็ให้ตัดนิ้วทิ้งแล้วค่อยกลับมาด้วย
แล้วก็เคยมีบั๊กที่หาสาเหตุไม่เจอ ซึ่งที่จริงแล้วต้นตอมาจากลิฟต์ที่อยู่หลังผนังตรงจุดติดตั้งอุปกรณ์
ยังจำได้เลยว่าเคยเข้าไปติดตั้งโน่นนี่ในสถานที่ประชุม APEC ที่เกาะดงแบ็ก เมืองปูซาน ก่อนเปิดใช้งานอย่างเป็นทางการด้วย ฮ่าๆ
ความคิดเห็นจาก Hacker News
เคยดูแลแอปพลิเคชัน VB ที่ซับซ้อนในบริษัทแรก
ในงานแรก เคยบำรุงรักษาผลิตภัณฑ์ legacy ที่เขียนด้วย COBOL และ Java
เคยรีแฟกเตอร์สคริปต์ Perl ที่ยาวกว่า 12,000 บรรทัด
รู้สึกถึงความต่างระหว่างทฤษฎีกับความเป็นจริง
โค้ดเบสของไคลเอนต์ Telegram บน Android มีความซับซ้อนมาก
ในงานแรก เคยแก้ปัญหา memory ของงานทำรายงานได้
ชอบประสบการณ์ที่ได้คุยกับลูกค้าโดยตรงและแก้ปัญหาให้
เคยสร้างระบบที่รองรับหลายตลาด
ในงานแรก มีคนที่ประสบการณ์น้อยอยู่มาก
มีการอธิบายวิธีแก้ปัญหาใน SQL Server สมัยใหม่
ตารางกำหนดเลขลำดับ (SequenceKey) กับตารางวันทำการ (Calendar)
ชวนให้หวนคิดถึงความหลังเลยครับ ตอนนี้ไม่แน่ใจว่าทำกันอย่างไรแล้ว แต่เมื่อก่อนเป็นตารางที่ใช้กันทั่วไป ถ้าทำ SI ฝั่งส่วนงานกลางของระบบก็มักจะพัฒนาฟังก์ชันที่เกี่ยวข้องไว้ครับ