🔌Adapter Pattern

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

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

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

หมายเหตุ เนื้อหาของบทความนี้จะเน้นให้เข้าใจหลักการทำงานของ Design Patterns แต่ละตัว โดยภาพจากเกม Ragnarok เป็นการอธิบาย ซึ่งหลายๆอย่างนั้นมโนขึ้นมาเพื่อความสนุก และทำให้เข้าใจเนื้อหาได้ง่าย Gravity อย่ามาจับผมนะผมโดนแมวน้ำครอบงำ + รู้เท่าไม่ถึงการ + ผมเป็นคนดี + ผมมีลูกมีเมียมีสามีที่ต้องดูแล 😭

🧐 โจทย์

สมมุติว่าเรารับงานเว็บทายผลบอลทุกคู่ทั่วโลกเพื่อความบันเทิงมา (ไร้ซึ่งการพนันใดๆทั้งสิ้น 🤐) โดยเมื่อบอลแข่งเสร็จ ตัวเว็บของเราก็ต้องไปดึงผลการแข่งขันจาก 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 ยังไงล่ะ

Single-Responsibility Principle (SRP) การออกแบบที่ละเมิดหลักในการออกแบบนี้จะทำให้ เวลาที่ Requirement เปลี่ยนมาทีนึง มันก็จมีโอกาสสูงมากที่การเปลี่ยนนั้นมันจะไปกระทบเจ้าสิ่งนั้น ทำให้เราต้องแก้ไขมัน ซึ่งผองเพื่อนอื่นๆที่มันดูแลอยู่นั้นไม่ได้เกี่ยวข้องเลยก็มีผลกระทบด้วยนั่นเอง ส่วนใครที่ลืมหรืออยากทบทวนเรื่อง SRP สามารถเข้าไปอ่านได้จากลิงค์นี้เบย Single-Responsibility Principle

Open & Close Principle (OCP) การออกแบบที่ละเมิดหลักในการออกแบบนี้จะทำให้ทุกครั้งที่มีของใหม่ๆถูกเพิ่มเข้าไปปุ๊ป เราก็ต้องไปแก้โค้ดเดิมเสมอ สำหรับใครที่ลืมหลักในการออกแบบเรื่องนี้ไปแล้วให้กดอ่านได้จากตรงนี้ Open & Close Principle

Dependency-Inversion Principle (DIP) การละเมิดกฎข้อนี้จะทำให้ module หลักต้องถูกแก้ไขบ่อยๆ เมื่อตัวที่ทำงานตัวเล็กตัวน้อยมีการเปลี่ยนแปลง แม้จะเปลี่ยนเพียงแค่เล็กน้อยก็ตาม Dependency-Inversion Principle

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

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

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

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

🐲 ควบคุมไม่ได้ เมื่อมีของที่เราควบคุมไม่ได้อยู่ในระบบของเราความบรรลัยก็จะบังเกิด เพราะ วันดีคืนดีอยู่ๆของพวกนั้นเกิดใช้งานไม่ได้ หรือพฤติกรรมมันเปลี่ยนขึ้นมา ต่อให้โค้ดฝั่งเราเขียนดีแค่ไหนก็ตามก็ย่อมมีผลกระทบตามมา หลายคนอาจจะคิดว่าเป็นของไกลตัวโปรเจคเราไม่มีของพวกนั้นหรอก งั้นแมวน้ำถามกลับว่า โปรเจคเราได้ใช้ Library ของคนอื่นป่ะ? ซึ่งของพวกนั้นดูเหมือนจะไม่มีพิษมีภัยอะไร แต่จริงๆมันก็คือของที่เราควบคุมไม่ได้แบบหนึ่งเหมือนกัน ลองศึกษา Case Study ด้านล่างต่อละกันถ้าสนใจ

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

เกร็ดความรู้ Class Adapter Pattern นั้นถูกออกแบบมาสำหรับภาษาที่รองรับการทำ multi-inheritance ก็จริง แต่ก็ไม่ได้หมายความว่าภาษาที่ไม่รองรับ multi-inheritance จะทำไม่ได้นะ เพราะเราสามารถทำ implement interface ได้หลายตัว

😎 Perfect Adapter

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

🎯 บทสรุป

👍 ข้อดี

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

👎 ข้อเสีย

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

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

เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺

Last updated