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 เดียวกัน)
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้แสดง virtual proxy สำหรับโหลดรูปภาพ (เลื่อนโหลดจนกว่าจะถูก render จริง) และเพิ่ม protection proxy คั่นอีกชั้นเพื่อควบคุมสิทธิ์:
// interface ร่วมระหว่าง real subject และ proxypublic 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 ซ้ำ