🔌Adapter Pattern

แนวคิดในการเปลี่ยนสิ่งที่ทำงานร่วมกันได้ยาก มาทำงานร่วมกันได้ง่ายๆ 😘

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

แนะนำให้อ่าน บทความนี้เป็นส่วนหนึ่งของมหากาพย์ Design Patterns ที่จะมาเป็น guideline ในการแก้ปัญหาในการออกแบบซอฟต์แวร์โปรเจค หากใครสนใจอยากเข้าใจตั้งแต่ต้นว่ามันคืออะไร และเจ้า patterns ทั้ง 23 ตัวมีอะไรบ้าง ก็สามารถจิ้มตรงนี้เพื่อไปอ่านบทความหลักได้เบยครัช 👦 Design Patterns

🧐 โจทย์

สมมุติว่าเรารับงานเว็บทายผลบอลทุกคู่ทั่วโลกเพื่อความบันเทิงมา (ไร้ซึ่งการพนันใดๆทั้งสิ้น 🤐) โดยเมื่อบอลแข่งเสร็จ ตัวเว็บของเราก็ต้องไปดึงผลการแข่งขันจาก Web API ชั้นนำ 2 แห่งนั่นคือ LongOnGoal, LifeScore ตามรูปด้านล่าง

🤔 ทำไมต้องดึงมาจากหลายเว็บเหรอ? ก็บางเว็บมีผลการแข่งเฉพาะคู่เล็กๆ บางเว็บมีผลเฉพาะคู่ใหญ่ๆ และบางทีเราก็อาจจะต้องตรวจสอบว่าผลมันตรงกันหรือเปล่า เพื่อที่จะได้จ่าย... เอ้ย ให้แต้มคนที่ทายถูกได้นั่นเอง 😅 (ส่วนแต้มต่อก็ ... ช่างมันเถอะ 🤐)

เท่าที่อ่านๆมาก็เหมือนจะง่ายๆเนอะ แค่ไปดึง API จากเว็บ 2 ตัวมาก็จบแล้วชิมิ ไหนลองไปดู API สำหรับ ดึงผลคะแนน ของเขาดูดิ๊

🤯 บรึ๊ม!! แม้ว่าจะเป็นเว็บผลคะแนนบอลเหมือนกันแต่ API เรียกใช้ไม่เหมือนกัน, parameters ต่างกัน, models ต่างกัน แล้วแบบนี้เราจะเขียนโค้ดยังไงดีเนี่ย? 😵

🧒 แก้โจทย์ครั้งที่ 1

อย่าไปคิดเยอะดิ ก็เขียนๆมันไปเลยจะยากอะไร อย่างแรกเลยก็เขียนไปดึงข้อมูลกับ LongOnGoalAPI มาเก็บไว้ แล้วก็ดึงจาก LifeScoreAPI มา สุดท้ายก็เอา 2 ตัวนี้มาเทียบกันก็จบละนิ ป๊ะโถ่วววว

var result1 = longOnGoalAPI.GetMatchResult(1234);
var result2 = lifeScoreAPI.GetHistory(DateTime.Now, "ManU", "Liver");

🤔 ก็เหมือนจะง่ายนะ แต่เราลืมอะไรไปป่ะว่า model ที่ได้กลับมามันไม่เหมือนกัน แล้วจะเปรียบเทียบยังไงอ่ะ?

😎 งั้นก็ขอเขียนโค้ดแปลง model ทั้ง 2 ตัวไว้ตรงนี้เลยละกัน จะได้เทียบกันได้ง่ายๆ ตามด้านล่าง

var homeScoreFromResult1 = // หาคะแนนของทีมเหย้า จาก LongOnGoalAPI
var awayScoreFromResult1 = // หาคะแนนของทีมเยือน จาก LongOnGoalAPI

var homeScoreFromResult2 = // หาคะแนนของทีมเหย้า จาก LifeScoreAPI
var awayScoreFromResult2 = // หาคะแนนของทีมเยือน จาก LifeScoreAPI

จากโค้ดด้านบนก็ไม่ได้ทำอะไรผิดนะ แก้โจทย์ที่ของเราได้ตามปรกติเลย แต่มันจะเกิดอะไรขึ้นถ้า เราเพิ่ม Web API ตัวที่ 3, 4, 5 ... เข้ามาล่ะ? เราก็ต้องไปไล่แก้โค้ดใหม่ทุกครั้งอะดิ แถมยิ่งแก้ยิ่งพันกันเป็นสปาเก็ตตี้ขึ้นไปเรื่อย 😨

เพราะโค้ดของเรามันมีหลายอย่างที่ไม่ตรงหลักในการออกแบบที่ดี เช่น SRP, OCP, DIP ยังไงล่ะ

🥴 อาวล่ะแม้ว่ามันจะมีปัญหาในอนาคตก็ตาม แต่อย่างน้อยเราก็ได้เห็นตัวอย่างในการบ้าพลัง เห็นโจทย์แล้วเมากาวกระโดดลงโค้ดเลย ผลก็คือ Design แบบกาวๆเหล่านั้นจะมีปัญหาในอนาคตนั่นเอง งั้นเดี๋ยวลองวางสมอง แล้วเติมน้ำหอมกันหน่อย เพื่อวิเคราะห์ปัญหาแล้วหาทางแก้ไขกันต่อ

🧒 แก้โจทย์ครั้งที่ 2

🔥 วิเคราะห์ปัญหา

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

Case Study มี Library หลายตัวที่แมวน้ำใช้มาหลายปีก็ไม่มีปัญหาอะไร เช่น SimpleInjector, SendGrid แต่พอมาถึงวันนึงที่เราต้องอัพเกรดเวอร์ชั่นปุ๊ป สิ่งที่เกิดขึ้นกับ SimpleInjector นั้นดูเหมือนไม่มีอะไรโค้ด compile ได้ไม่มีปัญหาแต่พฤติกรรมของมันเปลี่ยนไป ทำให้ life-cycle ของแอพผิดปรกติ นั่งหากันยกใหญ่ ส่วนเจ้า SendGrid ก็แสบไม่แพ้กัน พี่แกเล่นลบ core interface หลักทิ้ง แล้วไปขึ้น core interface ใหม่โดยไม่เหลืออันเดิมให้ใช้เลย (ปรกติเขาจะไม่ลบทิ้ง แต่จะใส่ obsolete attribute ไว้) ทำให้เราต้องมานั่งเสียเวลา re-implement ใหม่ หรือ Library บางตัวออกอัพเดทใหม่ก็มี bugs แถมมาด้วย 😒 แต่ทั้งหมดจะไปโทษเขาก็ไม่ได้หรอก เราในฐานะ Developer ต้องตรวจของที่จะเอามาใช้งานใน production ก่อนเสมอแหละ (อย่าให้ผู้ใช้มาเป็นเทสเตอร์ให้เราเลย 😂)

🔥 แก้ไขปัญหา

เมื่อเรามีของที่เราควบคุมไม่ได้ เราต้องเปลี่ยนเป็นสิ่งที่เราควบคุมได้ก่อน เพราะไม่อย่างนั้นสนุกแน่ ซึ่งการที่เราจะไปควบคุมของอื่นๆมีหลายวิธีเลย ซึ่งวิธีง่ายสุดคือการทำ Wrapper Class โดยเราจะสร้างคลาสขึ้นมาครอบของที่ควบคุมไม่ได้เอาไว้ แล้วเราก็ทำงานกับคลาสตัวนั้นแทนที่จะทำงานกับของที่ควบคุมไม่ได้นั่นเอง ตามรูปด้านล่าง

Design Pattern ที่มีลักษณะเป็น Wrapper Class มีหลายตัวเลย เช่น Proxy, Decorator ลองไปศึกษาต่อได้

โดยปรกติ Wrapper Class จะมีหน้าที่เพียงแค่เรื่องเดียวคือควบคุมสิ่งที่มันดูแลอยู่ ดังนั้นเรื่องการจัดการ Web API ทั้ง 2 ตัวนั้น เราก็จะมี Wrapper Class เอาไว้ดูแลมันโดยเฉพาะเลย ตามรูปด้านล่าง

ถัดมาเรื่องการใช้ Web API ทั้ง 2 ตัว แม้ว่ามันจะต่างกันคนละโลกแค่ไหนก็ตาม แต่ถ้าลองคิดดูดีๆเราก็จะพบว่า ของที่เราอยากได้จากมันจริงๆนั้นคือคะแนนการแข่งขันระหว่าง 2 ทีม ดังนั้นจริงๆเราไม่ต้องสนใจด้วยซ้ำว่า API พวกนั้นจะส่ง model หน้าตาแบบไหนกลับมา ขอแค่เราสามารถแปลง model พวกนั้นให้กลายเป็นของที่เราสนใจได้ก็พอ ตามรูปด้านล่าง

ส่วนเรื่องที่ API ทั้ง 2 ตัวมันมีวิธีเรียกใช้ไม่เหมือนกัน ซึ่งถ้าคิดดูดีๆก็จะพบว่า หน้าที่ในการดูแล API มันไปตกอยู่กับพวก Wrapper ดังนั้นหน้าที่ในการจัดการความซับซ้อนของ API เลยเป็นหน้าที่ของ Wrapper จึงทำให้เราเลือกได้เลยว่าเราอยากให้ Wrapper มีหน้าตาเป็นยังไง นั่นเองตามรูป

พอมันกลายเป็นแบบรูปด้านบนปุ๊ป เราก็จะพบว่า Wrapper ของเราไม่มีความต่างละ ดังนั้นเราก็สามารถจัดการมันในรูปแบบของ interface ได้เลย ตามรูปด้านล่าง

🤠 จากที่ว่ามาทั้งหมดปัญหาเรื่อง เวลาของที่เราควบคุมไม่ได้มีการเปลี่ยนแปลง มันก็จะกระทบแค่ Wrapper ที่ดูแลสิ่งนั้นอยู่เท่านั้น เพราะตัว Wrapper รับผิดชอบการดูแลไปแล้วนั่นเอง ซึ่งก็จะตรงกับกฎของ Single-Responsibility Principle เรียบร้อย

🤠 หรือเราจะไปดึงข้อมูลจาก Web API อื่นๆ โค้ดเดิมก็ไม่มีผลกระทบอะไรเลย เพราะเราก็แค่เพิ่ม Adapter ตัวใหม่เข้าไป ซึ่งตรงกับกฎของ Open & Close Principle เรียบร้อย

🤠 และสุดท้ายต่อให้เราจะเลิกใช้ Web API ตัวไหนไป หรือเรามีการแก้ไข Web API นิดๆหน่อยก็จะไม่มีผลกระทบกับ module หลักที่เรียกใช้ Adapter เหล่านี้แล้ว ซึ่งก็ตรงกับกฎ Dependency-Inversion Principle เช่นกัลล์

ยินดีด้วยในตอนนี้คุณได้ใช้สิ่งที่เรียกว่า Adapter Pattern เรียบร้อยแล้ว ไม่ว่าจะรู้ตัวหรือไม่ก็ตาม เย่ๆ 👏

🤔 Adapter Pattern คือไย ?

มันคือ แนวคิดในการแก้ปัญหาตอนที่เรามีของ 2 อย่างที่ทำงานร่วมกันไม่ได้หรือทำได้ยาก แต่เราก็อยากให้มันทำงานด้วยกันได้โดยไม่ทำให้โค้ดเราซับซ้อนเกินไป ซึ่งในโลกความเป็นจริงเราก็จะเห็น Adapter Pattern ได้จากที่ชาร์จโทรศัพท์ไง เพราะเครื่องบางยี่ห้อจะรับเฉพาะ Lightning บางยี่ห้อรับเฉพาะ Micro USB บางอันเป็น USB-C ดังนั้นหน้าที่ของ Adapter Pattern ก็คือการแปลง ของที่เข้ากันไม่ได้ ให้ลงรอยกันได้นั่นเอง ตามรูปด้านล่าง

ดังนั้นเมื่อไหร่ก็ตามที่เราเจอปัญหาเข้ากันไม่ได้ ให้รู้ได้เลยว่า Adapter Pattern อาจจะเป็นหนึ่งในทางออกของเราก็ได้

💡 หลักในการคิด

เมื่อไหร่ก็ตามที่เราเจอของ 2 อย่างที่มีการทำงานแตกต่างกัน เราก็แค่สร้างมาตรฐานใหม่ที่จะเอามากำกับดูแลของพวกนั้น ให้มันทำงานตรงตามสิ่งที่เราอยากได้ก็พอ ซึ่งในตัวอย่างของเราได้สร้างมาตรฐานใหม่ที่ชื่อว่า IFootballAdapter เอาไว้กำกับดูแลการทำงานของ Web API ทั้งหลายนั่นเอง

หมายเหตุ โดยปรกติตัวมาตรฐานใหม่ที่เราสร้างขึ้นมาเราจะเรียกมันว่า Adapter ส่วนตัวที่ถูก adapter ดูแลอยู่ เราจะเรียกมันว่า Adaptee

🤠 การนำ Adapter ไปใช้งานนั้น สามารถออกแบบได้เยอะมาก ไม่ได้จำกัดว่าจะต้องเหมือกับในตัวอย่าง เช่นการทำ Class Adapter Pattern, Object Adapter Pattern

Object Adapter Pattern (Wikipedia)
Class adapter Pattern (Wikipedia)

😎 Perfect Adapter

เมื่อเราสามารถแปลงข้อมูลจากฝั่ง A ไปหาฝั่ง B ได้ ในทฤษฎีของ Adapter Pattern นั้นก็ยังบอกอีกว่า เราก็ควรทำให้เจ้า Adapter สามารถแปลงข้อมูลกลับจากฝั่ง B ไปหาฝั่ง A ได้ด้วยเช่นกัน

ส่งข้อมูลไปกลับได้ทั้ง 2 ฝั่ง

🎯 บทสรุป

👍 ข้อดี

การนำ Adapter Pattern มาใช้งานนั้นจะช่วย ลดการผูกกันของโค้ดลง เพราะเราไม่ต้องไปวุ่นวายกับ Concrete class ที่อาจเปลี่ยนแปลงภายหลังได้ แถมจะเพิ่มจะลบ Adapter ตัวไหนก็สามารถทำได้เลยอีกด้วย

👎 ข้อเสีย

เพิ่มความซับซ้อนโดยไม่จำเป็น เพราะการนำ Adapter ไปใช้ จะทำให้เราไม่สามารถทำงานกับ Source ได้ตรงๆ

ช่องทางสนับสนุนค่าอาหารแมวน้ำกั๊ฟ 😘

Last updated

Was this helpful?