Proxy
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Proxy คือตัวแทน (placeholder) ของ object อีกตัว ใช้ interface เดียวกันและคุมทางเข้าถึงเอาไว้ proxy จึงตัดสินใจได้ว่าจะแตะ object จริงหรือไม่ ตอนไหน และอย่างไร
บางครั้งคุณอยากใช้ object ตัวหนึ่ง แต่การเข้าถึงตรง ๆ มีต้นทุนสูง อ่อนไหว หรืออยู่ไกลออกไป รูปความละเอียดสูงไม่ควรโหลดจากดิสก์จนกว่าจะมีใครสั่งแสดงจริง ๆ operation ที่ต้องใช้สิทธิ์พิเศษควรรันให้เฉพาะผู้เรียกที่ได้รับอนุญาต และ remote service ควรเข้าถึงผ่านตัวแทนในเครื่อง ทุกกรณีที่ว่ามา ตัว object จริงไม่ได้มีปัญหาอะไร สิ่งที่ขาดคือชั้นควบคุมทางเข้าออกต่างหาก
Proxy คือชั้นนั้น โดย implement interface เดียวกับ subject ตัวจริง client จึงแยกไม่ออกว่ากำลังคุยกับใคร และภายในก็ถือ reference ไปยัง object จริง (หรือรู้วิธีไปเอามา) ทุกครั้งที่ถูกเรียก proxy จะแทรกงานของตัวเองก่อนได้ เช่น สร้าง subject แบบ lazy ตรวจสิทธิ์ cache ผลลัพธ์ หรือบันทึกการเข้าถึง แล้วค่อยส่งต่อไปยัง object จริง หรือปฏิเสธไปเลย เพราะ proxy ใช้แทน subject ได้ คุณจึงสอดการควบคุมพวกนี้เข้าไปได้โดยไม่ต้องแก้ code ฝั่ง client เลยสักบรรทัด
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Image {
<<interface>>
+display() string
}
class RealImage {
-file: string
+display() string
}
class LazyImageProxy {
-file: string
-real: RealImage
+display() string
}
class Client
Image <|.. RealImage
Image <|.. LazyImageProxy
LazyImageProxy --> RealImage : creates and forwards on demand
Client --> Image : uses - Subject (
Image) — interface ที่ object จริงกับ proxy ใช้ร่วมกัน เพื่อให้สลับแทนกันได้ - Real Subject (
RealImage) — object จริงที่ทำงานและมีต้นทุนสูงหรืออ่อนไหวต่อการเข้าถึง - Proxy (
LazyImageProxy) — implement subject interface ควบคุมการเข้าถึง และส่งต่อไปยัง real subject เมื่อเหมาะสม - Client — คุยผ่าน subject interface โดยไม่รู้ว่าปลายทางเป็น proxy หรือ object จริง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”virtual proxy ที่หน่วงการโหลดรูปภาพหนักไว้จนกว่าจะมีการเรียก display ครั้งแรก แล้วจึง cache object ที่โหลดแล้วไว้ใช้ซ้ำ
interface Image { display(): string;}
class RealImage implements Image { constructor(private readonly file: string) { // Pretend this reads a large file from disk. console.log(`loading ${file}`); } display = () => `showing ${this.file}`;}
class LazyImageProxy implements Image { private real: RealImage | null = null; constructor(private readonly file: string) {} display(): string { if (this.real === null) { this.real = new RealImage(this.file); // load only on first use } return this.real.display(); }}
const image: Image = new LazyImageProxy('photo.png'); // nothing loaded yetconsole.log(image.display()); // loading photo.png \n showing photo.pngconsole.log(image.display()); // showing photo.png (cached, no reload)from typing import Optional, Protocol
class Image(Protocol): def display(self) -> str: ...
class RealImage: def __init__(self, file: str) -> None: print(f"loading {file}") # pretend to read a large file self._file = file
def display(self) -> str: return f"showing {self._file}"
class LazyImageProxy: def __init__(self, file: str) -> None: self._file = file self._real: Optional[RealImage] = None
def display(self) -> str: if self._real is None: self._real = RealImage(self._file) # load only on first use return self._real.display()
image: Image = LazyImageProxy("photo.png") # nothing loaded yetprint(image.display()) # loading photo.png \n showing photo.pngprint(image.display()) # showing photo.png (cached, no reload)package main
import "fmt"
type Image interface { Display() string}
type RealImage struct{ file string }
func NewRealImage(file string) *RealImage { fmt.Printf("loading %s\n", file) // pretend to read a large file return &RealImage{file: file}}
func (r *RealImage) Display() string { return "showing " + r.file }
type LazyImageProxy struct { file string real *RealImage}
func (p *LazyImageProxy) Display() string { if p.real == nil { p.real = NewRealImage(p.file) // load only on first use } return p.real.Display()}
func main() { var image Image = &LazyImageProxy{file: "photo.png"} // nothing loaded yet fmt.Println(image.Display()) // loading photo.png + showing photo.png fmt.Println(image.Display()) // showing photo.png (cached)}trait Image { fn display(&mut self) -> String;}
struct RealImage { file: String,}
impl RealImage { fn new(file: &str) -> Self { println!("loading {file}"); // pretend to read a large file RealImage { file: file.to_string() } }}
impl Image for RealImage { fn display(&mut self) -> String { format!("showing {}", self.file) }}
struct LazyImageProxy { file: String, real: Option<RealImage>,}
impl Image for LazyImageProxy { fn display(&mut self) -> String { if self.real.is_none() { self.real = Some(RealImage::new(&self.file)); // load on first use } self.real.as_mut().unwrap().display() }}
fn main() { let mut image = LazyImageProxy { file: "photo.png".to_string(), real: None }; println!("{}", image.display()); // loading photo.png + showing photo.png println!("{}", image.display()); // showing photo.png (cached)}เปรียบเทียบกับ Decorator และ Facade
หัวข้อที่มีชื่อว่า “เปรียบเทียบกับ Decorator และ Facade”ทั้งสามตัวห่อของเหมือนกัน แต่จุดประสงค์คนละเรื่อง Decorator ใช้ interface ของ subject ร่วมกันและ เพิ่มพฤติกรรม ให้ผลลัพธ์ของแต่ละการเรียกสมบูรณ์ขึ้น คุณจึงซ้อน decorator หลายชั้นเพื่อประกอบ feature ได้
ส่วน Proxy ใช้ interface เดียวกันเหมือนกัน แต่หน้าที่คือ ควบคุมการเข้าถึง คือตัดสินใจว่าการเรียกจริงจะเกิดขึ้นหรือไม่ (lazy loading, ตรวจสิทธิ์, caching) ไม่ได้ไปเสริมค่าที่ส่งกลับ และมักดูแล lifecycle ของ real subject ตัวเดียว
Facade ไม่ทำทั้งสองอย่าง แต่ออกแบบ interface ใหม่ที่ง่ายกว่า ครอบ subsystem ทั้งก้อนที่มี class เต็มไปหมด ดังนั้น facade จึงไม่ได้ใช้ interface ของ subject ร่วมกัน และใช้แทน object เบื้องล่างตัวใดตัวหนึ่งไม่ได้
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: ควบคุมการเข้าถึงอย่างโปร่งใส คือ lazy loading การควบคุมการเข้าถึง การ caching หรือ remoting โดยไม่ต้องเปลี่ยน client
- ข้อดี: หน่วงหรือหลีกเลี่ยงงานที่มีต้นทุนสูงจนกว่าจะจำเป็นจริง ๆ (virtual proxy)
- ข้อดี: รวมงาน cross-cutting อย่างการตรวจสิทธิ์หรือ logging ไว้ที่จุดเดียว
- ข้อเสีย: เพิ่มชั้นของ indirection ที่อาจบดบังว่างานหรือ latency เกิดขึ้นที่ไหนจริง ๆ
- ข้อเสีย: proxy แบบ lazy หรือ remote สร้างความประหลาดใจเรื่องจังหวะเวลา คือการเรียกครั้งแรกทำงานต่างจากครั้งต่อ ๆ มามาก
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Decorator ใช้รูปแบบการห่อร่วมกันแต่เพิ่มพฤติกรรมแทนที่จะควบคุมการเข้าถึง
- Facade ทำให้ subsystem เรียบง่ายไว้หลัง interface ใหม่ ขณะที่ proxy คงไว้ซึ่ง interface เดียวกับ subject ตัวเดียว
- Adapter เปลี่ยน interface ให้ตรงกับ client ส่วน proxy จงใจคงไว้ซึ่ง interface ที่เหมือนเดิม
| Proxy | Decorator | Adapter | |
|---|---|---|---|
| จุดประสงค์ | ควบคุม access ไปยัง object | เพิ่ม behavior โดยไม่เปลี่ยน interface | แปลง interface ให้ตรงกัน |
| client รู้ไหม | ไม่รู้ (interface เดียวกัน) | ไม่รู้ (interface เดียวกัน) | ไม่รู้ (interface ใหม่) |
| ประเภทที่พบบ่อย | virtual, protection, remote, cache | logging, validation, caching | legacy wrapper, API bridge |
| ตัวอย่าง | ES6 Proxy, lazy image load | stream.pipe(), middleware | JDBC driver, fetch polyfill |