Adapter
ทำให้2 class ที่ interface เข้ากันไม่ได้ ทำงานร่วมกันได้
ในระบบจริง เรามักเจอสถานการณ์ที่มี “ของสองชิ้นที่ควรทำงานร่วมกันได้ แต่ interface ไม่ตรงกัน” — อาจเป็น library ภายนอกที่ signature method ต่างจากที่ code เราคาดหวัง, เป็นระบบ legacy ที่แก้ code ไม่ได้แล้ว, หรือเป็น service ของทีมอื่นที่ออกแบบมาคนละแนวคิดกับของเรา
ทางเลือกที่แย่ที่สุดคือการแก้ code client ให้รู้จักรายละเอียดของ Adaptee โดยตรง เพราะจะทำให้ business logic ปนกับ logic การแปลงข้อมูล/รูปแบบการเรียก และผูก code เราเข้ากับ interface ภายนอกที่เราควบคุมไม่ได้
Adapter (หรือ Wrapper) แก้ปัญหานี้ด้วยการสร้าง class ตัวกลางที่ implement interface ที่ client คาดหวัง (Target) แล้วภายในค่อยแปลคำเรียกไปยัง interface เดิมของ Adaptee เทียบได้กับอะแดปเตอร์ไฟฟ้าในโลกจริง ที่ทำให้ปลั๊กสามขาเสียบเข้ากับเต้ารับสองขาได้ โดยที่ทั้งอุปกรณ์และเต้ารับไม่ต้องเปลี่ยนแปลงอะไรเลย
หัวใจของแนวคิดคือ การแยกความรับผิดชอบเรื่องการแปลง interface ออกจาก business logic — client รู้จักแค่ Target, ไม่รู้จัก Adaptee เลย ทำให้เปลี่ยน Adaptee หรือเพิ่ม Adapter ตัวใหม่ได้โดยไม่กระทบ client (สอดคล้องกับ Open/Closed Principle) ในบางกรณี Adapter หลายตัวยัง chain ต่อกันได้ เช่น แปลงจาก format A → B → C ทีละขั้น
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”Adapter มีสองแบบหลัก คือ Object Adapter (ใช้ composition ถือ instance ของ Adaptee ไว้ภายใน — พบบ่อยใน C#/Java เพราะไม่ต้องพึ่ง multiple inheritance) และ Class Adapter (ใช้ inheritance สืบทอดจาก Adaptee โดยตรง — ใช้ได้เฉพาะภาษาที่รองรับ multiple inheritance เช่น C++) บทความนี้เน้น Object Adapter ซึ่งเป็นแบบที่ใช้จริงใน C# เกือบทั้งหมด
classDiagram
class Client {
+DoWork()
}
class ITarget {
+Request()
}
class Adapter {
-Adaptee adaptee
+Request()
}
class Adaptee {
+SpecificRequest()
}
Client --> ITarget
ITarget <|.. Adapter
Adapter o-- Adaptee
- ITarget — interface ที่ client คาดหวังและเรียกใช้งานตรง ๆ
- Adaptee — class เดิมที่มี interface เข้ากันไม่ได้ (มักเป็น library ภายนอกหรือ legacy code ที่แก้ไม่ได้)
- Adapter — implement
ITargetแล้วถือ reference ไปยังAdapteeภายใน เมื่อ client เรียกRequest()อะแดปเตอร์จะแปลง argument/ผลลัพธ์แล้วเรียกSpecificRequest()ของ Adaptee แทน - Client — ใช้งานผ่าน
ITargetเท่านั้น ไม่รู้จักAdapteeเลย
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานตรงไปตรงมา: client เรียก method บน ITarget → runtime dispatch ไปที่ implementation จริงคือ Adapter → Adapter แปลงรูปแบบข้อมูล/parameter ให้ตรงกับที่ Adaptee ต้องการ → เรียก Adaptee → แปลงผลลัพธ์กลับ (ถ้าจำเป็น) แล้วส่งคืนให้ client
sequenceDiagram
participant C as Client
participant A as Adapter
participant D as Adaptee
C->>A: Request(data)
A->>A: แปลงรูปแบบข้อมูล
A->>D: SpecificRequest(convertedData)
D-->>A: ผลลัพธ์เดิม
A-->>C: ผลลัพธ์ที่แปลงกลับแล้ว
จุดสำคัญคือ Adapter ไม่เพิ่มพฤติกรรมใหม่ ให้ Adaptee (ต่างจาก Decorator) มันแค่ “แปลภาษา” ระหว่าง2 interface เท่านั้น ความซับซ้อนทั้งหมดของการแปลงถูกเก็บไว้ในจุดเดียว ทำให้ทดสอบและดูแลง่าย
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้ขยายจากเคส payment gateway เดิม ให้เห็นภาพชัดขึ้นว่า Adapter ช่วยแยก business logic ออกจากรายละเอียดของ library ภายนอกอย่างไร
// Target — interface ที่ระบบของเราออกแบบไว้ และ client (เช่น OrderService) เรียกใช้public interface IPaymentGateway{ PaymentResult Pay(decimal amount, string currency);}
public record PaymentResult(bool Success, string TransactionId);
// Adaptee — SDK ของผู้ให้บริการภายนอก ที่เราแก้ code ไม่ได้// สังเกตว่ารับเงินเป็น "สตางค์" (int) และคืนค่าเป็น string ธรรมดา ไม่ตรงกับ IPaymentGateway เลยpublic class LegacyStripeSdk{ public string MakeCharge(int amountInCents, string currencyCode) { // เรียก API จริงของผู้ให้บริการ (จำลอง) return $"ch_{Guid.NewGuid():N}"; }}
// Adapter — แปลง IPaymentGateway ไปเรียก LegacyStripeSdkpublic class StripeAdapter : IPaymentGateway{ private readonly LegacyStripeSdk _stripe;
public StripeAdapter(LegacyStripeSdk stripe) { _stripe = stripe; }
public PaymentResult Pay(decimal amount, string currency) { // แปลงหน่วยเงินจาก decimal (บาท/ดอลลาร์) ไปเป็น int (สตางค์/เซนต์) int cents = (int)(amount * 100);
string transactionId = _stripe.MakeCharge(cents, currency.ToUpperInvariant());
// ห่อผลลัพธ์กลับให้อยู่ในรูปแบบที่ client ของเราคาดหวัง return new PaymentResult(Success: !string.IsNullOrEmpty(transactionId), transactionId); }}
// Client — ไม่รู้จัก LegacyStripeSdk เลย รู้จักแค่ IPaymentGatewaypublic class OrderService{ private readonly IPaymentGateway _paymentGateway;
public OrderService(IPaymentGateway paymentGateway) { _paymentGateway = paymentGateway; }
public void Checkout(decimal total) { var result = _paymentGateway.Pay(total, "THB"); if (!result.Success) throw new InvalidOperationException("ชำระเงินไม่สำเร็จ");
// ... บันทึกคำสั่งซื้อ, ส่งอีเมลยืนยัน ฯลฯ }}
// Composition root — จุดเดียวที่รู้จักทั้ง Adapter และ Adapteevar gateway = new StripeAdapter(new LegacyStripeSdk());var orderService = new OrderService(gateway);ถ้าวันหนึ่งเปลี่ยนผู้ให้บริการเป็นเจ้าอื่น ก็แค่เขียน PayPalAdapter : IPaymentGateway ตัวใหม่ โดยที่ OrderService ไม่ต้องแก้แม้แต่บรรทัดเดียว
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ต้องการใช้ class ที่มีอยู่แล้ว แต่ interface ของมันไม่ตรงกับที่ code ส่วนอื่นคาดหวัง
- ต้องผสาน third-party library หรือ legacy system ที่แก้ไข source ไม่ได้ (หรือไม่อยากแก้ เพราะเสี่ยงกระทบส่วนอื่น)
- ต้องการ isolate code business logic จากรายละเอียดของ external dependency เพื่อให้ทดสอบและสลับ implementation ได้ง่าย (มัก mock ผ่าน Target interface)
- มี interface หลายแบบที่ทำหน้าที่คล้ายกันแต่ signature ต่างกัน และอยากให้ client เรียกผ่าน interface เดียว
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- ถ้าแก้ไข source ของ Adaptee ได้โดยตรงและไม่มีความเสี่ยง การแก้ interface ตรง ๆ อาจง่ายกว่าเพิ่ม layer ใหม่
- ถ้าต้องการ “เพิ่มพฤติกรรม” ให้ object (ไม่ใช่แค่แปลง interface) ให้ใช้ Decorator แทน
- ถ้าปัญหาคือ “interface ทั้งชุดซับซ้อนเกินไป” ไม่ใช่แค่ “ไม่ตรงกัน” ให้พิจารณา Facade ซึ่งย่อ interface ให้ง่ายลง แทนที่จะแปลง interface หนึ่งไปอีกอันหนึ่ง
- ถ้ากำลังออกแบบ boundary ระหว่าง bounded context ในระบบ DDD การใช้ Adapter เดี่ยว ๆ อาจไม่พอ ควรพิจารณา Anti-Corruption Layer ซึ่งกว้างกว่าและรวม translation logic ของทั้ง model ไว้เป็น layer เดียว
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | แยก interface-conversion logic ออกจาก business logic (Single Responsibility) |
| ข้อดี | เพิ่ม Adapter ใหม่ได้โดยไม่แก้ code client เดิม (Open/Closed Principle) |
| ข้อดี | ทำให้ mock/stub ได้ง่ายในการทดสอบ เพราะ client ผูกกับ interface ไม่ใช่ class จริง |
| ข้อดี | ช่วยให้ swap ผู้ให้บริการภายนอก (เช่น payment gateway) ได้โดยกระทบน้อย |
| ข้อเสีย | เพิ่มจำนวน class และ interface ทำให้ code ซับซ้อนขึ้นเล็กน้อย |
| ข้อเสีย | ถ้า Adaptee เปลี่ยน API บ่อย ต้องคอยอัปเดต Adapter ตาม |
| ข้อเสีย | ถ้าใช้พร่ำเพรื่อโดยไม่จำเป็น (เช่น Adaptee ไม่เคยเปลี่ยนและ interface ตรงกันอยู่แล้ว) อาจเป็นการเพิ่ม abstraction โดยไม่ได้ประโยชน์ |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Facade — ย่อ interface ที่ซับซ้อนให้ง่ายลง แทนที่จะแปลง interface หนึ่งไปอีกอันหนึ่งเหมือน Adapter
- Decorator — เพิ่มพฤติกรรมให้ object โดยคง interface เดิม ต่างจาก Adapter ที่เปลี่ยน interface
- Bridge — แยก abstraction ออกจาก implementation ตั้งแต่ต้นออกแบบ ต่างจาก Adapter ที่มักใช้แก้ปัญหาย้อนหลังกับ code ที่มีอยู่แล้ว
- Proxy — ควบคุมการเข้าถึง object โดยคง interface เดิม ไม่ได้แปลง interface เหมือน Adapter
- Anti-Corruption Layer — แนวคิดระดับ bounded context ใน DDD ที่มักนำ Adapter ไปประกอบใช้เพื่อกัน model ภายนอกไม่ให้ปนเปื้อน domain model ของเรา