Key Highlights:
- Design patterns คือแนวคิดในการแก้ปัญหาของการออกแบบซอฟต์แวร์ ซึ่งปัญหาในการเขียนโปรแกรมนั้นส่วนใหญ่จะเป็นเรื่องเดิม ๆ ซ้ำ ๆ ที่เหล่าโปรแกรมเมอร์ทั่วโลกประสบพบเจอกัน จึงได้เกิดกลุ่มบุคคลที่รวมตัวกันเพื่อแก้ไขปัญหานี้ หรือเรียกพวกเขาว่า Gang of Four (GoF) และพวกเขาได้เขียนหนังสือรวบรวมวิธีแก้ไขปัญหาต่าง ๆ ไว้ในหนังสือที่มีชื่อว่า Design Patterns: Elements of Reusable Object-Oriented Software
จากหนังสือที่เขียนมานั้นตั้งแต่ปี 1994 ซึ่งปฏิเสธไม่ได้เลยว่า Design patterns นี้ได้ช่วยเหลือให้เหล่าโปรแกรมเมอร์มากมาย แก้ปัญหามาอย่างยาวนาน และในวันนี้เราจะมาดูกันครับว่า Design patterns แบบไหนที่ยังสามารถแก้ไขปัญหาได้ดีในปัจจุบันโดยเฉพาะอย่างยิ่งในมุมมองของภาษา Go และจะขอหยิบยกมาพูดถึงในบทความนี้ครับ
Builder
Builder หรือชื่ออื่น ๆ ที่เรียกกัน เช่น functional options หรือ fluent interfaces ยังฮอตฮิตตดลมบนมาจนถึงปัจจุบัน ปัญหายุ่งยากในการสร้าง object ที่มีความซับซ้อนจะหมดไป ด้วยเจ้าสิ่งนี้นี่เอง โดยในบทความนี้จะขอยกตัวอย่างการใช้งาน builder เพื่อสร้าง *http.Request object
และนี่คือเจ้าตัว builder ของเราซึ่งเรากำหนดให้ input เป็น url เพราะส่วนประกอบอื่นๆ เราสามารถทำการตั้งค่าเริ่มต้นได้ สำหรับส่วนประกอบตัวอื่นๆ ก็สามารถปรับแต่งได้ผ่านตัว HTTPBuilder ที่เป็น interface นั้นเอง
ในตัวอย่างการใช้งานเราสามารถเรียกใช้งานการสร้าง http.Request ง่ายๆ ได้ด้วยการเรียก NewBuilder และ input url เข้าไปได้เลย หากอยากเพิ่ม header ก็เพียงแค่เรียก .AddHeader() ตาม และต่อด้วย .Build() เพียงเท่านี้เราก็ได้จะ object ของตัว http.Request แบบเขียนง่าย อ่านง่าย สบายตา
Abstract factory/factory method
เปรียบเสมือนโรงงานผลิต object ซึ่งสามารถทำให้สร้าง object หลายรูปแบบได้แค่เพียงตอนใช้งานต้องใช้ในรูปแบบเดียวกัน
ยกตัวอย่างง่ายๆ เราจะทำการทดสอบระบบซึ่งต้องทำการเปิดการเชื่อมต่อของ socket ไปที่ service อื่น ถ้าเราใช้ตัว net.Dialer โดยตรง มันจะทำให้เราทดสอบระบบได้อย่างยากลำบาก แต่ถ้าเราสามารถสร้าง object ของ net.Conn การทดสอบระบบก็จะเป็นเรื่องที่ง่ายขึ้นดังตัวอย่างต่อไปนี้
เราต้องทำการจำลอง net.Addr กับ net.Conn และทำการคืนค่า struct และ factory property ใน socketClient จะมีหน้าตาแบบนี้ func(ctx context.Context, network, address string) (net.Conn, error) ซึ่งเหมือนกับ net.Dialer เป๊ะเลย เพียงเท่านี้ในการทดสอบระบบก็จะทำได้ง่ายขึ้น
Adapter
อีกหนึ่งสิ่งที่นิยมใช้กันอย่างแพร่หลายในปัจจุบันนั้นคือ เจ้า adapter ของเรานั้นเอง ตัวอย่างที่เห็นได้ชัดเจนคือ เจ้าตัว database driver ที่สร้างจาก driver.Driver interface กับ registers กับ database/sql ซึ่งไม่ได้สนใจเลยว่าจะใช้งาน database เจ้าไหนเลย
สามารถศึกษาเพิ่มเติมได้จาก go-clound
Decorator
สามารถนำ decorator มาใช้งานเพื่อทำการ เพิ่ม/ลด ความสามารถให้กับ object ขณะ runtime ซึ่งจะทำการสร้าง wrapper class ที่สามารถเพิ่มความสามารถเข้าไปได้เรื่อยๆ และยังสามารถนำ wrapper class มา wrap ตัว wrapper class ที่เราต้องการความสามารถนั้นๆ
ในตัวอย่างที่ยกมาเราจะทำการเพิ่ม buffer ไปที่ io.Reader ซึ่งวิธีนี้ไม่ใช่วิธีการจูนประสิทธิภาพแต่เป็นตัวอย่างที่จะทำให้เห็นภาพที่ชัดเจนขึ้น
Facade
คือทำให้ของที่ใช้งานยากๆซับซ้อนๆ สามารถใช้งานได้แบบง่ายๆ โดยการ สร้าง class ที่ทำงานกับ subsystem ที่วุ่นวายๆ แล้วจัดการเรื่องที่ client ต้องเรียกใช้ทั้งหมด สร้างช่องทางให้ client เรียกใช้งานแบบง่ายๆ
ศึกษาตัวอย่างดีๆ ได้ที่ go-cloud
Proxy
เมื่อการต่อ database เป็นเรื่องวุ่นวายมากๆ บางครั้งก็ไม่จำเป็นที่จะต้องต่อ หรือบางทีแค่จำลองข้อมูลมาก็พอ ซึ่งเราสามารถใช้ lazy load technique จะได้ไม่เปลืองทรัพยากร แต่ไหนจะต้องกำหนดการใช้งานก่อนหลังอีก เพื่อแก้ปัญหานี้อย่างยั่งยืนจึงได้เกิดเป็นเจ้านี่ขึ้นมา proxy pattern ยังไงละ
Chain of responsibility and Command
หนึ่งแบบดีอยู่แล้ว รวมสองแบบดียิ่งกว่า!!!
และนี่คือการรวมร่างกันระหว่าง Chain of Responsibility และ Command ไปดูตัวอย่างกันเลย
และนี่คือ command ของเราที่เอาไว้จัดการกับ HTTP Request ซึ่งมันไม่สำคัญว่าเราจะเขียนโค้ดออกมาแบบไหนเพียงแค่ว่ามี signature เหมือนกันก็พอ
ทั้งสองตัวอย่าง สามารถใช้งานแทนกันได้ สิ่งที่สำคัญคือ input และ output จะต้องเหมือนกัน ซึ่งทั้งสองตัวอย่างทำการรับค่า http.ResponseWriter และ http.Request พร้อมทั้งไม่มีการรีเทิร์นค่ากลับไปเหมือนกันนั้นเอง
มี Command แล้วเจ้า Chain of Responsibility อยู่ไหนละ อยู่ที่ Middlewares หรือเปล่านะ ไปดูกันต่อเลยดีกว่า
เกือบจะเหมือนกับตัว http.Handler แต่มีบางอย่างที่แตกต่างนั้นคือการรับพารามิเตอร์ next นั้นเอง ลองไปใช้งานกันดีกว่า ว่าจะเป็นอย่างไร
ในตอนนี้เราได้ทำการใช้งาน MiddlewareToHandler method เพื่อทำการเพิ่ม log จาก middleware ไปยัง OkHandler เราสามารถทำการเพิ่มฟีเจอร์ใน middleware อีกเท่าไหร่ก็ได้ โดยที่เราไม่ต้องไปเปลี่ยนตัว command เลย ในตัวอย่างมี middleware ตัวเดียว หากต้องการศึกษาให้ลึกและระเอียดมากกว่านี้ตามไปที่ gorilla/mux ได้เลย
Iterator
ก่อนอื่นต้องยอมรับก่อนว่าในภาษา Go เองนั้นไม่มีไลบรารี่เหมือนภาษาอื่น ๆ สำหรับเจ้าตัว iterator นี้ แต่สิ่งที่ทำคือ สร้างตัว iterator ขึ้นมาใช้งานโดยเฉพาะ ซึ่งในเวอร์ชั่น 1.18 จะมีเจ้าตัว generics เข้ามาก็จะทำให้ชีวิตง่ายขึ้น ไม่ต้องทนใช้ for…range อีกต่อไป สำหรับเจ้าตัว iterator ที่ฮิตฮอตที่สุดใน go นั้นก็คือ sql.Rows นั้นเอง ซึ่งทุกคนต้องผ่านหู ผ่านตากับการจัดการ SQL ใน Go
Observer
เป็นอีกหนึ่งสิ่งที่เราจะเห็นได้บ่อยที่สุดใน Go สิ่งนั้นคือ Channels ยังไงละ ซึ่งมากับคอนเซ็ปต์ 1 ผู้ส่ง กับ 1 ผู้รับ แต่มันก็จะเริ่มซับซ้อนมากขึ้นเมื่อต้องการส่งข้อความไปยังผู้รับหลาย ๆ คน
Strategy
อย่างที่ทราบกันดีว่า Strategy pattern นั้นเป็นการสร้างการทำงานหลาย ๆ แบบ แยกออกเป็น class และมี interface ร่วมกัน ยกตัวอย่างเช่นการจัดเรียงข้อมูล ไม่ว่าจะเป็น merge sort, bubble sort, quick sort เราสามารถนำมาประยุกต์ใช้งานได้โดยการสร้างเพียง method เดียวและทำการกำหนด signature ให้เหมือนกันนั้นเอง
จากตัวอย่างจะให้ได้ว่าเราไม่จำเป็นจะต้องแยก class ของแต่ละอัน
Template Method
ในปัจจุบันสิ่งที่ใช้กันอยู่พูดได้ว่า Template Function มากกว่า ซึ่งจะมี abstract method ไว้พร้อมใช้งานใน sub class ซึ่งนั้นก็คือ function นั้นเอง สำหรับตัวอย่าง Template Method ในบทความนี้จะขอใช้การ Sorting objects
สำหรับการใช้งานเราไม่รู้ว่า ในการจัดเรียงนั้นได้ทำการจัดเรียงแบบไหน แต่สิ่งที่ทำคือสร้างฟังก์ชั่นสำหรับเปรียบเทียบขึ้นมาใหม่โดยไม่จำเป็นที่จะต้องทำการแยก class
ยังมี Design patterns แบบอื่น ๆ ที่ไม่ได้ถูกยกมาพูดถึงในบทความนี้อีกมากหมาย ซึ่งไม่ได้หมายความว่า แบบอื่น ๆ จะใช้ไม่ได้แล้วในภาษา Go
ทั้งนี้เราควรแก้ปัญหาต่าง ๆ ที่เกิดขึ้น ด้วยการเลือกใช้งาน pattern ให้เหมาะสม ซึ่ง design patterns จะเป็นตัวช่วยทำให้เราปรับปรุงและพัฒนาซอฟต์แวร์ให้ดียิ่งขึ้น
หากผู้เขียนอธิบายส่วนไหนไม่เข้าใจหรืออธิบายผิด กราบขออภัยไว้ ณ ที่นี้ ด้วยครับ