構造に関するパターン
Proxy
必要になってから作る
概要
インタフェース(メソッド名)が同じでより軽いオブジェクトを挟んで負荷対策
日常は代用品のインタフェースを呼び出して、使う時になったら初めて本物のインタフェースを代用品経由で呼び出す
マンガでわかる Proxy
マンガでわかる Proxy #デザインパターン - Qiita
でざぱたんで覚える Proxy
ちびキャラは「プロキシたん」。中身をパイ皮で包み隠す食えないパン職人で、決め台詞は「パイの中身?知らない方が良いと思うよ」。同じ皮(インターフェース)で包んでしまえば、中身を遅延実行や非同期処理に差し替えても呼び出し側は気づかない、というラッパーの隠蔽性の擬人化。恩恵を得るにはインスタンス生成箇所を集約しておくのが肝。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.063〜)
登場人物
- Subject: 主体。RealSubjectとProxy共通のインタフェース
- Proxy: 代理人。
- Clientからの要求をできるだけ処理して、自分では処理できないときに限りRealSubjectを生成
- Subjectを実装
- RealSubject: 実際の主体
- Client:利用者。Subjectを利用
クラス図
このサイトの実装(プリンタの例)での対応関係:
classDiagram
class Printable {
<<interface>>
+setPrinterName(name)
+getPrinterName() String
+print(string)
}
class PrinterProxy {
-name String
-real Printer
+setPrinterName(name)
+getPrinterName() String
+print(string)
-realize()
}
class Printer {
-name String
+setPrinterName(name)
+getPrinterName() String
+print(string)
-heavyJob(msg)
}
Printable <|.. PrinterProxy
Printable <|.. Printer
PrinterProxy o-- Printer
やり方
(TODO: ADD)
メリット
メモリ節約
- RealSubjectはクソ重いオブジェクトであり、生成に時間がかかる場合を想定する
- プリンタ
- 画像
- 簡単な仕事はProxyにやらせて必要になってから重い仕事を本物にやらせれば良い
分割統治
- やろうと思えばRealSubjectに最初から遅延評価(必要になってからインスタンス生成する)を組み込むこともできなくもない
- しかしインタフェースで分けることで、Proxyにいくら処理を追加しても、RealSubjectには影響はない。分割統治である。
- 遅延評価する必要がないなら最初からRealSubjectをnewすればよく、それがProxyの機能を使う/使わないの選択として表現される
関連
Flyweight
- Flyweightの実現手段としてのProxy。クソ重いオブジェクトの生成を、Proxyを代わりに生成して同じクソ重いオブジェクトを参照させるなど
Java
PrinterProxy.java
public class PrinterProxy implements Printable {
private String name; // 名前
private Printer real; // 「本人」
public PrinterProxy() {
}
public PrinterProxy(String name) { // コンストラクタ
this.name = name;
}
public synchronized void setPrinterName(String name) { // 名前の設定
if (real != null) {
real.setPrinterName(name); // 「本人」にも設定する
}
this.name = name;
}
public String getPrinterName() { // 名前の取得
return name;
}
public void print(String string) { // 表示
realize();
real.print(string);
}
private synchronized void realize() { // 「本人」を生成
if (real == null) {
real = new Printer(name);
}
}
}
Printable.java
public interface Printable {
public abstract void setPrinterName(String name); // 名前の設定
public abstract String getPrinterName(); // 名前の取得
public abstract void print(String string); // 文字列表示(プリントアウト)
}
Main.java
public class Main {
public static void main(String[] args) {
Printable p = new PrinterProxy("Alice");
System.out.println("名前は現在" + p.getPrinterName() + "です。");
p.setPrinterName("Bob");
System.out.println("名前は現在" + p.getPrinterName() + "です。");
p.print("Hello, world.");
}
}
Printer.java
public class Printer implements Printable {
private String name;
public Printer() {
heavyJob("Printerのインスタンスを生成中");
}
public Printer(String name) { // コンストラクタ
this.name = name;
heavyJob("Printerのインスタンス(" + name + ")を生成中");
}
public void setPrinterName(String name) { // 名前の設定
this.name = name;
}
public String getPrinterName() { // 名前の取得
return name;
}
public void print(String string) { // 名前付きで表示
System.out.println("=== " + name + " ===");
System.out.println(string);
}
private void heavyJob(String msg) { // 重い作業(のつもり)
System.out.print(msg);
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.print(".");
}
System.out.println("完了。");
}
}
Go
sync.MutexでJavaのsynchronized相当を表現。名前設定はProxyだけで完結し、実際に印字が必要になった瞬間に初めて重いPrinterを生成する遅延がログで分かる。
実行: go run ./GoF/patterns/Proxy/go
$ go run ./GoF/patterns/Proxy/go
printable.go
package main
// Printable はプリンタの「本人」と「代理人」が共通して実装するインタフェース。
// 呼び出し側はPrintableだけを見て、相手がProxyか本物かを意識しない。
//
// Go の interface は暗黙実装(implements を書かない)。
// ここが Java との一番わかりやすい違い。
type Printable interface {
SetPrinterName(name string) // 名前の設定
GetPrinterName() string // 名前の取得
Print(str string) // 表示
}
printer.go
package main
import (
"fmt"
"time"
)
// Printer は Proxy の向こう側にいる「本人」(RealSubject)。
// 生成コストが高い(という設定)ため、Printable を直接使わせず
// PrinterProxy 経由でしか触らせないことを想定している。
type Printer struct {
name string
}
// NewPrinter は Printer を生成する。ここが「重い処理」。
// Java版は1000ms x 5回sleepするが、デモの実行時間短縮のため
// 150ms x 5回に縮めている(挙動は同じ、待ち時間だけ違う)。
func NewPrinter(name string) *Printer {
p := &Printer{name: name}
p.heavyJob(fmt.Sprintf("Printerのインスタンス(%s)を生成中", name))
return p
}
func (p *Printer) SetPrinterName(name string) {
p.name = name
}
func (p *Printer) GetPrinterName() string {
return p.name
}
func (p *Printer) Print(str string) {
fmt.Printf("=== %s ===\n", p.name)
fmt.Println(str)
}
// heavyJob は重い作業(のつもり)。
func (p *Printer) heavyJob(msg string) {
fmt.Print(msg)
for i := 0; i < 5; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Print(".")
}
fmt.Println("完了。")
}
printer_proxy.go
package main
import "sync"
// PrinterProxy は Printer の「代理人」。名前の設定・取得はこの中だけで
// 完結させ、実際に Print() が呼ばれるまで Printer の生成(重い処理)を
// 遅延する。これが Proxy パターンの本質=「本当に必要になるまで
// 重い本人を作らない」。
//
// Java版は setPrinterName/realize を synchronized にしているので、
// Go版では sync.Mutex で同じ排他をかける(Flyweight版 BigCharFactory と同じ流儀)。
type PrinterProxy struct {
mu sync.Mutex
name string
real *Printer // 「本人」。Print() で初めて生成されるまで nil
}
func NewPrinterProxy(name string) *PrinterProxy {
return &PrinterProxy{name: name}
}
// SetPrinterName は名前の設定。本人が既に生成済みなら本人にも反映する。
func (p *PrinterProxy) SetPrinterName(name string) {
p.mu.Lock()
defer p.mu.Unlock()
if p.real != nil {
p.real.SetPrinterName(name)
}
p.name = name
}
// GetPrinterName は名前の取得。本人を生成せず Proxy が持つ名前を返すだけ。
func (p *PrinterProxy) GetPrinterName() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.name
}
// Print は表示。ここで初めて本人(Printer)を生成し、処理を委譲する。
// 「本人の生成」と「本人への委譲」を同じロックの中で行うことで、
// SetPrinterName による p.real の差し替え・p.name の書き換えと競合しないようにする
// (Proxyの外に Printer への参照を漏らさない=アクセスは常にProxy経由に一本化するのが肝)。
func (p *PrinterProxy) Print(str string) {
p.mu.Lock()
defer p.mu.Unlock()
if p.real == nil {
p.real = NewPrinter(p.name)
}
p.real.Print(str)
}
main.go
package main
import "fmt"
// 実行:
//
// go run ./GoF/patterns/Proxy/go
//
// PrinterProxy は名前の設定・取得だけをその場で処理し、実際に印字が
// 必要になった瞬間(Print)まで重い Printer の生成を遅らせる。
// ログを見ると「名前設定→印字要求→ここで初めてインスタンス生成→印字」
// の順になっていることがわかる。
func main() {
var p Printable = NewPrinterProxy("Alice")
fmt.Printf("名前は現在%sです。\n", p.GetPrinterName())
p.SetPrinterName("Bob")
fmt.Printf("名前は現在%sです。\n", p.GetPrinterName())
p.Print("Hello, world.")
}
PHP
index.php
<?php
ini_set("display_errors", 1);
// 初期化に時間のかかるものを必要になるまで後回し
// 代理人で処理をデコレートしたり
/**
* パイのインタフェース
*
*
*/
interface IPie
{
public function addMaterial(String $material);
public function fill();
}
/**
* 代理のパイ
* 材料の追加は仮のパイに任せる
* 材料を混ぜ込む時に本物のパイを取り出す
*/
class ProxyPie implements IPie
{
private $realPie = null;
private $materials = array();
/**材料追加 */
public function addMaterial(String $m)
{
if ($this->realPie != null) {
$this->realPie->addMaterial($m);
}
$this->materials[] = $m;
print $m . "を追加するよ<br>";
}
/**
* 材料を混ぜ込む
*
*/
// fillするときに初めて本物のパイのfillが呼び出される
public function fill()
{
if ($this->realPie == null) {
$this->realPie = new RealPie($this->materials);
}
$this->realPie->fill();
}
}
/**
* 本物のパイ
* 生成に時間がかかる
*/
class RealPie implements IPie
{
private $materials;
public function __construct($materials)
{
$this->materials = $materials;
print("実物のパイ生地を作り始める<br>");
sleep(2);
print("実物のパイ生地を作成。<br>");
}
// 材料追加
public function addMaterial($m)
{
$this->materials[] = $m;
}
// 混ぜる
public function fill()
{
foreach ($this->materials as $m) {
print "$m <br>";
}
print "材料をパイに詰め込むよ <br>";
}
}
$proxy = new ProxyPie();
$proxy->addMaterial("人参");
$proxy->addMaterial("よくわからんもの");
$proxy->addMaterial("ケミカルX");
$proxy->fill();
TypeScript
Node.jsはシングルスレッドのイベントループなのでロックは不要(Go版のMutexとの対比)。
実行: npx tsx GoF/patterns/Proxy/typescript/main.ts
$ npx tsx GoF/patterns/Proxy/typescript/main.ts
printable.ts
// Printable: プリンタの「本人」と「代理人」が共通して実装するインタフェース。
// 呼び出し側はPrintableだけを見て、相手がProxyか本物かを意識しない
// (Go版のPrintableと同じ役割。TSではimplementsで明示実装する)。
export interface Printable {
setPrinterName(name: string): void; // 名前の設定
getPrinterName(): string; // 名前の取得
print(str: string): void; // 表示
}
printer.ts
import type { Printable } from "./printable";
// Printer: Proxyの向こう側にいる「本人」(RealSubject)。
// 生成コストが高い(という設定)。
export class Printer implements Printable {
private name: string;
constructor(name: string) {
this.name = name;
this.heavyJob(`Printerのインスタンス(${name})を生成中`);
}
setPrinterName(name: string): void {
this.name = name;
}
getPrinterName(): string {
return this.name;
}
print(str: string): void {
console.log(`=== ${this.name} ===`);
console.log(str);
}
// heavyJobは重い作業(のつもり)。Java版は1000ms x 5回sleepするが、
// デモの実行時間短縮のため150ms x 5回に縮めている(Go/Python版と同じ理由)。
// コンストラクタ内なのでasyncにできず、Node.jsに同期sleepの標準APIもない
// ため、時間切れまで回すビジーウェイトで代用する。
private heavyJob(msg: string): void {
process.stdout.write(msg);
for (let i = 0; i < 5; i++) {
const until = Date.now() + 150;
while (Date.now() < until) {
// busy wait
}
process.stdout.write(".");
}
console.log("完了。");
}
}
printer_proxy.ts
import type { Printable } from "./printable";
import { Printer } from "./printer";
// PrinterProxy: Printerの「代理人」。名前の設定・取得はこの中だけで
// 完結させ、実際にprint()が呼ばれるまでPrinterの生成(重い処理)を遅延する
// (Go版のPrinterProxyと同じ設計)。
//
// Java版はsetPrinterName/realizeをsynchronizedにしているが、
// JS/TSはシングルスレッド(イベントループ)で実行されるためロックは不要
// (Go版のsync.Mutexに相当するものが要らない、という対比になる)。
export class PrinterProxy implements Printable {
private name: string;
private real: Printer | null = null; // 「本人」。realize()されるまでnull
constructor(name: string) {
this.name = name;
}
// 名前の設定。本人が既に生成済みなら本人にも反映する。
setPrinterName(name: string): void {
if (this.real !== null) {
this.real.setPrinterName(name);
}
this.name = name;
}
// 名前の取得。本人を生成せずProxyが持つ名前を返すだけ。
getPrinterName(): string {
return this.name;
}
// 表示。ここで初めてrealize()により本人を生成し、処理を委譲する。
print(str: string): void {
this.realize();
this.real!.print(str);
}
// realizeは「本人」を生成する。既に生成済みなら何もしない。
private realize(): void {
if (this.real === null) {
this.real = new Printer(this.name);
}
}
}
main.ts
// Proxy パターン: プリンタの代理人 (Java版と同じ題材)
//
// 実行:
// npx tsx main.ts
//
// PrinterProxyは名前の設定・取得だけをその場で処理し、実際に印字が
// 必要になった瞬間(print)まで重いPrinterの生成を遅らせる。ログを見ると
// 「名前設定→印字要求→ここで初めてインスタンス生成→印字」の順になって
// いることがわかる(Go版main.goと同じ構成)。
import type { Printable } from "./printable";
import { PrinterProxy } from "./printer_proxy";
function main(): void {
const p: Printable = new PrinterProxy("Alice");
console.log(`名前は現在${p.getPrinterName()}です。`);
p.setPrinterName("Bob");
console.log(`名前は現在${p.getPrinterName()}です。`);
p.print("Hello, world.");
}
main();
Python
threading.LockでJavaのsynchronized相当を表現。
実行: python3 GoF/patterns/Proxy/python/main.py
$ python3 GoF/patterns/Proxy/python/main.py
printable.py
"""Printable: プリンタの「本人」と「代理人」が共通して実装するインタフェース。
呼び出し側はPrintableだけを見て、相手がProxyか本物かを意識しない。
PythonにはJavaのinterfaceに相当する言語機能はないため、Builder版と同じく
抽象基底クラス(ABC, abcモジュール)で表す。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
class Printable(ABC):
@abstractmethod
def set_printer_name(self, name: str) -> None: ... # 名前の設定
@abstractmethod
def get_printer_name(self) -> str: ... # 名前の取得
@abstractmethod
def print(self, string: str) -> None: ... # 表示
printer.py
"""Printer: Proxyの向こう側にいる「本人」(RealSubject)。
生成コストが高い(という設定)。
"""
from __future__ import annotations
import time
from printable import Printable
class Printer(Printable):
def __init__(self, name: str) -> None:
self._name = name
self._heavy_job(f"Printerのインスタンス({name})を生成中")
def set_printer_name(self, name: str) -> None:
self._name = name
def get_printer_name(self) -> str:
return self._name
def print(self, string: str) -> None:
print(f"=== {self._name} ===")
print(string)
def _heavy_job(self, msg: str) -> None:
"""重い作業(のつもり)。Java版は1000ms x 5回sleepするが、デモの実行
時間短縮のため150ms x 5回に縮めている(Go/TS版と同じ理由)。
"""
print(msg, end="", flush=True)
for _ in range(5):
time.sleep(0.15)
print(".", end="", flush=True)
print("完了。")
printer_proxy.py
"""PrinterProxy: Printerの「代理人」。
名前の設定・取得はこの中だけで完結させ、実際にprint()が呼ばれるまで
Printerの生成(重い処理)を遅延する(Go/TS版のPrinterProxyと同じ設計)。
"""
from __future__ import annotations
import threading
from printable import Printable
from printer import Printer
class PrinterProxy(Printable):
def __init__(self, name: str) -> None:
self._name = name
self._real: Printer | None = None # 「本人」。_realize()されるまでNone
# Java版のsetPrinterName/realizeのsynchronizedに相当(Go版のsync.Mutexと同じ)。
self._lock = threading.Lock()
def set_printer_name(self, name: str) -> None:
"""名前の設定。本人が既に生成済みなら本人にも反映する。"""
with self._lock:
if self._real is not None:
self._real.set_printer_name(name)
self._name = name
def get_printer_name(self) -> str:
"""名前の取得。本人を生成せずProxyが持つ名前を返すだけ。"""
with self._lock:
return self._name
def print(self, string: str) -> None:
"""表示。本人の生成と委譲を同じロックの中で行い、set_printer_nameによる
差し替えと競合しないようにする(本人への参照をロックの外に漏らさない)。"""
with self._lock:
if self._real is None:
self._real = Printer(self._name)
self._real.print(string)
main.py
"""Proxy パターン: プリンタの代理人 (Java版と同じ題材)
実行:
python3 main.py
PrinterProxyは名前の設定・取得だけをその場で処理し、実際に印字が必要に
なった瞬間(print)まで重いPrinterの生成を遅らせる。ログを見ると
「名前設定→印字要求→ここで初めてインスタンス生成→印字」の順になって
いることがわかる(Go/TS版main.goと同じ構成)。
"""
from __future__ import annotations
from printable import Printable
from printer_proxy import PrinterProxy
def main() -> None:
p: Printable = PrinterProxy("Alice")
print(f"名前は現在{p.get_printer_name()}です。")
p.set_printer_name("Bob")
print(f"名前は現在{p.get_printer_name()}です。")
p.print("Hello, world.")
if __name__ == "__main__":
main()