はじめに
GoFデザインパターンの一つで構造に関するパターンである
Proxyパターンについて見ていきます.
Proxyパターンとは
Proxy(代理人)パターンは,オブジェクトへアクセスする際に
代理のオブジェクトを使うようなデザインパターンです.
代理のオブジェクトを用意することで,元のオブジェクトへリクエストが
行く前後にアクションを起こすことができます.
Proxyパターンのメリット
Proxyパターンのメリットは以下のようなものが挙げられます.
- アクセスの制限
- メモリの節約
- 透過性
Proxyパターンの構造
この内容については、Proxyパターンとは|GoFデザインパターンの解説で
以下のように説明されています.
- Subject:本来のオブジェクトのインターフェースを定義します
- RealSubject:Subjectの実装クラスで、実際の処理を行います
- Proxy:RealSubjectへのアクセスを制御する代理クラスです
Proxyパターンの実装(適用前)
オンラインショップのショッピングカートのシステムで,
商品を注文に追加する方法を考えます.
考えるべきことは以下となります.
- 顧客が商品を注文する
- それぞれの商品には値段がある
- 各商品の注文数を保持する
これらを踏まえて,存在するオブジェクトとして
Customer, Order, Item, Productを用意します.
このとき,クラス図をシンプルな以下の構造で考えます.
クラス図
これは,Customerが0個以上のOrderをして,
それぞれのProductを何個OrderしたかをItemが保持するというものです.
このクラス図に,新しいアイテムをオーダーに追加する方法を考えると,
OrderクラスにaddItemメソッドを追加するのが自然に考えられます.
データベースへの拡張
オンラインで扱うシステムにおいてデータベースは不可欠です.
このとき,それぞれのオブジェクトがデータベースに保存されているデータだとします.
データベースに保存されているものを取り出すためにIdを指定する必要があります.
そのため,上記のクラス図にidを追加したのが以下のクラス図となります.
id追加後のクラス図
ここで,skuはStock Keeping Unit(在庫管理コード)です.
上記のクラス図のポイントは以下の通りになります
- 特定のカスタマからのオーダーを探すため,そのカスタマID(cusid)を持つ
オーダーをすべて探す - 特定のオーダーに登録されているアイテムを探すため,オーダーID(orderId)を持つ
アイテムをすべて探す - アイテムを指定してその商品を参照するためskuを使う
データベースを用いて,アイテムをオーダーに追加する場合もデータベースへ拡張する前と
ほとんど同じ機能を要するメソッドを追加することで実現できます.
異なる点は,ソースコードにデータベースを使う処理を書かなければいけない点です.
問題点
しかし,このプログラムには問題があります.
それは,データベースを使う処理を書くことによって,
ソフトウェア開発における原則を破る可能性がある点です.
例えば,アイテムとオーダーにおいて,SQL文やデータベース接続といった処理は必要ないので
単一責任の原則
を破っていることになります.
その他にも,(コードの構造にもよりますが)依存関係逆転の原則
など様々な
ソフトウェア開発における原則を破ってしまう場合があります.
このようなソフトウェアでは問題が発生してしまったときに困ります.
この不具合を解決するためにProxyパターンを適用してみます.
Proxyパターンの実装(適用後)
ProxyパターンをProductクラスに適用した場合のクラス図は以下のようになります.
クラス図
それぞれのオブジェクトをProxyパターンの構造に当てはめると以下のようになります.
-
Subject
Productオブジェクトが当てはまり,Productクラスをインタフェースに変えて
Proxyパターンを使えるようにしています. -
RealSubject
ProductImplementationオブジェクトが当てはまり,インタフェースです.
Proxyがこのインスタンスを作成するなどの実際の処理を行うクラスです. -
Proxy
Product DB Proxyオブジェクトが当てはまり,ProductImplementationへの
制御を行います.
このパターンを踏まえてオーダーに商品を追加するコードを実装すると
以下のようになります.
(AIを参考にして作ったものです.)
ソースコード
public class Customer {
private String cusId;
private String name;
public Customer(String cusId, String name) {
this.cusId = cusId;
this.name = name;
}
public String getCusId() {
return cusId;
}
public String getName() {
return name;
}
}
class Order {
private String orderId;
private List<Item> items = new ArrayList<>();
private Customer customer;
public Order(String orderId, Customer customer) {
this.orderId = orderId;
this.customer = customer;
}
public void addItem(String sku, int quantity) {
items.add(new Item(orderId, sku, quantity));
}
public String getOrderId() {
return orderId;
}
public List<Item> getItems() {
return items;
}
public Customer getCustomer() {
return customer;
}
}
public class Item {
private String orderId;
private int quantity;
private Product product;
public Item(String orderId, String sku, int quantity) {
this.orderId = orderId;
this.product = new ProductProxy(sku);
this.quantity = quantity;
}
public String getOrderId() {
return orderId;
}
public Product getProduct() {
return product;
}
public int getQuantity() {
return quantity;
}
}
public interface Product {
String getSku();
String getName();
double getPrice();
}
public class ProductImplementation implements Product {
private String sku;
private String name;
private double price;
public ProductImplementation(String sku, String name, double price) {
this.sku = sku;
this.name = name;
this.price = price;
System.out.println("Loading product from DB: " + name + " (SKU: " + sku + ")");
}
@Override
public String getSku() {
return data.getSku();
}
@Override
public String getName() {
return data.getName();
}
@Override
public double getPrice() {
return data.getPrice();
}
}
public class ProductProxy implements Product {
private ProductImplementation productImplementation;
private String sku;
public ProductProxy(String sku) {
this.sku = sku;
}
private void loadProductIfNeeded() {
if (productImplementation == null) {
productImplementation = DB.loadProduct(sku)
}
}
@Override
public String getSku() {
return sku;
}
@Override
public String getName() {
loadProductIfNeeded();
return productImplementation.getName();
}
@Override
public double getPrice() {
loadProductIfNeeded();
return productImplementation.getPrice();
}
}
public cladd DB{
public ProductImplementation loadProduct(String sku){
// 商品データの獲得処理
}
}
public class Main {
public static void main(String[] args) {
// カスタマーを作成
Customer customer = new Customer("CUS001", "John Doe");
// 注文を作成
Order order = new Order("ORD001", customer);
// 商品を注文に追加(この時点ではデータは読み込まれない)
order.addItem("SKU001", 2);
order.addItem("SKU002", 1);
// 商品情報にアクセスした時点で初めてDBから読み込まれる
System.out.println("Order ID: " + order.getOrderId());
System.out.println("Customer ID: " + order.getCustomer().getCusId());
for (Item item : order.getItems()) {
System.out.println(
"Order ID: " + item.getOrderId() +
", SKU: " + item.getProduct().getSku() +
", Product: " + item.getProduct().getName() +
", Price: " + item.getProduct().getPrice() +
", Quantity: " + item.getQuantity()
);
}
}
}
全体のクラス図は以下のようになります.
クラス図
問題点の解決・メリット
ソースコードとクラス図を参考に問題点が解決されていることやメリットを確認します.
単一責任の原則
各クラスに注目すると,CutomerやOrder,ItemといったクラスにおいてSQL文やデータベース接続といった処理を書く必要がなくなりました.
これにより,それぞれのクラスの役割(責任)はただ一つに絞ることができました.
また,DBに関する処理はDBファイルを追加することで凝集性が向上します.
パフォーマンスの最適化
OrderクラスにItemを追加することを考えます.
そのとき用いるメソッドは以下のようなaddItemメソッドです.
public void addItem(String sku, int quantity) {
items.add(new Item(orderId, sku, quantity)); // この時点でProxyのみ作成
}
このメソッドを使用して,OrderクラスにItemを追加した段階では,Proxyオブジェクトを
作っただけとなっています.
つまり,実際にデータベースへのアクセスは行っていないことになります.
データベースへのアクセスは,DBクラスのloadProductメソッドを使用して
初めて行われます.
これにより,メモリ使用量の削減や不要なDBアクセスを回避することができ,
必要なときにだけDBにアクセスします.
アクセス制御の管理
Proxyクラスにアクセスロジックを集中させることで,
アクセス制御の管理を容易に行うことができます.
キャッシュ機能など,新たな機能実装時もキャッシュの管理をProxyに担当させることで,
アクセス制御に関するロジックを一つにまとめることができます.
インタフェースの共有
Proxyは実際のオブジェクトと同じインターフェースを実装することで,
透過的なアクセスを実現します.
呼び出し側のコードは自分が直接ProductImplementationを使用していると
考えています.
しかし,実際には,呼び出し側とProductImplementationの間にProxyが存在し,
データベースアクセスやネットワーク通信などの処理を代理で行っています.
このとき,呼び出し側はこれらの複雑な処理の存在を意識する必要がありません.
Proxyが複雑な処理を隠蔽することで,システムを変更する際に呼び出し側のコードを
変更する必要がなくなります.
Proxyパターンの問題点
このソースコード,実は問題があります.
それは,ProductImplementationを作る必要がないという点です.
...ProductProxyはProductImplementationが返すデータを既に持っています.
つまり,ProductImplementationはリソースを無駄遣いしており,
不必要なオブジェクト生成をしていることになります.
このような問題がなぜ発生したのか?
それは,Proxyパターンの構造に従ってソースコードを書いたからです.
このように,Proxyパターンは取り扱いが難しく,うまくいくことはなかなかありません.
したがって,「アジャイルソフトウェア開発の奥義」によると
まずはFecadeパターンから用いることがオススメとされています.
最後に
Proxyパターンを適用するのは難しく,実用的に扱うことはないのかなと思いました.
とはいえ,デザインパターンがどのような意図で設計されているのかを理解することが
デザインパターンを学ぶ上で大事だと思っています.
(今回でいうと,データベースによる責任が誰のものかわからない)
そのため,引き続き様々なデザインパターンを学んでいきたいと思います.
参考