blog

デザインパターン学習ノート:ヘドニック・パターン

システムの実行時に生成されるオブジェクトの数が多すぎると、パフォーマンスの低下やその他の問題につながる可能性があります。 たとえば、テキスト文字列に繰り返しの文字が多数ある場合、各文字を別々のオブジェ...

Aug 18, 2020 · 14 min. read
シェア

概要

はじめに

システムが実行時に生成するオブジェクトの数が多すぎると、パフォーマンスの低下やその他の問題を引き起こす可能性があります。たとえば、繰り返し文字が多いテキスト文字列の場合、各文字を別々のオブジェクトで表現すると、メモリ領域が多くなってしまいます。

では、クライアントのオブジェクト指向の運用能力に影響を与えることなく、同一または類似のオブジェクトが大量に存在しないようにするには、どうすればよいのでしょうか?

ヘドニック・パターンは、この問題を解決するために作られたもので、同一または類似のオブジェクトの再利用を可能にする共有技法です。

ヘドニック・パターンでは、共有インスタンスが保存される場所をヘドニック・プールと呼びます。 以下の図に示すように、異なるキャラクターごとにヘドニック・オブジェクトを作成し、それをヘドニック・プールに置き、必要なときに取り出すことができます:

内部状態と外部状態

ヘドニック・パターンは、多数のきめ細かなオブジェクトの再利用を効率的にサポートし、その共有の鍵となるのが内部状態と外部状態の区別です。

  • 内部状態:快楽的なオブジェクトの内部に保存され、環境によって変化することはありません。内部状態は共有することができます。

定義

ヘドニック・パターン:共有技術を利用して、多数の細かいオブジェクトの再利用を効率的にサポートします。

このシステムでは、すべて類似した少数のオブジェクトしか使用せず、状態の変化も最小限であるため、オブジェクトの複数回の再利用が可能です。ヘドニック・パターンは、共有できるオブジェクトが細かいオブジェクトでなければならないため、軽量パターン、オブジェクト構造化パターンとも呼ばれます。

構造

ヘドニックパターンは一般的にファクトリーパターンと併用され、以下のような構造になっています:

ロール

  • フライウェイト:通常インターフェースか抽象クラスで、抽象ヘドニッククラスが具体ヘドニッククラスに公開されるメソッドを宣言し、これらのメソッドを通じてヘドニックオブジェクトの内部データを外部に提供したり、外部データを設定したりすることができます。
  • ConcreteFlyweight:抽象共有クラスを実装/継承し、インスタンスは共有オブジェクトと呼ばれ、具体的なヒューリスティック・クラスの内部状態のためのストレージを提供します。
  • UnsharedConcreteFlyweightすべての抽象ヘドニック・サブクラスが共有される必要はありません。共有できないサブクラスは、非具体ヘドニック・オブジェクトが必要なときに直接インスタンス化できる、非共有具象ヘドニック・クラスとして設計できます。
  • FlyweightFactory: FlyweightFactoryクラスは、ヘゴン・オブジェクトの作成と管理、抽象ヘゴン・クラスのプログラム、ヘゴン・プールへの具象ヘゴン・オブジェクトの格納に使用されます。一般的には、キーと値のペアのコレクション(JavaのHashMapなど)をヘドニックプールとして使用し、クライアントがヘドニックオブジェクトを取得すると、まずそれが存在するかどうかを判断し、存在する場合はコレクションから取り出して返し、存在しない場合は具体的なヘドニックの新しいインスタンスを作成してヘドニックプールに格納し、新しいインスタンスに返します

典型的な実装

ステップ

  • 抽象ヘドニッククラスの定義:抽象ヘドニッククラスをインターフェースまたは抽象クラスとして定義し、ビジネスメソッドを宣言します。
  • 具体的なヘドニックスクラスの定義:抽象的なヘドニックスを継承または実装し、そこに含まれるビジネスメソッドを実装する一方、各具体的なヘドニックスクラスがユニークなヘドニックスオブジェクトを提供することを保証するためにシングルトンパターンの設計を使用します。
  • 非共有具象ヘドニッククラスの定義:シングルトンパターンを使って設計されていない抽象ヘドニッククラスを継承または実装し、クライアントがそれを取得するたびに新しいインスタンスを返します。
  • ヘドニック・ファクトリー・クラスの定義:通常、キーと値のペアのコレクションがヘドニック・プールとして使用され、キー値に基づいて対応する具象ヘドニック・オブジェクトまたは非共有具象ヘドニック・オブジェクトを返します。

抽象ヘドニッククラス

ここではインターフェイスの実装が使用され、オペレーションのビジネスメソッドが含まれています:

interface Flyweight
{
 void operation(String extrinsicState);
}

特定のヘドニッククラス

単一のインスタンスを列挙するための2つの具体的なヘドニッククラスのシンプルな設計:

enum ConcreteFlyweight1 implements Flyweight
{
 INSTANCE("INTRINSIC STATE 1");
 private String intrinsicState;
 private ConcreteFlyweight1(String intrinsicState)
 {
 this.intrinsicState = intrinsicState;
 }
 @Override
 public void operation(String extrinsicState)
 {
 System.out.println("特定のヘドニック操作");
 System.out.println("内部状態:"+intrinsicState);
 System.out.println("外部状態:"+extrinsicState);
 }
}
enum ConcreteFlyweight2 implements Flyweight
{
 INSTANCE("INTRINSIC STATE 2");
 private String intrinsicState;
 private ConcreteFlyweight2(String intrinsicState)
 {
 this.intrinsicState = intrinsicState;
 }
 @Override
 public void operation(String extrinsicState)
 {
 System.out.println("特定のヘドニック操作");
 System.out.println("内部状態:"+intrinsicState);
 System.out.println("外部状態:"+extrinsicState);
 }
}

非共有具体的ヒューリスティック・メタクラス

列挙されたシングルトン・クラスではない、2つの単純な非共有具象ヒューリスティック・クラス:

class UnsharedConcreteFlyweight1 implements Flyweight
{
 @Override
 public void operation(String extrinsicState)
 {
 System.out.println("非共有具体的ヘドニック操作");
 System.out.println("外部状態:"+extrinsicState);
 }
}
class UnsharedConcreteFlyweight2 implements Flyweight
{
 @Override
 public void operation(String extrinsicState)
 {
 System.out.println("非共有具体的ヘドニック操作");
 System.out.println("外部状態:"+extrinsicState);
 }
}

ヘドニックファクトリークラス

クライアントとファクトリーがコンクリートヘドンと非共有コンクリートヘドンを管理しやすくするために、ヘドンプールのキーとして2つの列挙クラスが作成されます:

enum Key { KEY1,KEY2 }
enum UnsharedKey { KEY1,KEY2 }

このファクトリークラスは列挙型シングルトンを使用しています:

enum Factory
{
 INSTANCE;
 private Map<Key,Flyweight> map = new HashMap<>();
 public Flyweight get(Key key)
 {
 if(map.containsKey(key))
 return map.get(key);
 switch(key)
 {
 case KEY1: 
 map.put(key, ConcreteFlyweight1.INSTANCE);
 return ConcreteFlyweight1.INSTANCE;
 case KEY2:
 map.put(key, ConcreteFlyweight2.INSTANCE);
 return ConcreteFlyweight2.INSTANCE;
 default:
 return null;
 }
 }
 public Flyweight get(UnsharedKey key)
 {
 switch(key)
 {
 case KEY1:
 return new UnsharedConcreteFlyweight1();
 case KEY2:
 return new UnsharedConcreteFlyweight2();
 default:
 return null;
 }
 }
}

HashMap<String,Flyweight>HENTAIプールとして使用:

  • 具体的なヘドニッククラスに対して、キーの値によってヘドニックプールに具体的なヘドニックオブジェクトがあるかどうかを判断し、あればそれを直接返し、なければ具体的なヘドニックシングルトンをヘドニックプールに入れ、シングルトンを返します。
  • 非共有具象ヒューリスティック・クラスの場合、それらは「非共有」なので、ヒューリスティック・プールにインスタンス・オブジェクトを保存する必要はなく、各呼び出しは直接新しいインスタンスを返します。

クライアント

クライアントは、まずhengesファクトリー・シングルトンを取得し、ファクトリー・メソッドを使用して対応する列挙パラメーターを渡し、対応する具象hengesまたは非共有具象hengesを取得することで、抽象hengesをプログラムします:

public static void main(String[] args) 
{
 Factory factory = Factory.INSTANCE;
 Flyweight flyweight1 = factory.get(Key.KEY1);
 Flyweight flyweight2 = factory.get(Key.KEY1);
 System.out.println(flyweight1 == flyweight2);
 flyweight1 = factory.get(UnsharedKey.KEY1);
 flyweight2 = factory.get(UnsharedKey.KEY1);
 System.out.println(flyweight1 == flyweight2);
}

リフレクションの簡略化

具体的なヘドニックオブジェクトの数が多くなると、ファクトリークラスのget()のスイッチが非常に長くなってしまうので、ファクトリークラスだけでなく、キーバリュークラスのget()も、上記の具体的なヘドニッククラスを2つ増やすなどして、コードを簡略化するように改善します:

enum ConcreteFlyweight3 implements Flyweight {...}
enum ConcreteFlyweight4 implements Flyweight {...}

この方法では、ファクトリークラスのスイッチは2つのキーを追加する必要があります:

switch(key)
{
 case KEY1: 
 map.put(key, ConcreteFlyweight1.INSTANCE);
 return ConcreteFlyweight1.INSTANCE;
 case KEY2:
 map.put(key, ConcreteFlyweight2.INSTANCE);
 return ConcreteFlyweight2.INSTANCE;
 case KEY3:
 map.put(key, ConcreteFlyweight3.INSTANCE);
 return ConcreteFlyweight3.INSTANCE;
 case KEY4:
 map.put(key, ConcreteFlyweight4.INSTANCE);
 return ConcreteFlyweight4.INSTANCE;
 default:
 return null;
}

これは、具体的なヒューリスティック・クラスの命名スキームを使用することで簡略化できます。つまり、リフレクションを使ってクラスを取得すれば、シングルトン・オブジェクトを直接取得できます:

public Flyweight get(Key key)
{
 if(map.containsKey(key))
 return map.get(key);
 try
 {
 Class<?> cls = Class.forName("ConcreteFlyweight"+key.code());
 Flyweight flyweight = (Flyweight)(cls.getField("INSTANCE").get(null));
 map.put(key,flyweight);
 return flyweight;
 }
 catch(Exception e)
 {
 e.printStackTrace();
 return null;
 }
}

ここでKeyクラスを修正する必要があります:

enum Key
{
 KEY1(1),KEY2(2),KEY3(3),KEY4(4);
 private int code;
 private Key(int code)
 {
 this.code = code;
 }
 public int code()
 {
 return code;
 }
}

コードフィールドをフラグとして追加し、特定のヘンデケーターごとに区別できるようにします。

非共有コンクリートヘンジの場合も同様に、まず UnsharedKey を変更し、同様に code フィールドを追加します:

enum UnsharedKey
{
 KEY1(1),KEY2(2),KEY3(3),KEY4(4);
 private int code;
 private UnsharedKey(int code)
 {
 this.code = code;
 }
 public int code()
 {
 return code;
 }
}

次にgetメソッドを修正します:

public Flyweight get(UnsharedKey key)
{
 try
 {
 Class<?> cls = Class.forName("UnsharedConcreteFlyweight"+key.code());
 return (Flyweight)(cls.newInstance());
 }
 catch(Exception e)
 {
 e.printStackTrace();
 return null;
 }
}

作者はOpenJDK11を使用しているため、newInstanceはobsoleteとマークされています:

そのため、newInstance() を直接使用する代わりに、以下を使用します:

return (Flyweight)(cls.getDeclaredConstructor().newInstance());

違いは以下の通り:

  • newInstance: インスツルメンテッドでないコンストラクタ・メソッドの直接呼び出し
  • getDeclaredConstructor().newInstance()getDeclaredConstructor()パラメータがない場合は、クラスのパラメータ化されていないコンストラクタ・メソッドを返し、newInstance を呼び出してインスタンス化します。

インスタンス

囲碁の駒のデザイン: 白と黒の同じ駒が多数配置された碁盤を、駒のヘドニックパターンを使ってデザインします。

デザインは以下の通り:

  • 抽象的なヒューリスティック・クラス: IgoChessman インターフェース。
  • 特定の快楽的なクラス:BlackChessman+WhiteChessman、列挙されたシングルトン・クラス
  • 非共有特定の快楽カテゴリー:なし
  • ヘドニック・ファクトリー・クラス:ファクトリー、列挙型シングルトン・クラス、具体的なヘドニックを取得するメソッドとして単純なgetを含み、さらにコンストラクタ・メソッドでヘドニック・プールを初期化する白と黒の単純なカプセル化を含みます。

コードは以下の通り:

//抽象ヘドニック・インターフェイス
interface IgoChessman
{
 Color getColor();
 void display();
}
//特定のヘドニック列挙シングルトンクラス
enum BlackChessman implements IgoChessman
{
 INSTANCE;
 
 @Override
 public Color getColor()
 {
 return Color.BLACK;
 }
 @Override
 public void display()
 {
 System.out.println("ピースの色 "+getColor().color());
 }
}
//特定のヘドニック列挙シングルトンクラス
enum WhiteChessman implements IgoChessman
{
 INSTANCE;
 
 @Override
 public Color getColor()
 {
 return Color.WHITE;
 }
 @Override
 public void display()
 {
 System.out.println("ピースの色 "+getColor().color());
 }
}
//ヘドニックファクトリー列挙シングルトンクラス
enum Factory
{
 INSTANCE;
 //HashMap<Color,IgoChessman>ヘドニック・プールとして
 private Map<Color,IgoChessman> map = new HashMap<>();
 private Factory()
 {
 	//コンストラクタでプールを直接初期化する
 map.put(Color.WHITE, WhiteChessman.INSTANCE);
 map.put(Color.BLACK, BlackChessman.INSTANCE);
 }
 public IgoChessman get(Color color)
 {
 	//コンストラクタで初期化されるので、存在しない場合はnullを返すか、新しいインスタンスをプールに追加して返す。
 if(!map.containsKey(color))
 return null;
 return (IgoChessman)map.get(color);
 }
 //単純なカプセル化
 public IgoChessman white()
 {
 return get(Color.WHITE);
 }
 public IgoChessman black()
 {
 return get(Color.BLACK);
 }
}
enum Color
{
 WHITE("白"),黒;
 private String color;
 private Color(String color)
 {
 this.color = color;
 }
 public String color()
 {
 return color;
 }
}

HENTAIプールを初期化する際、特定のHENTAIクラスが多すぎる場合は、リフレクションを使用することで、手作業で1つずつ配置することなく処理を簡略化することができます:

private Factory()
{
	map.put(Color.WHITE, WhiteChessman.INSTANCE);
	map.put(Color.BLACK, BlackChessman.INSTANCE);
}

列挙された値の配列に基づいて、ListとforEachを組み合わせ、配列内の値を1つずつ使用して対応するクラスを取得し、インスタンスを取得します:

private Factory()
{
 List.of(Color.values()).forEach(t->
 {
 String className = t.name().substring(0,1)+t.name().substring(1).toLowerCase()+"Chessman";
 try
 {
 map.put(t,(IgoChessman)(Class.forName(className).getField("INSTANCE").get(null)));
 }
 catch(Exception e)
 {
 e.printStackTrace();
 map.put(t,null);
 } 
 });
}

テスト:

public static void main(String[] args) 
{
 Factory factory = Factory.INSTANCE;
 IgoChessman white1 = factory.white();
 IgoChessman white2 = factory.white();
 white1.display();
 white2.display();
 System.out.println(white1 == white2);
 IgoChessman black1 = factory.black();
 IgoChessman black2 = factory.black();
 black1.display();
 black2.display();
 System.out.println(black1 == black2);
}

外部状態の追加

黒と白の駒の共有は上記の方法ですでに可能ですが、まだ解決されていない問題があります。

解決策はそれほど難しくありません。座標クラスCoordinatesを追加し、displayが呼び出されたときに配置される座標のパラメータとして関数に渡します。

まず座標クラスを追加します:

class Coordinates
{
 private int x;
 private int y; 
 public Coordinates(int x,int y)
 {
 this.x = x;
 this.y = y;
 }
	//setter+getter...
}

次に、抽象的なヘドニックインターフェースを変更する必要があります:

interface IgoChessman
{
 Color getColor();
 void display(Coordinates coordinates);
}

それなら、特定の快楽的なクラスを修正すればいいのです:

enum BlackChessman implements IgoChessman
{
 INSTANCE;
 
 @Override
 public Color getColor()
 {
 return Color.BLACK;
 }
 @Override
 public void display(Coordinates coordinates)
 {
 System.out.println("ピースの色 "+getColor().color());
 System.out.println("座標を表示する:");
 System.out.println(""水平座標"+coordinates.getX());
 System.out.println(""垂直座標"+coordinates.getY());
 }
}

クライアント側では、hedonicオブジェクトを作成するコードを変更する必要はなく、Coordinatesパラメータを渡してdisplayを呼び出すところだけを変更します:

IgoChessman white1 = factory.white();
IgoChessman white2 = factory.white();
white1.display(new Coordinates(1, 2));
white2.display(new Coordinates(2, 3));

単純ヘドニック・パターンと複合ヘドニック・パターン

単純なヘドニックパターン

標準的なヘドニック・パターンは、具象ヘドニック・クラスと非共有具象ヘドニック・クラスの両方を含むことができます。

しかし、単純快楽主義パターンでは、すべての具体的快楽主義クラスは共有され、つまり共有されない具体的快楽主義クラスは存在しません。

例えば、上記のチェスの駒の例では、黒と白の駒は具体的なヒューリスティック・クラスとして共有されており、共有されていない具体的なヒューリスティック・クラスは存在しません。

複合ヘドニック・パターン

単純な快楽的対象は、組み合わせパターンを使って組み合わせることができ、複合快楽的対象を形成することができます

複合ヘドニック・パターンは、複合ヘドニック・クラスに含まれる各単純ヘドニック・クラスが同じ外部状態を持つことを保証しますが、これらの単純ヘドニック要素の内部状態は、例えば上記のチェスの駒の例では異なることがあります

  • 黒い円盤は単にヘンウォン。
  • 白いディスクもシンプルな恒元
  • この2つの単純な快楽要素の内部状態は異なります。
  • しかし、同じ外部状態を

まず、抽象ヒューリスティックにint引数で表示を追加します:

interface IgoChessman
{
 Color getColor();
 void display(int size);
}

特定のヘンゲンで実施すれば十分です:

enum BlackChessman implements IgoChessman
{
 INSTANCE;
 
 @Override
 public Color getColor()
 {
 return Color.BLACK;
 }
 @Override
 public void display(int size)
 {
 System.out.println("ピースの色 "+getColor().color());
 System.out.println(""ピースサイズ"+size);
 }
}

次に、すべての具象ヘドンを格納するHashMapを含む複合ヘドンクラスを追加します:

enum Chessmans implements IgoChessman
{
 INSTANCE;
 private Map<Color,IgoChessman> map = new HashMap<>();
 public void add(IgoChessman chessman)
 {
 map.put(chessman.getColor(),chessman);
 }
 @Override
 public Color getColor()
 {
 return null;
 }
 @Override
 public void display(int size)
 {
 map.forEach((k,v)->v.display(size));
 }
}

表示では、HashMapは実際にトラバースされ、それぞれのヒューリスティックの表示に同じパラメータが渡されます。テスト

public static void main(String[] args) {
 Factory factory = Factory.INSTANCE;
 IgoChessman white = factory.white();
 IgoChessman black = factory.black();
 Chessmans chessmans = Chessmans.INSTANCE;
 chessmans.add(white);
 chessmans.add(black);
 chessmans.display(30);
}

出力:

このようにして、異なる内部状態を持つ2つの具体的な快楽的クラスは、複合快楽的クラスによって同じ外部状態を持つように設定されます。

補足

  • 他のパターンとの併用: ヘドニックパターンは通常、ファクトリーパターン、シングルトンパターン、コンビナトリアルパターンなど、他のパターンと併用する必要があります。
  • JDKのヘドニックパターン:JDKの文字列は、ヘドニックパターンを使用しています。我々は、すべてのStringは不変クラスであることを知って、同様のString a = "123 "この宣言のために、"123 "ヘドニックオブジェクトの値を作成します、あなたが取得するヘドニックプールから "123 "を使用する次の時間は、ヘドニックオブジェクトの変更では、例えば、a + = "1"、元のオブジェクトは、最初のオブジェクトのコピーをコピーし、新しいオブジェクトを変更し、このメカニズムは、"コピーオン "と呼ばれています。このメカニズムは "Copy On Write "と呼ばれています。このメカニズムは "Copy On Write "と呼ばれています。 基本的な考え方は、誰もが最初にコンテンツを共有し、誰かがそれを修正する必要があるときに、それをコピーして新しいコンテンツを形成し、それを修正するというものです。

主な利点

  • メモリ消費量の削減:ヘドニックパターンは、メモリ内のオブジェクト数を大幅に削減できるため、同一または類似のオブジェクトのコピーが1つだけメモリに保持され、システムリソースを節約し、システムパフォーマンスを提供します。
  • 外的状態の独立性:快楽的パターンの外的状態は比較的独立しており、内的状態に影響を与えないため、快楽的対象を異なる環境間で共有することが可能。

主な欠点

  • 複雑性の増大:ヘドニック・モデルは、内部状態と外部状態を分離することでシステムを複雑にし、プログラムの論理を複雑にします。
  • 長いランタイム:オブジェクトを共有可能にするために、ヘドニックパターンはヘドニックオブジェクトの状態の一部を外部に出す必要があり、外部の状態を読み出すことでランタイムが長くなります。

シナリオ

  • 類似または同一のオブジェクトが多数存在するシステムでは、多くのメモリが浪費されます。
  • オブジェクトの状態のほとんどは外部化することができ、この外部状態をオブジェクトに渡すことができます。
  • ヘンジのプールを維持することによるリソースのオーバーヘッドのため、ヘンジ・オブジェクトを本当に何度も再利用する必要がある場合にのみ、ヘドニック・パターンを使う価値があります。

Read next

CDNアーキテクチャ

CDNの概要\n詳細情報については、-dn/をご覧ください。\nCDNとは?\n\nCDNはウェブホスティングと同じですか?\nCDNはコンテンツをホスティングするものではなく、適切なウェブホスティングの必要性を代替するものではありませんが、ウェブのエッジでコンテンツをキャッシュするのに役立ちます。

Aug 18, 2020 · 4 min read