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

Proxy

ตัวแทน​ที่​ควบคุม​การ​เข้าถึง object อื่น

บาง​ครั้ง object จริง​ที่ client ต้องการ​ใช้งาน​นั้น “แพง” หรือ “อ่อนไหว” เกิน​กว่า​จะ​ให้ client เข้าถึง​ตรง ๆ — อาจ​เป็น​เพราะ​สร้าง​ช้า (โหลด file ใหญ่, เชื่อม​ต่อ database), อยู่​คนละ​เครื่อง (remote service), หรือ​มี​ข้อมูล​ที่​ต้อง​จำกัด​สิทธิ์​การ​เข้าถึง (sensitive data) ปัญหา​คือ​ถ้า​ให้ client อ้างอิง object จริง​โดยตรง code ของ client จะ​ผูก​ติด​กับ​รายละเอียด​เหล่า​นั้น​ทันที — ต้อง​รู้​ว่า​เมื่อไหร่​ควร​สร้าง, ต้อง​เช็ค​สิทธิ์​เอง, หรือ​ต้อง​จัดการ network call เอง

Proxy แก้​ปัญหา​นี้​ด้วย​การ​สร้าง ตัวแทน (placeholder) ที่​มี interface เดียว​กับ object จริง (real subject) แล้ว​ยืน​คั่น​กลาง​ระหว่าง client กับ object จริง​นั้น proxy ควบคุม​การ​เข้าถึง​ก่อน​ส่ง​ต่อ​คำขอ (forward) ไป​ยัง object จริง — จะ​เลื่อน​การ​สร้าง, เช็ค​สิทธิ์, cache ผลลัพธ์, หรือ log ก่อน​ส่ง​ต่อ​ก็ได้ ทั้งหมด​นี้​เกิด​ขึ้น​อย่าง​โปร่งใส client ไม่รู้ตัว​ว่า​กำลัง​คุย​กับ proxy อยู่ เพราะ​เรียก​ผ่าน interface เดียวกันเป๊ะ

แนวคิด​นี้​มา​จาก Gang of Four (Design Patterns, 1994) และ​มี​หลาย​ชนิด​ย่อย​ตาม​จุด​ประสงค์:

  • Virtual proxy — เลื่อน​การ​สร้าง​และ initialize ทรัพยากร​จริง​จนกว่า​จะ​จำเป็น (ORM อย่าง Entity Framework ใช้​เทคนิค​นี้​กับ navigation property)
  • Remote proxy — เป็น​ตัวแทน​ของ object ที่​อยู่​คนละ address space หรือ​คนละ​เครื่อง (เช่น gRPC client stub, WCF proxy)
  • Protection proxy — ควบคุม​สิทธิ์​การ​เข้าถึง ตรวจสอบ​ว่า client มี​สิทธิ์​เรียก method นั้น​หรือ​ไม่​ก่อน​ส่ง​ต่อ
  • Smart reference / caching proxy — เพิ่ม​พฤติกรรม​เสริม​เมื่อ​มี​การ​เข้าถึง object เช่น นับ reference, cache ผลลัพธ์​เดิม, หรือ lock ทรัพยากร

ข้อ​ควร​ระวัง​คือ​ความ​ซับซ้อน​ที่​เพิ่ม​ขึ้น​จาก​การ​มี class คั่น​กลาง​อีก​ชั้น — ถ้า proxy ไม่​ได้​แก้​ปัญหา​จริง (เช่น เข้าถึง object ตรง ๆ ก็​ถูก​อยู่​แล้ว ไม่​แพง ไม่​ต้อง​คุม​สิทธิ์) ก็​ไม่​คุ้ม​ที่​จะ​เพิ่ม layer นี้

proxy กับ real subject implement interface (ISubject) เดียวกัน ทำให้ client เรียก​ผ่าน interface นั้น​ได้​โดย​ไม่​ต้อง​รู้​ว่า​กำลัง​คุย​กับ​ใคร proxy ถือ reference ไป​ยัง real subject (สร้าง​ตอน​นั้น​หรือ​ได้​รับ​มา​ก็ได้) แล้ว​มอบหมาย (delegate) งาน​ต่อ พร้อม​แทรก​พฤติกรรม​เสริม​ก่อน/หลัง​การ​ส่ง​ต่อ

classDiagram
    class Client {
    }
    class ISubject {
      +Request()
    }
    class RealSubject {
      +Request()
    }
    class Proxy {
      -RealSubject real
      +Request()
    }
    Client --> ISubject
    ISubject <|.. RealSubject
    ISubject <|.. Proxy
    Proxy o-- RealSubject
  • ISubject — interface ร่วม​ที่ client เห็น กำหนด operation ที่​ทั้ง real subject และ proxy ต้อง implement
  • RealSubject — object จริง​ที่​ทำงาน​ตาม​หน้าที่​ธุรกิจ อาจ​สร้าง​ช้า อยู่​ไกล หรือ​อ่อนไหว​ต่อ​สิทธิ์
  • Proxy — ถือ reference (หรือ​รู้​วิธี​สร้าง) RealSubject ควบคุม​การ​เข้าถึง​ก่อน​ส่ง​ต่อ​คำขอ

เมื่อ client เรียก Request() บน proxy ผ่าน ISubject proxy จะ​ตัดสิน​ใจ​ว่า​จะ​ทำ​อะไร​ก่อน​ส่ง​ต่อ เช่น เช็ค​ว่า​มี real subject อยู่​แล้ว​หรือ​ยัง (ถ้า​ยัง​ก็​สร้าง​ตอน​นี้ — lazy init), เช็ค​สิทธิ์​ผู้​เรียก, หรือ​ดู cache ก่อน​ว่า​มี​ผลลัพธ์​เดิม​อยู่​แล้ว​หรือ​ไม่ ถ้า​ทุก​อย่าง​ผ่าน proxy จะ delegate ไป​ยัง real subject แล้ว​อาจ​แปลง/บันทึก​ผลลัพธ์​ก่อน​ส่ง​กลับ client

ลำดับ​การ​ทำงาน​ของ virtual proxy (lazy loading) ทั่วไป​เป็น​แบบ​นี้:

sequenceDiagram
    participant C as Client
    participant P as Proxy
    participant R as RealSubject
    C->>P: Request()
    alt ยังไม่เคยสร้าง RealSubject
        P->>R: new RealSubject()
        activate R
    end
    P->>R: Request()
    R-->>P: ผลลัพธ์
    deactivate R
    P-->>C: ผลลัพธ์

จุด​สำคัญ​คือ client เรียก proxy เหมือน​เรียก real subject ทุก​ประการ — สลับ implementation ระหว่าง proxy กับ object จริง​ได้​โดย​ไม่​กระทบ code client เลย (Liskov substitutability ผ่าน interface เดียวกัน)

ตัวอย่าง​นี้​แสดง virtual proxy สำหรับ​โหลด​รูปภาพ (เลื่อน​โหลด​จนกว่า​จะ​ถูก render จริง) และ​เพิ่ม protection proxy คั่น​อีก​ชั้น​เพื่อ​ควบคุม​สิทธิ์:

// interface ร่วมระหว่าง real subject และ proxy
public interface IImage
{
void Render();
}
// real subject: การสร้างมีต้นทุนสูง (โหลด file จาก disk/เครือข่าย)
public class RealImage : IImage
{
private readonly string _path;
public RealImage(string path)
{
_path = path;
LoadFromDisk(); // ค่าใช้จ่ายสูง เกิดขึ้นตอนสร้าง object เท่านั้น
}
private void LoadFromDisk() =>
Console.WriteLine($"กำลังโหลด file รูปจริงจาก disk: {_path}");
public void Render() =>
Console.WriteLine($"แสดงผลรูป: {_path}");
}
// virtual proxy: เลื่อนการสร้าง RealImage จนกว่าจะถูกใช้ครั้งแรก
public class ImageProxy : IImage
{
private readonly string _path;
private RealImage? _real; // ยังไม่โหลดจนกว่าจะถูกเรียก
public ImageProxy(string path) => _path = path;
public void Render()
{
_real ??= new RealImage(_path); // โหลดครั้งแรกที่ถูกเรียกจริงเท่านั้น
_real.Render();
}
}
// protection proxy: คั่นอีกชั้นเพื่อเช็คสิทธิ์ก่อนส่งต่อ
public class ProtectedImageProxy : IImage
{
private readonly IImage _inner;
private readonly Func<bool> _hasAccess;
public ProtectedImageProxy(IImage inner, Func<bool> hasAccess)
{
_inner = inner;
_hasAccess = hasAccess;
}
public void Render()
{
if (!_hasAccess())
{
Console.WriteLine("ปฏิเสธการเข้าถึง: ไม่มีสิทธิ์ดูรูปนี้");
return;
}
_inner.Render(); // ส่งต่อให้ proxy ชั้นในหรือ real subject
}
}
// การใช้งาน — client เห็นแค่ IImage ไม่รู้ว่ามี proxy กี่ชั้น
IImage image = new ProtectedImageProxy(
new ImageProxy("photo.jpg"),
hasAccess: () => currentUser.IsAuthenticated
);
image.Render(); // ยังไม่โหลด file จนกว่าจะเรียกถึงตรงนี้ และผ่านการเช็คสิทธิ์ก่อน
  • ต้องการ เลื่อน​การ​สร้าง object ที่​มี​ต้นทุน​สูง จนกว่า​จะ​ถูก​ใช้​จริง (virtual proxy / lazy loading)
  • ต้องการ ควบคุม​สิทธิ์​การ​เข้าถึง object โดย​ไม่​ปน​ตรรกะ​สิทธิ์​เข้า​กับ​ตัว object เอง (protection proxy)
  • ต้องการ​เรียก object ที่​อยู่ คนละ process หรือ​คนละ​เครื่อง ให้​ดูเหมือน object ใน​เครื่อง​เดียวกัน (remote proxy เช่น gRPC/WCF client stub)
  • ต้องการ cache ผลลัพธ์ ของ operation ที่​แพง แล้ว​คืน​ค่า​เดิม​โดย​ไม่​เรียก​ซ้ำ (caching proxy)
  • ต้องการ log หรือ audit ทุก​การ​เข้าถึง object โดย​ไม่​แก้ code ของ object จริง
  • ORM ต้องการ navigation property แบบ lazy load โดย​ไม่​ให้ client รู้​ว่า​กำลัง​คุย​กับ proxy อยู่
  • เมื่อ​การ​เข้าถึง object จริง​ไม่มี​ต้นทุน​สูง ไม่​ต้อง​คุม​สิทธิ์ และ​ไม่​ต้อง​เพิ่ม​พฤติกรรม​ใด ๆ — proxy จะ​เพิ่ม​ความ​ซับซ้อน​โดย​ไม่​ได้​อะไร​กลับ​มา
  • เมื่อ​สิ่ง​ที่​ต้องการ​จริง ๆ คือ เพิ่ม​พฤติกรรม​แบบ compose ได้​หลาย​ชั้น​ตาม​ต้องการ (เช่น ห่อ stream หลาย​ชั้น) — กรณี​นั้น​ควร​ใช้ Decorator ซึ่ง​เน้น​เพิ่ม​ความ​รับผิดชอบ​มากกว่า​เน้น​ควบคุม​การ​เข้าถึง
  • เมื่อ latency ที่​เพิ่ม​จาก layer คั่น​กลาง (โดย​เฉพาะ remote proxy) กระทบ performance เกิน​กว่า​ที่​ยอมรับ​ได้​ใน​เส้นทาง​วิกฤต (hot path)
  • เมื่อ​ทีม​สามารถ​แก้​ที่​ต้นตอ​ได้​ตรง ๆ (เช่น ทำให้ constructor ของ object จริง​เร็ว​ขึ้น) แทนที่​จะ​ห่อ​ด้วย proxy เพื่อ​กลบ​ปัญหา
ด้านรายละเอียด
ข้อดีclient ควบคุม access โดย​ไม่รู้ตัว เพราะ​เรียก​ผ่าน interface เดียวกัน
ข้อดีจัดการ lifecycle ของ object หนัก​ได้​อย่าง​เป็น​ระบบ (สร้าง​เมื่อ​จำเป็น, ปล่อย​เมื่อ​ไม่​ใช้)
ข้อดีทำงาน​ได้​แม้ real subject ยัง​ไม่​พร้อม (เช่น service ยัง​ไม่​ตอบ ก็​ยัง queue คำขอ​ผ่าน proxy ได้)
ข้อดีสอดคล้อง​กับ Open/Closed Principle — เพิ่ม​พฤติกรรม​ใหม่​โดย​ไม่​แก้ real subject
ข้อ​เสียเพิ่ม​จำนวน class และ indirection ทำให้ code อ่าน​ยาก​ขึ้น​เล็กน้อย
ข้อ​เสียอาจ​เพิ่ม latency จาก layer คั่น​กลาง โดย​เฉพาะ remote proxy
ข้อ​เสียถ้า proxy หลาย​ชั้น​ซ้อน​กัน (protection + caching + virtual) การ debug ว่า​คำขอ​ไป​ติด​ที่​ชั้น​ไหน​จะ​ยาก​ขึ้น
  • Decorator — เพิ่ม​ความ​รับผิดชอบ​แบบ compose หลาย​ชั้น​ได้ ต่าง​จาก proxy ที่​เน้น​ควบคุม​การ​เข้าถึง
  • Lazy Load — pattern ระดับ​กว้าง​ที่ virtual proxy เป็น​หนึ่ง​ใน​เทคนิค​การ implement
  • Flyweight — ทั้ง​คู่​จัดการ object ที่​มี​ต้นทุน​สูง แต่ flyweight เน้น share state ร่วม​กัน​เพื่อ​ลด​หน่วย​ความ​จำ
  • Facade — คั่น​กลาง​เช่น​กัน แต่ facade ลด​ความ​ซับซ้อน​ของ subsystem ทั้ง​ชุด ไม่ใช่​คุม​การ​เข้าถึง object เดียว
  • Adapter — คั่น​กลาง​เช่น​กัน แต่ adapter แปลง interface ให้​เข้า​กัน​ได้ ไม่ใช่​คุม​การ​เข้าถึง
  • Repository — มัก​ถูก implement คู่​กับ caching proxy เพื่อ​ลด​การ​เรียก database ซ้ำ