![]()
![]()
มีโอกาสมาเรียน course Ultimate Go Workshop จากงาน
GopherCon Singapore
โดยมีหัวข้อต่าง ๆ ดังนี้
- Ultimate Go
- Language mechanic
- Software design
- Concurrency
- Profiling
ซึ่งเป็นเรื่องพื้นฐานแบบลึกมาก ๆ
เนื่องจากลงไปถึง philosophy ของตัวภาษา Go ว่าเป็นอย่างไร ?
ตั้งแต่เรื่อง integrity, readability, simplicity ไปจนถึง
Productivity vs Performance
Correctness s Performance
ลงไปจนถึงว่า CPU แต่ละตัวทำงานอย่างไร
ทำอย่างไรให้สามารถดึงข้อมูลมาทำงานใน CPU ได้รวดเร็วที่สุด
Hardware นั้นชอบ data structure แบบ Array มากที่สุด
เนื่องจากมัน predictable ได้ง่ายนั่นเอง
ส่วน LinkedList มันตรงข้ามกันเลย
การจัดการหน่วยความจำ !!
การใช้งาน go tool สำหรับ tracing, profiling เพื่อ optimize การทำงานของ code !!
แต่เรื่องที่น่าสนใจและสนุกมาก ๆ คือ Interface and Composition Design
ซึ่งทำให้เข้าใจการออกแบบระบบได้อย่างชัดเจน
รวมทั้งเข้าใจเหตุผลของการออกแบบแต่ละอย่างอีกด้วย
ว่าต้องการแก้ไขปัญหาอะไรบ้าง ?
ที่สำคัญทำให้ code แยกออกจากกันอย่างชัดเจน (Decouple)
ผลที่ตามมาคือ ง่ายต่อการแก้ไข
ให้จำไว้ว่า
Coding for Today
Design for Tomorrow
มาเริ่มกันเลย
ขั้นตอนที่ 1 Structure composition
ในการพัฒนาระบบนั้นเริ่มต้น
จากการแบ่งงานใหญ่ออกเป็นงานย่อย ๆ
โดย code ตัวอย่างนั้น
ต้องการดึงข้อมูลจากอุปกรณ์ต่าง ๆ ออกมา
ซึ่งข้อมูลมีมากกว่า 1 บรรทัด
จากนั้นทำการจัดเก็บข้อมูลเหล่านั้น
มาดูกันว่า ทำการแยกงานย่อย ๆ ได้อะไรบ้าง
1. สร้างโครงสร้างข้อมูลชื่อว่า Data
โดยสร้างด้วย struct ดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="1.go"]
2. สร้างส่วนการดึงข้อมูลจากอุปกรณ์
โดยข้อมูลของอุปกรณ์ประกอบไปด้วย ชื่อเครื่องกับ Timeout
สร้างด้วย struct และมี method Pull() สำหรับดึงข้อมูล ดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="2.go"]
3. สร้างส่วนการจัดเก็บข้อมูลจากอุปกรณ์
มีโครงสร้างเหมือนกัน แต่สิ่งที่ต่างคือมี method Store() สำหรับจัดเก็บข้อมูลดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="3.go"]
4. สร้างส่วน logic ของการ pull ข้อมูลจากอุปกรณ์จริง ๆ
ด้วยการสร้าง method pull() ขึ้นมา
จากนั้น extract ข้อมูลแต่ละบรรทัดออกมาดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="4.go"]
5. สร้างส่วน logic ของการ store ข้อมูล
ด้วยการสร้าง method store() ขึ้นมาดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="5.go"]
6. สิ่งที่ยังขาดไปคือ ส่วนการทำงานหลัก สำหรับดึงและบันทึกข้อมูล
[gist id="f32b0009b12116d03ef92848f1219f5c" file="6.go"]
จะเห็นได้ว่าเราทำงานแบ่งปัญหาใหญ่ ๆ ออกเป็นปัญหาเล็ก ๆ
เพื่อทำการแก้ไขไปเรื่อย ๆ
จากนั้นทำการรวมหรือ composition เข้าด้วยกัน
จนสามารถแก้ไขปัญหาที่ต้องการได้
นี่คือ ความสามารถที่สำคัญของนักพัฒนา software
แต่ว่านี่เป็นเพียงการเริ่มต้นเท่านั้น
สำหรับ Coding for Today
มันยังมีต่ออีกยาว !!
จากตัวอย่าง code นั้นพบว่า ทำงานได้อย่างดี
แต่ปัญหาที่น่าสนใจ หรือ Code Smell คือ
ในตอนนี้ Data กับ Behavior ของ Device และ Storage รวมกันอยู่
นั่นคือผูกมัดกันอย่างมาก
ลองคิดดูสิว่า ถ้ามีจำนวน Device และ Storage จำนวนมาก
ซึ่งมีพฤติกรรมการทำงานที่แตกต่างกัน
ดังนั้นจึงต้องหาวิธีแก้ไขปัญหา ?
ขั้นตอนที่ 2 Decoupling with Interface
โดย interface นั้นใช้สำหรับการกำหนดพฤติกรรมการทำงาน
ซึ่งถูกใช้งานจากผู้เรียกใช้งานนั่นเอง
เป็นแนวทางที่มาก ๆ ใน Go
จากตัวอย่างจะแยกพฤติกรรมออกมาจาก Device และ Storage ดังนี้
สำหรับ Device จะแยกออกมาเป็น Puller
สำหรับ Storage จะแยกออกมาเป็น Storer
เขียน code ได้ดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="7.go"]
ผลที่ตามมาคือ
ต้องทำการแก้ไข code ในส่วนของ method ที่ทำงาน pull() และ store() ข้อมูล
จากเดิมที่ส่ง struct เข้าไป ก็เปลี่ยนเป็น interface ได้ดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="8.go"]
มาถึงตรงนี้เราสามารถแยกส่วนของข้อมูลกับพฤติกรรมการทำงานออกจากกัน
ทำให้ตอบโจทย์เรื่องการเปลี่ยนแปลงได้ง่ายมากขึ้น
และลดผลกระทบจากการเปลี่ยนแปลงอีกด้วย
ในงาน GopherCon มักจะพูดว่า interface คือ first-class citizen ด้วยนะ
โดยการทำงานร่วมกันนั้น
เราไม่ต้องรู้หรอกว่า คุณเป็นใคร สนใจเพียงว่าทำอะไรร่วมกันได้บ้าง
นั่นคือที่มาของ interface นั่นเอง
ขั้นตอนที่ 3 Interface Composition
จะเห็นได้ว่าการใช้งาน interface
Puller และ
Storer นั้นจะทำงานร่วมกันเสมอ
ดังจะเห็นได้จาก method
Copy()
แสดงดัง code
[gist id="f32b0009b12116d03ef92848f1219f5c" file="9.go"]
สังเกตุได้ว่าสิ่งที่ส่งมายัง method Copy() คือ
System
ซึ่งเป็น struct ที่รวมเอา Device และ Storage ไว้ด้วยกัน
แต่สิ่งที่ควรส่งมาคือ พฤติกรรมของการทำงานมากกว่า !!
คำถามคือ จะส่งอะไรมาระหว่าง Puller และ Storer ?
คำตอบคือทั้งสองตัว !!
สิ่งที่ต้องทำคือ สร้าง interface ใหม่ชื่อว่า
PullStorer
เพื่อทำงานรวมทั้งสอง interface เข้าด้วยกัน
แสดงดัง code
[gist id="f32b0009b12116d03ef92848f1219f5c" file="10.go"]
ยังไม่พอนะ ยังไม่จบ
ขั้นตอนที่ 4 Decoupling with Interface composition
จะเห็นได้ว่า struct
System นั้น
ยังผูกมัดอยู่กับ struct
Device และ
Storage คือข้อมูลนั่นเอง
ดังนั้นเราควรที่จะแยกออกมา
ด้วยการใช้งาน interface composition แทน
นั่นคือ
[gist id="f32b0009b12116d03ef92848f1219f5c" file="11.go"]
มาถึงตรงนี้จะเห็นได้ว่า
การทำงานแต่ละส่วนแยกออกจากกันอย่างชัดเจนด้วย interface
ซึ่งทำให้ง่ายต่อการเปลี่ยนแปลง
และลดผลกระทบจากการเปลี่ยนแปลงอีกด้วย
ยังไม่จบนะ จะสังเกตุได้ว่า มี composition interface ที่หน้าตาเหมือนกันเลย
นั่นคือเกิด duplication code ขึ้นมาแล้ว (Remove interface pollution)
ดังนั้นให้ทำการลบ code ส่วนนั้นทิ้งไปซะ
นั่นคือ interface
PullStorer นั่นเอง
จะได้ code ทั้งหมดดังนี้
[gist id="f32b0009b12116d03ef92848f1219f5c" file="12.go"]
มาถึงจุดนี้กันได้อย่างไร ?
รู้สึกอย่างไรบ้างสำหรับการออกแบบ Interface และ Composition
สุดท้ายแล้วยังมีเรื่องที่น่าสนใจและมึนงงอีกมากมาย
ทั้งเรื่องการทำงานของ Garbase Collection
ทั้งเรื่องการทำงานของ Slice
ทั้งเรื่องการทำงานของ Goroutine
ทั้งเรื่องการทำงานของ Concurrency
ทั้งเรื่องของ Profiling และ Optimization
แน่นอนว่าต้องใช้เวลาในการย่อยอีกสักพัก
แล้วจะนำมาเขียนสรุปต่อไปครับ
ขอให้สนุกกับการเขียน code
ที่สำคัญต้องเข้าใจด้วยว่าปัญหาที่คุณกำลังแก้ไขคืออะไร
นั่นคือคุณเข้าใจ domain และ data นั่นเอง
Reference Websites
https://github.com/ardanlabs/gotraining
https://www.goinggo.net/2015/09/composition-with-go.html