Skip to content
varokas edited this page Apr 8, 2013 · 7 revisions

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

ซึ่งการทำความสะอาดนี้ ใน Agile มีเทคนิคนึงคือ refactoring ซึ่งจะเป็นการปรับโค้ดของเรา ให้ยังสามารถทำงานได้เหมือนเดิม แต่เป็นดีไซน์ที่ดีขึ้น และสามารถอ่านเข้าใจและแก้ไขได้ง่ายและรวดเร็ว ซึ่งแน่นอนว่าการที่เราจะแก้โค้ดได้อย่างมั่นใจว่ายังทำงานได้เหมือนเดิม สิ่งที่ช่วยเราได้อย่างมากคือการที่มีเทสกำหนดไว้ชัดเจนว่ามีอะไรที่สำคัญที่ต้องทำงานได้เหมือนเดิมบ้าง และตัวเทสก็มักจะเป็นตัวบอกเราเองว่า ดีไซน์เรามีจุดอ่อนอะไรบ้าง

สำหรับเรื่องอื่นลองอ่านได้เพิ่มเติมใน Refactoring: Improving the Design of Existing Code ของ Martin Fowler

หน้านี้จะเป็นการรวบรวมตัวอย่างที่เกิดขึ้นจริงในโปรเจคนี้ เป็นกรณีศึกษา

Case Study - Handling Duplicates

จากรูปจะเห็นเทสของ Core เป็นส่วนหลักของโปรแกรม รับผิดชอบ Business Logic หลักๆ และคุยกับ data layer โดยตรง ในรูปจะพบว่าทุกๆ function ใน core จะต้องมีการเรียกโค้ดที่จะต้องสร้าง data structure เปล่าๆ ขึ้นมาใน data layer ถ้ายังไม่เคยมี ให้ลองนึกถึง hashmap ใน Java ที่ถ้าเราจะไป get อะไรที่มันไม่เคยมีอยู่ มันก็จะ return null มาให้เรา แล้วเราก็ต้องไปจัดการกรณีนั้นโดยการสร้าง object เปล่าๆ ขึ้นมา ซึ่งจะแยกกันกับกรณีที่เรา get แล้วมี object คืนมาอยู่แล้ว

ขั้นที่ 1 ระดับโค้ด

คือ การดูก่อนว่าในแต่ละ function ที่ต้อง ensureExist มันมีโค้ดที่ซ้ำกัน เหมือนไปลอกการบ้าน copy-n-paste มาหรือเปล่า ซึ่งเป็นเรื่องปกติ และยอมรับได้ถ้ามีแค่ 1-2 function แต่ถ้าเริ่มมีมากกว่านั้นก็ควรจะให้มาเรียกที่โค้ดเดียวกัน

โค้ดเดิมที่มี behavior ซ้ำๆ กันอยู่หลายๆ จุด                               
   Core
     + addStory()        { (1) ensureExistDup1..... , (2) push to dataStore.. }
     + listStory()       { (1) ensureExistDup2...,    (2) read from dataStore.. }
     + addParticipant()  { (1) ensureExistDup3.....,  (2) push to dataStore.. }
     + listParticipant() { (1) ensureExistDup4...,    (2) read from dataStore.. }

ก็จับมาเป็นโค้ดร่วมกัน จะได้ไม่ต้องเขียนหลายบรรทัดอยู่หลายที่
   Core
     - ensureExist()
     + addStory()        { (1) ensureExist(), (2) push to dataStore... }
     + listStory()       { (1) ensureExist(), (2) read from dataStore... }
     + addParticipant()  { (1) ensureExist(), (2) push to dataStore... }
     + listParticipant() { (1) ensureExist(), (2) read from dataStore... }

ขั้นที่ 2 ระดับดีไซน์ของโค้ด

เป็นขั้นที่คนส่วนมากลืมทำ แต่สำคัญมากกับการทำให้ดีไซน์เราดีอยู่เสมอ ลองสังเกตดูว่า ถึงแม้ว่าเราจะจับโค้ดที่เหมือนกันมาอยู่ที่เดียวกันแล้ว ตัวเทสเราก็ยังมีอะไรที่ต้องทำซ้ำๆ กันอยู่หลายที่อีกหรือไม่ ถ้าเรามีเทสที่ครอบคลุมกรณีทุกกรณีที่เราสนใจ(ทำได้ง่ายสุดด้วย TDD) เทสทุกตัวจะเป็นตัวบอกเราว่า ระบบเรามี behavior อะไรอยู่บ้าง และการที่เราเห็น behavior ซ้ำๆ กัน เทสกำลังกระซิบบอกเราว่า ดีไซน์เรามีโค้ดที่ยังจัดกลุ่มไม่ถูก ยังวางโค้ดได้ไม่ค่อยถูกที่ ศัพท์เทคนิคที่เค้าเรียกกันก็คือ ยังมี coupling-cohesion ไม่เหมาะสม

ซึ่งในกรณีนี้ เราสังเกตได้ว่ายังมีเทสที่ทดสอบอะไรที่ซ้ำๆ กันอยู่ ในตอนนี้ทุก test ที่ต้องทำงานกับ dataStore จะต้องมีเคสของการที่ dataStore จะส่ง null คืนมา เราก็จะมองเห็นว่า จริงๆ แล้วมันควรเป็นหน้าที่ของ dataStore มากกว่าที่จะต้องทำให้ทุกคนที่จะมา read/write กับเรามั่นใจได้ว่าสิ่งที่อ่านออกมามันเอาไปใช้ได้เลยไม่ต้องมาเช็ค null อีก ซึ่งถ้าจัดโค้ดใหม่ก็จะเป็นแบบนี้

เมื่อปรับใหม่ก็จะพบว่าใน core ของเราก็จะไม่มี ensureExist อีกแล้ว แต่จะไปอยู่ใน dataStore แทน
   Core
     + addStory()        { push to dataStore... }
     + listStory()       { read from dataStore... }
     + addParticipant()  { push to dataStore... }
     + listParticipant() { read from dataStore... }

   DataStore
     - ensureExist() 
     + get() { ensureExist(); readFromMemory() }
     + put() { writeToMemory() }

เมื่อเราปรับเทสใหม่ตามแล้ว ก็จะเห็นได้ว่าระบบเราซับซ้อนน้อยลงจริงๆ เพราะเทสของ core ไม่ต้องมีเทสกรณีที่ dataStore return 
เป็น null อีกเลย หน้าที่รับผิดชอบนี้เป็นของ dataStore เต็มๆ core ก็จะมีกรณีเทสลดลง 4 เคส ซึ่งจะไปเพิ่มที่ dataStore เพียง
1 เคส และต่อๆจากนี้ไป ถ้า core หรือโมดูลอื่นๆ จะเรียกใช้ dataStore อีก ก็ไม่ต้องจัดการกับกรณีที่จะมี return null อีกเลย

   CoreTest
     + testAddStory()     [ไม่ต้องเทสกรณีที่ dataStore return เป็น null แล้ว]
     + testListStory()    [ไม่ต้องเทสกรณีที่ dataStore return เป็น null แล้ว]
     + addParticipant()   [ไม่ต้องเทสกรณีที่ dataStore return เป็น null แล้ว]
     + listParticipant()  [ไม่ต้องเทสกรณีที่ dataStore return เป็น null แล้ว]

   DataStore
     + testGetReturnsContentFromMemory() 
     + testGetReturnsBlankStructureIfNotExist() #ensureExist ที่เคยอยู่ 4 ที่มาอยู่ที่นี่ที่เดียว
     + testPutWritesToMemory()

Related Commits

Clone this wiki locally