ข้าม​ไป​ยัง​เนื้อหา

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 จริง​คือ AdapterAdapter แปลง​รูปแบบ​ข้อมูล/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 เท่านั้น ความ​ซับซ้อน​ทั้งหมด​ของ​การ​แปลง​ถูก​เก็บ​ไว้​ใน​จุด​เดียว ทำให้​ทดสอบ​และ​ดูแล​ง่าย

ตัวอย่าง​นี้​ขยาย​จาก​เคส 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 ไปเรียก LegacyStripeSdk
public 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 เลย รู้จักแค่ IPaymentGateway
public 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 และ Adaptee
var 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 ของ​เรา