【Java】Chain of Responsibility パターン【デザインパターン】
目次
- Chain of Responsibilityパターンについて
- Chain of Responsibilityパターンとは
- Chain of Responsibilityパターンのメリット
- Chain of Responsibilityパターンのデメリット
- 今日のポイント
Chain of Responsibilityパターンについて
Chain of Responsibilityパターンとは
- 複数のオブジェクトを鎖で繋いでおき、そのオブジェクトの鎖を順次渡り歩いて目的のオブジェクトを決定するパターンです。
- Chain of Responsibilityは、責任の連鎖という意味で、"たらい回し構造" というイメージです
- ある要求が発生した時に、その要求を処理するオブジェクトをダイレクトに決定できない時に、委譲によって、"たらい回し"にします
- こうすることで、要求を行う側と処理を行う側の結合度を下げ、部品として独立性を高めることができます
サンプルプログラム
- Chain of Responsibilityパターンを使って、発生したトラブルを、どこかのサポートセンターが処理する例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Trouble | 発生したトラブルを表すクラス |
Support (Handler) | トラブルを解決する抽象クラス |
NoSupport (ConcreteHandler) | トラブルを解決する具象クラス 常に処理しない |
LimitSupport (ConcreteHandler) | トラブルを解決する具象クラス 指定した番号未満のトラブルを解決できる |
OddSupport(ConcreteHandler) | トラブルを解決する具象クラス 奇数番号のトラブルを解決できる |
SpecialSupport (ConcreteHandler) | トラブルを解決する具象クラス 特定番号のトラブルを解決できる |
Main (Client) | Supportの連鎖を作り、トラブルを発生させる動作確認用のクラス |
Troubleクラス
- numberはトラブル番号で、getNumberでトラブル番号を得ます
public class Trouble { private int number; // トラブル番号 public Trouble(int number) { // トラブルの生成 this.number = number; } public int getNumber() { // トラブル番号を得る return number; } public String toString() { // トラブルの文字列表現 return "[Trouble " + number + "]"; } }
Supportクラス
- nextフィールドはたらい回しの先を表し、setNextメソッドで次のたらい回し先を決定します
- resolveメソッドはサブクラスで実装し、trueなら要求が処理され、falseは処理されなかったことを表します
- supportメソッドでは、resolveを呼びだし、戻り値がfalseなら次の人にたらい回しにします
- もし次のたらい回し先がなかった場合、自分が最後の連鎖となり、処理できなかったことを表示します
- supportメソッドは抽象メソッドresolveを使ったTemplate Methodパターンになっています
public abstract class Support { private String name; // このトラブル解決者の名前 private Support next; // たらい回しの先 public Support(String name) { // トラブル解決者の生成 this.name = name; } public Support setNext(Support next) { // たらい回しの先を設定 this.next = next; return next; } public void support(Trouble trouble) { // トラブル解決の手順 if (resolve(trouble)) { done(trouble); } else if (next != null) { next.support(trouble); } else { fail(trouble); } } public String toString() { // 文字列表現 return "[" + name + "]"; } protected abstract boolean resolve(Trouble trouble); // 解決用メソッド protected void done(Trouble trouble) { // 解決 System.out.println(trouble + " is resolved by " + this + "."); } protected void fail(Trouble trouble) { // 未解決 System.out.println(trouble + " cannot be resolved."); } }
NoSupportクラス
- Supportクラスのサブクラスで、resolveメソッドを実装します
- resolveメソッドでは、常にfalseを返します
public class NoSupport extends Support { public NoSupport(String name) { super(name); } protected boolean resolve(Trouble trouble) { // 解決用メソッド return false; // 自分は何も処理しない } }
LimitSupportクラス
- Supportクラスのサブクラスで、resolveメソッドを実装します
- resolveメソッドでは、limitで指定した番号未満のトラブルを処理します
public class LimitSupport extends Support { private int limit; // この番号未満なら解決できる public LimitSupport(String name, int limit) { // コンストラクタ super(name); this.limit = limit; } protected boolean resolve(Trouble trouble) { // 解決用メソッド if (trouble.getNumber() < limit) { return true; } else { return false; } } }
OddSupportクラス
- resolveメソッドでは、奇数番号のトラブルを処理します
public class OddSupport extends Support { public OddSupport(String name) { // コンストラクタ super(name); } protected boolean resolve(Trouble trouble) { // 解決用メソッド if (trouble.getNumber() % 2 == 1) { return true; } else { return false; } } }
SpecialSupportクラス
- resolveメソッドでは、指定番号のトラブルのみ処理します
public class SpecialSupport extends Support { private int number; // ここで指定した番号だけ解決できる public SpecialSupport(String name, int number) { // コンストラクタ super(name); this.number = number; } protected boolean resolve(Trouble trouble) { // 解決用メソッド if (trouble.getNumber() == number) { return true; } else { return false; } } }
Mainクラスで動作確認
- Supportクラス型の変数である"解決者"に代入されたインスタンスで、解決者がトラブルを処理します
- 例えば、AliceはNoSupportのインスタンスが割り当てられているので、何も解決しません
- setNextメソッドで解決者を一列に並べ、トラブルを一個ずつ作成します
- はじめに"alice"に渡し、誰が問題を解決したか表示します
public class Main { public static void main(String[] args) { Support alice = new NoSupport("Alice"); Support bob = new LimitSupport("Bob", 100); Support charlie = new SpecialSupport("Charlie", 429); Support diana = new LimitSupport("Diana", 200); Support elmo = new OddSupport("Elmo"); Support fred = new LimitSupport("Fred", 300); // 連鎖の形成 alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo).setNext(fred); // さまざまなトラブル発生 for (int i = 0; i < 500; i += 33) { //33ずつ番号を増加 alice.support(new Trouble(i)); } } }
- 実行結果
[Trouble 0] is resolved by [Bob]. [Trouble 33] is resolved by [Bob]. [Trouble 66] is resolved by [Bob]. [Trouble 99] is resolved by [Bob]. [Trouble 132] is resolved by [Diana]. [Trouble 165] is resolved by [Diana]. [Trouble 198] is resolved by [Diana]. [Trouble 231] is resolved by [Elmo]. [Trouble 264] is resolved by [Fred]. [Trouble 297] is resolved by [Elmo]. [Trouble 330] cannot be resolved. [Trouble 363] is resolved by [Elmo]. [Trouble 396] cannot be resolved. [Trouble 429] is resolved by [Charlie]. [Trouble 462] cannot be resolved. [Trouble 495] is resolved by [Elmo].
Chain of Responsibilityパターンのメリット
- Chain of Responsibilityパターンのポイントは、"要求を出す人(Main)" と "要求を処理する人(Handler)" を緩やかに結びつけることです
- "要求を出す人(Main)"が、最初の人に要求を出すと、連鎖の中をその要求が流れていき、適切な人が要求を処理します
- Chain of Responsibilityパターンを使わないと、"この要求はこの人が処理する" という知識を定義する必要があります
- その知識を"要求を出す人(Main)"に持たせてしまうと、部品としての独立性が損なわれてしまいます
- もしくは、ConcreteHandlerが仕事の振り分け条件を定義する必要があります
- 委譲によって、たらい回しをすることで、状況に合わせて ConcreteHandlerの順番を変更することができます
- もし、トラブルと解決者が固定されていたら、プログラム実行中に、動的に解決者を変更することができません
- たらい回しにすることで、各オブジェクトが自分の処理に集中できます
Chain of Responsibilityパターンのデメリット
- Chain of Responsibilityパターンは柔軟性が高くなりますが、たらい回しにする分、処理が遅くなり易いです
今日のポイント
- 複数のオブジェクトを鎖で繋いでおき、そのオブジェクトの鎖を順次渡り歩いて目的のオブジェクトを決定するパターンです。
- ある要求が発生した時に、その要求を処理するオブジェクトをダイレクトに決定できない時に、委譲によって、"たらい回し"にします
- こうすることで、要求を行う側と処理を行う側の結合度を下げ、部品として独立性を高めることができます
- たらい回しにすることで、各オブジェクトが自分の処理に集中できます
本日もお疲れ様です😊
【Java】Visitorパターン【デザインパターン】
目次
Visitorパターンについて
Visitorパターンとは
- Visitorパターンは、データ構造と処理を分離するパターンです
- データ構造の中にアクセスする"訪問者クラス"を用意し、訪問者クラスに処理を任せます
- 新しい処理を追加したいときは新しい訪問者を作り、データ構造の方は、訪問者をアクセスさせたら良いことになります
サンプルプログラム
- Visitorパターンを使って、ディレクトリ、ファイルの一覧を表示する例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Visitor | 訪問者を表す抽象クラス |
Element | Visitorクラスのインスタンス(訪問者)を受け入れるデータ構造を表すインターフェース |
ListVisitor (ConcreteVisitor) | Visitorクラスのサブクラスで、ファイル、ディレクトリの一覧を表示するクラス |
Entry | FileとDirectoryのスーパークラスとなる抽象クラス |
File (ConcreteElement) | ファイルを表すクラス |
Directory ( ConcreteElement / ObjectStructure ) | ディレクトリを表すクラス |
Visitorクラス
- 訪問者を表す抽象クラスです
- 訪問者は訪問先のデータ構造である、FileとDirectoryに依存しています
- 2つのvisitメソッドは、引数の型によって、メソッドの識別を行います(オーバーロード)
public abstract class Visitor { public abstract void visit(File file); public abstract void visit(Directory directory); }
Elementインターフェース
- 訪問者を受け入れるインターフェースです
public interface Element { public abstract void accept(Visitor v); }
Entryクラス
- Elementインターフェースを実装します
- acceptメソッドを実際に実装するのはEntryのサブクラス(File、Directory)クラスです
import java.util.Iterator; public abstract class Entry implements Element { public abstract String getName(); // 名前を得る public abstract int getSize(); // サイズを得る public Entry add(Entry entry) throws FileTreatmentException { // エントリを追加する throw new FileTreatmentException(); } public Iterator iterator() throws FileTreatmentException { // Iteratorの生成 throw new FileTreatmentException(); } public String toString() { // 文字列表現 return getName() + " (" + getSize() + ")"; } }
Fileクラス
- acceptメソッドを実装します
v.visit(this);
で Visitorのvisitメソッドを呼び出します
public class File extends Entry { private String name; private int size; public File(String name, int size) { this.name = name; this.size = size; } public String getName() { return name; } public int getSize() { return size; } public void accept(Visitor v) { v.visit(this); } }
Directoryクラス
v.visit(this);
で Visitorのvisitメソッドを呼び出します
import java.util.Iterator; import java.util.ArrayList; public class Directory extends Entry { private String name; // ディレクトリの名前 private ArrayList dir = new ArrayList(); // ディレクトリエントリの集合 public Directory(String name) { // コンストラクタ this.name = name; } public String getName() { return name; } public int getSize() { int size = 0; Iterator it = dir.iterator(); while (it.hasNext()) { Entry entry = (Entry)it.next(); size += entry.getSize(); } return size; } public Entry add(Entry entry) { // エントリの追加 dir.add(entry); return this; } public Iterator iterator() { // Iteratorの生成 return dir.iterator(); } public void accept(Visitor v) { // 訪問者の受け入れ v.visit(this); } }
ListVisitorクラス
- Visitorのサブクラスで、データ構造の中を歩き、一覧を表示するクラスです
- acceptメソッドはvisitメソッドを呼び、visitメソッドはacceptメソッドを呼びます
- visitメソッドとacceptメソッドが互いに相手を呼び出しているということです
import java.util.Iterator; public class ListVisitor extends Visitor { private String currentdir = ""; // 現在のディレクトリ名 public void visit(File file) { // Fileクラスのインスタンスに行う処理 System.out.println(currentdir + "/" + file); } public void visit(Directory directory) { // Directoryクラスのインスタンスに行う処理 System.out.println(currentdir + "/" + directory); String savedir = currentdir; currentdir = currentdir + "/" + directory.getName(); Iterator it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry)it.next(); entry.accept(this); } currentdir = savedir; } }
FileTreatmentException クラス
public class FileTreatmentException extends RuntimeException { public FileTreatmentException() { } public FileTreatmentException(String msg) { super(msg); } }
Mainクラスで動作確認
- Directoryの中身を表示するのに、"表示を行う訪問者"である、ListVisitorクラスのインスタンスを使います
- ディレクトリの表示も、データ構造内の各要素に対して行う処理であるので、Visitor側で実装しています
public class Main { public static void main(String[] args) { try { System.out.println("Making root entries..."); Directory rootdir = new Directory("root"); Directory bindir = new Directory("bin"); Directory tmpdir = new Directory("tmp"); Directory usrdir = new Directory("usr"); rootdir.add(bindir); rootdir.add(tmpdir); rootdir.add(usrdir); bindir.add(new File("vi", 10000)); bindir.add(new File("latex", 20000)); rootdir.accept(new ListVisitor()); System.out.println(""); System.out.println("Making user entries..."); Directory kiki = new Directory("kiki"); Directory nausicaa = new Directory("nausicaa"); Directory chihiro = new Directory("chihiro"); usrdir.add(kiki); usrdir.add(nausicaa); usrdir.add(chihiro); kiki.add(new File("diary.html", 100)); kiki.add(new File("Composite.java", 200)); nausicaa.add(new File("memo.tex", 300)); chihiro.add(new File("game.doc", 400)); chihiro.add(new File("junk.mail", 500)); rootdir.accept(new ListVisitor()); } catch (FileTreatmentException e) { e.printStackTrace(); } } }
Visitor/Elementの処理のまとめ
- Directory・Fileクラスのインスタンスに対しては、acceptメソッドが呼ばれます
- acceptメソッドはインスタンスごとに一回しか呼ばれません
- ListVisitorメソッドのインスタンスに対して、visit(Directory)・visit(File)が呼ばれます
- visit(Directory)やvisit(File)を処理しているのは、1つのListVisitorインスタンスです
- →ListVisitorにvisitの処理が集中しています
- Visitor パターンでは、受入者 ( ConcreteAcceptor / ConcreteElement ) のacceptメソッドを呼び出すと、 訪問者 (ConcreteVisitor) のvisitメソッド が呼び出され、実行する処理が決定します。このような "2重呼び出し"を ダブルディスパッチ (2 重振り分け) と呼びます
Visitorパターンのメリット
- Visitorパターンの目的は、データ構造と処理を分離することです
- データ構造は、要素を集合としてまとめたり、要素間を繋いだりしてくれます
- そのデータ構造を保持しておくことと、その構造を基礎とした処理を書くことは別のものです。
- 訪問者役 (ListVisitor) は、受け入れ役 (File、Directory) とは独立して開発することができます
- つまり、Visitorパターンは、受け入れ役のクラスの部品としての独立性を高めていることになります
- 処理の内容をFileクラスやDirectoryクラスのメソッドとして実装してしまうと、新しい処理を追加して機能拡張するたびにFileクラスやDirectoryクラスを修正する必要があります
今日のポイント
- Visitorパターンは、データ構造と処理を分離するパターンです
- データ構造の中にアクセスする訪問者クラスを用意し、訪問者クラスに処理を任せます
- Visitorパターンの目的は、データ構造と処理を分離することです
- 訪問者役 (ListVisitor) は、受け入れ役 (File、Directory) とは独立して開発することができ、受け入れ役のクラスの部品としての独立性を高めていることになります
- 部品としての独立性を高いということは、The Open-Closed Principle の原則に則っています
- "ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張のために開いていて、修正のために閉じていなければならない"
- つまり、変更が発生した場合は、"既存のコードには修正を加えずに、新しくコードを追加するだけで対応しましょう"
- 部品としての独立性を高いということは、The Open-Closed Principle の原則に則っています
本日もお疲れ様です😊
【Java】Decoratorパターン【デザインパターン】
目次
Decoratorパターンについて
Decoratorパターンとは
- Decoratorパターンは、オブジェクトに対してデコレーション (飾り付け) を行うパターンです
- スポンジに苺、チョコ、生クリーム。。とデコレーションするように、オブジェクトも機能を一つ一つ被せてデコレーションしていくイメージです
サンプルプログラム
- Decoratorパターンを使って、文字列の周りに飾り枠をつけて表示する例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Display | 文字列表示用の抽象クラス |
StringDisplay | 1行だけの文字列表示用のクラス |
Border | 飾り枠を表す抽象クラス |
SideBorder | 左右に飾り枠をつけるクラス |
FullBorder | 上下左右に飾り枠をつけるクラス |
Displayクラス
- 複数行の文字列を表示するための抽象クラスです
- showメソッドでは、getRowsメソッドで行数を取得し、getRowTextメソッドで表示する文字列を取得し、forループを使って全行を表示します
- showはgetRows、getRowTextメソッドの抽象メソッドを使ったTemplate Methodパターンになっています
public abstract class Display { public abstract int getColumns(); // 横の文字数を得る public abstract int getRows(); // 縦の行数を得る public abstract String getRowText(int row); // row番目の文字列を得る public void show() { // 全部表示する for (int i = 0; i < getRows(); i++) { System.out.println(getRowText(i)); } } }
StringDisplayクラス
- StringDisplayクラスは、一行の文字列を表示するクラスです
- StringDisplayクラスはDisplayクラスのサブクラスなので、Displayクラスで宣言している抽象メソッドを実装します
- stringフィールドに表示する文字列を保持します
public class StringDisplay extends Display { private String string; // 表示文字列 public StringDisplay(String string) { // 引数で表示文字列を指定 this.string = string; } public int getColumns() { // 文字数 return string.getBytes().length; } public int getRows() { // 行数は1 return 1; } public String getRowText(int row) { // rowが0のときのみstringを返す if (row == 0) { return string; } else { return null; } } }
Borderクラス
- 飾り枠を表すクラスです
- Displayクラスを継承しているので、飾り枠は中身と同じメソッドを持つことになります
- インターフェース的には、飾り枠と中身を同一視できることになります
- "中身"はSrtingDisplayクラスなのか、Borderクラスのサブクラスのインスタンス(飾り枠)かは分かりません
- Borderクラスのサブクラスのインスタンスも、さらにdisplayフィールドを持ちます
public abstract class Border extends Display { protected Display display; // この飾り枠がくるんでいる"中身"を指す protected Border(Display display) { // インスタンス生成時に"中身"を引数で指定 this.display = display; } }
SideBorderクラス
- Borderクラスのサブクラスの一つで、具体的な飾り枠です
- SideBorderクラスはスーパークラスで宣言したメソッドを全て実装しているので、抽象クラスではありません
public class SideBorder extends Border { private char borderChar; // 飾りとなる文字 public SideBorder(Display display, char ch) { // コンストラクタでDisplayと飾り文字を指定 super(display); this.borderChar = ch; } public int getColumns() { // 中身の両側+飾り文字分を加えた return 1 + display.getColumns() + 1; } public int getRows() { // 行数は中身と同じ return display.getRows(); } public String getRowText(int row) { // 中身の指定行の両側+飾り文字 return borderChar + display.getRowText(row) + borderChar; } }
FullBorderクラス
- Borderクラスのサブクラスの一つで、具体的な飾り枠です
public class FullBorder extends Border { public FullBorder(Display display) { super(display); } public int getColumns() { // 中身の両側+左右の飾り文字分を加えた return 1 + display.getColumns() + 1; } public int getRows() { //中身の行数+上下の飾り文字分を加えた return 1 + display.getRows() + 1; } public String getRowText(int row) { // 指定した行の内容 if (row == 0) { // 上端の枠 return "+" + makeLine('-', display.getColumns()) + "+"; } else if (row == display.getRows() + 1) { // 下端の枠 return "+" + makeLine('-', display.getColumns()) + "+"; } else { // それ以外 return "|" + display.getRowText(row - 1) + "|"; } } private String makeLine(char ch, int count) { // 文字chをcount個連続させた文字列を作る StringBuffer buf = new StringBuffer(); for (int i = 0; i < count; i++) { buf.append(ch); } return buf.toString(); } }
Mainクラスで動作確認
public class Main { public static void main(String[] args) { Display b1 = new StringDisplay("Hello, world."); //飾りなし Display b2 = new SideBorder(b1, '#'); //#の文字で左右に飾り付け Display b3 = new FullBorder(b2); //全体の飾り枠をつけた b1.show(); b2.show(); b3.show(); Display b4 = //何重にも重ねて枠をつける new SideBorder( new FullBorder( new FullBorder( new SideBorder( new FullBorder( new StringDisplay("こんにちは。") ), '*' ) ) ), '/' ); b4.show(); } }
Decoratorパターンのメリット
Decoratorパターンでは、飾り枠(Border)も中身(StringDisplay)も共通のインターフェースを持っています。 インターフェースは共通ですが、包めば包むほど機能が追加されていきます。 その際に、包まれる方を修正する必要はありません。 包まれるものを変更することなく、機能の追加を行うことができます。
今日のポイント
- Decoratorパターンは、オブジェクトに対してデコレーション (飾り付け) を行うパターンです
本日もお疲れ様です😊
【Java】Compositeパターン【デザインパターン】
目次
Compositeパターンについて
Compositeパターンとは
- 容器と中身を同一視し、再帰的な構造を作るパターンです
- Compositeは、混合物や複合物という意味です。
サンプルプログラム
- Compositeパターンを使って、ファイルとディレクトリを模式的に表現する例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Entry(Component) | Fileと Directoryを同一視するクラス |
File(Leaf) | ファイルを表すクラス |
Directory(Composite) | ディレクトリを表すクラス |
FileTreatmentException | ファイルにEntryを追加しようとした時に起きる例外クラス |
Entryクラス
- 抽象クラスで、ディレクトリエントリを表現します
- サブクラスで、FileクラスとDiirectoryクラスが作られます
- printListは引数なしとありの2つのメソッドがあり、メソッドをオーバーロード(多重継承)しています
- printList()はpublic、printList(String)はprotectedとして、Entryクラスのサブクラスでのみ使われるようにしています
public abstract class Entry { public abstract String getName(); // 名前を得る public abstract int getSize(); // サイズを得る public Entry add(Entry entry) throws FileTreatmentException { // エントリを追加する throw new FileTreatmentException(); } public void printList() { // 一覧を表示する printList(""); } protected abstract void printList(String prefix); // prefixを前につけて一覧を表示する public String toString() { // 文字列表現 return getName() + " (" + getSize() + ")"; } }
Fileクラス
- Entryクラスで定義していた、抽象クラス(getName,getSize,printList(String))を全て実装します
public class File extends Entry { private String name; private int size; public File(String name, int size) { this.name = name; this.size = size; } public String getName() { return name; } public int getSize() { return size; } protected void printList(String prefix) { System.out.println(prefix + "/" + this); } }
Directoryクラス
- getSizeメソッドの変数sizeにentryのサイズを加えていますが、このentryはFileのインスタンスでも、Directoryクラスのインスタンスでも、良いような使用になっています
- これが、容器と中身を同じものとみなす、ということになります
- entryはEntryのサブクラスのインスタンスなので、getSizeを呼ぶことができます
- Compositeパターンの再帰的構造が、getSizeメソッドの再帰的呼び出しに対応しています (printListも同様)
import java.util.Iterator; import java.util.ArrayList; public class Directory extends Entry { private String name; // ディレクトリの名前 private ArrayList directory = new ArrayList(); // ディレクトリエントリの集合 public Directory(String name) { // コンストラクタ this.name = name; } public String getName() { // 名前を得る return name; } public int getSize() { // サイズを得る int size = 0; Iterator it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry)it.next(); size += entry.getSize(); } return size; } public Entry add(Entry entry) { // エントリの追加 directory.add(entry); return this; } protected void printList(String prefix) { // エントリの一覧 System.out.println(prefix + "/" + this); Iterator it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry)it.next(); entry.printList(prefix + "/" + name); } } }
FileTreatmentExceptionクラス
- ファイルにaddメソッドを呼んでしまったときに投げられる例外です
public class FileTreatmentException extends RuntimeException { // RuntimeExceptionで正しいか? public FileTreatmentException() { } public FileTreatmentException(String msg) { super(msg); } }
Mainクラスで動作確認
- root,bin,tmp,usr ディレクトリを作り、binの下に vi,latex ファイルを作ります
- usrディレクトリのなかに、kiki,nausicaa,chihiro のディレクトリを作り、ファイルを格納します
public class Main { public static void main(String[] args) { try { System.out.println("Making root entries..."); Directory rootdir = new Directory("root"); Directory bindir = new Directory("bin"); Directory tmpdir = new Directory("tmp"); Directory usrdir = new Directory("usr"); rootdir.add(bindir); rootdir.add(tmpdir); rootdir.add(usrdir); bindir.add(new File("vi", 10000)); bindir.add(new File("latex", 20000)); rootdir.printList(); System.out.println(""); System.out.println("Making user entries..."); Directory kiki = new Directory("kiki"); Directory nausicaa = new Directory("nausicaa"); Directory chihiro = new Directory("chihiro"); usrdir.add(kiki); usrdir.add(nausicaa); usrdir.add(chihiro); kiki.add(new File("diary.html", 100)); kiki.add(new File("Composite.java", 200)); nausicaa.add(new File("memo.tex", 300)); chihiro.add(new File("game.doc", 400)); chihiro.add(new File("junk.mail", 500)); rootdir.printList(); } catch (FileTreatmentException e) { e.printStackTrace(); } } }
Compositeパターンのメリット
- 全てのオブジェクト(File・Directory)は、共通の抽象クラス(Entry)を持っているので、クライアントから見て、中身(File・Directory)を意識する必要がなく、一様に扱うことができます
- 新たなクラスを追加した場合でも、基底クラス (Entry) のインターフェースが変わらなければ、 Clientの処理には影響しません
今日のポイント
- Compositeパターンとは、容器と中身を同一視し、再帰的な構造を作るパターンです
- つまり複数個のものを集めて、あたかも一つのものであるかのように取り扱います
- 全ての Leaf、Composite オブジェクトは、共通の抽象クラスを持っているので、 Client から見て、中身を意識する必要がなく、一様に扱うことができます
本日もお疲れ様です😊
【Java】Strategyパターン【デザインパターン】
目次
Strategyパターンについて
Strategyパターンとは
サンプルプログラム
- Strategyパターンを使って、じゃんけんする例を取り上げます。"勝ったら次も同じ手を出す戦略"と、"一回前の手から次の手を確率的に計算する戦略"があります
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Hand | 手を表すクラス |
Strategy | 戦略を表すインターフェース |
WinningStrategy (ConcreteStrategy) | 勝ったら次も同じ手を出す戦略を表すクラス |
ProbStrategy (ConcreteStrategy) | 一回前の手から次の手を確率的に計算する戦略を表すクラス |
Player (Context) | じゃんけんを行うプレイヤー |
Handクラス
- グー・チョキ・パーを0・1・2のintで表現します
- Handクラスではインスタンスは3つしか作られません (Singletonパターンの一種)
(this.handvalue + 1) % 3 == h.handvalue
では、じゃんけんに勝ったかどうかを判断しています- thisの値に1を加えたものがhの手の値(thisがグーならhがチョキ、thisがチョキならhはパー)になっていたら、trueです
- %3で剰余を取っているのは、thisが2(パー)の時に、1を加えて0 (グー)になるようにするためです
public class Hand { public static final int HANDVALUE_GUU = 0; public static final int HANDVALUE_CHO = 1; public static final int HANDVALUE_PAA = 2; public static final Hand[] hand = { // じゃんけんの手を表す3つのインスタンス new Hand(HANDVALUE_GUU), new Hand(HANDVALUE_CHO), new Hand(HANDVALUE_PAA), }; private static final String[] name = { // じゃんけんの手の文字列表現 "グー", "チョキ", "パー", }; private int handvalue; // じゃんけんの手の値 private Hand(int handvalue) { this.handvalue = handvalue; } public static Hand getHand(int handvalue) { // 値からインスタンスを得る return hand[handvalue]; } public boolean isStrongerThan(Hand h) { // thisがhより強いときtrue return fight(h) == 1; } public boolean isWeakerThan(Hand h) { // thisがhより弱いときtrue return fight(h) == -1; } private int fight(Hand h) { // 引き分けは0, thisの勝ちなら1, hの勝ちなら-1 if (this == h) { return 0; } else if ((this.handvalue + 1) % 3 == h.handvalue) { return 1; } else { return -1; } } public String toString() { return name[handvalue]; } }
Strategyインターフェース
- じゃんけんの戦略のための抽象メソッドを集めたインターフェースです
- nextHandは次にだす手を得るメソッドで、これが呼ばれたら、Strategyインターフェース実装クラスが次の一手を決めます
- studyは"前回の手が勝ったかどうか"を判定するメソッドで、勝ったらstudy(true)、負けたらstudy(false)として呼び出します
- こうすることで、Strategy実装クラスが自分の内部状態を変更し、次回以降のnextHandメソッドの戻り値を決定する材料にします
public interface Strategy { public abstract Hand nextHand(); public abstract void study(boolean win); }
WinningStrategyクラス
- Strategyインターフェース実装クラスの一つです
- nextHandとstudyメソッドを実装します
- ここでの戦略は"前回勝ったら、次回も同じ手を出す"、"前回負けたら、次の手はランダムに出す"という方針です
import java.util.Random; public class WinningStrategy implements Strategy { private Random random; private boolean won = false; //前回の結果を保持する private Hand prevHand; //前回の手を保持する public WinningStrategy(int seed) { random = new Random(seed); //乱数を発生させる } public Hand nextHand() { if (!won) { //wonがtrueの場合にfalse、wonがfalseの場合にtrue prevHand = Hand.getHand(random.nextInt(3)); //負けたらランダム } return prevHand; //勝ったら同じ手 } public void study(boolean win) { won = win; } }
ProbStrategyクラス
- Strategyインターフェース実装クラスの一つです
- 次の手は乱数で決定しますが、過去の勝ち負けの履歴を使って確率を変更します
- historyフィールドで過去の勝敗を反映した表を表します(history[前回の手][今回の手])
- studyメソッドは、nextHandメソッドで返した手の勝敗をもとにhistoryフィールドの内容を変更します
import java.util.Random; public class ProbStrategy implements Strategy { private Random random; private int prevHandValue = 0; private int currentHandValue = 0; private int[][] history = { { 1, 1, 1, }, { 1, 1, 1, }, { 1, 1, 1, }, }; public ProbStrategy(int seed) { random = new Random(seed); } public Hand nextHand() { int bet = random.nextInt(getSum(currentHandValue)); int handvalue = 0; if (bet < history[currentHandValue][0]) { handvalue = 0; } else if (bet < history[currentHandValue][0] + history[currentHandValue][1]) { handvalue = 1; } else { handvalue = 2; } prevHandValue = currentHandValue; currentHandValue = handvalue; return Hand.getHand(handvalue); } private int getSum(int hv) { int sum = 0; for (int i = 0; i < 3; i++) { sum += history[hv][i]; } return sum; } public void study(boolean win) { if (win) { history[prevHandValue][currentHandValue]++; } else { history[prevHandValue][(currentHandValue + 1) % 3]++; history[prevHandValue][(currentHandValue + 2) % 3]++; } } }
Playerクラス
- じゃんけんを行う人を表現したクラスです
- 名前と戦略を与えられてインスタンスを作成します
- nextHandメソッドは次の手を得ますが、実際に決定するのは戦略です
- strategy.nextHandの戻り値が、そのままPlayer のnextHandメソッドの戻り値になります
- つまり、nextHandメソッドは処理をStrategyに委譲していることになります
- 勝敗結果を保持して次の戦略に活かすために、strategyフィールドを通じてstudyメソッドを呼びます
- studyメソッドを使って戦略の内部状態を変化させます
public class Player { private String name; private Strategy strategy; private int wincount; private int losecount; private int gamecount; public Player(String name, Strategy strategy) { // 名前と戦略を授けられる this.name = name; this.strategy = strategy; } public Hand nextHand() { // 戦略におうかがいを立てる return strategy.nextHand(); } public void win() { // 勝った strategy.study(true); wincount++; gamecount++; } public void lose() { // 負けた strategy.study(false); losecount++; gamecount++; } public void even() { // 引き分け gamecount++; } public String toString() { return "[" + name + ":" + gamecount + " games, " + wincount + " win, " + losecount + " lose" + "]"; } }
Mainクラスで動作確認
- 名前と戦略を渡して10000回じゃんけんを行います
public class Main { public static void main(String[] args) { if (args.length != 2) { System.out.println("Usage: java Main randomseed1 randomseed2"); System.out.println("Example: java Main 314 15"); System.exit(0); } int seed1 = Integer.parseInt(args[0]); int seed2 = Integer.parseInt(args[1]); Player player1 = new Player("Taro", new WinningStrategy(seed1)); Player player2 = new Player("Hana", new ProbStrategy(seed2)); for (int i = 0; i < 10000; i++) { Hand nextHand1 = player1.nextHand(); Hand nextHand2 = player2.nextHand(); if (nextHand1.isStrongerThan(nextHand2)) { System.out.println("Winner:" + player1); player1.win(); player2.lose(); } else if (nextHand2.isStrongerThan(nextHand1)) { System.out.println("Winner:" + player2); player1.lose(); player2.win(); } else { System.out.println("Even..."); player1.even(); player2.even(); } } System.out.println("Total result:"); System.out.println(player1.toString()); System.out.println(player2.toString()); } }
コンパイルして実行します
javac -encoding EUC-JP Main.java
java Main 314 15
//乱数を引数に
Even... Winner:[Hana:1 games, 0 win, 0 lose] Winner:[Taro:2 games, 0 win, 1 lose] Even... Winner:[Hana:4 games, 1 win, 1 lose] Winner:[Taro:5 games, 1 win, 2 lose] Even... Even... Winner:[Taro:8 games, 2 win, 2 lose] Winner:[Taro:9 games, 3 win, 2 lose] Winner:[Taro:10 games, 4 win, 2 lose] Even... //中略 Winner:[Hana:9996 games, 3489 win, 3167 lose] Even... Even... Even... Total result: [Taro:10000 games, 3167 win, 3490 lose] [Hana:10000 games, 3490 win, 3167 lose]
Strategyパターンのメリット
- Strategyパターンでは、アルゴリズムの部分をほかの部分と意識的に分離します
- アルゴリズムとのインターフェースの部分だけを規定し、委譲によってアルゴリズムを利用します
- こうすることで、アルゴリズムを改良してもっと高速にしたい場合、Strategyインターフェースを変更せず、アルゴリズムをだけを追加、修正すればいいのです
- 委譲というゆるやかな結びつきを使っているのため、アルゴリズムを用意に切り替えることができます。
- ゲームプログラム等では、ユーザーの選択に合わせて難易度を切り替えるなど
- また、プログラム実行中にConcreteStrategyを変更することもできます
- メモリの少ない環境や、多い環境で戦略を変更するなど
今日のポイント
- Strategyパターンは、問題を解くためのアルゴリズムをごっそり交換できるパターンです
- アルゴリズム (方法・戦略・方策) を簡単に変更し、同じ問題を別の方法で解くことができます
- Strategyパターンでは、アルゴリズムの部分を他と意識的に分離します
- アルゴリズムとのインターフェースの部分だけを規定し、委譲によってアルゴリズムを利用します
- こうすることで、アルゴリズムを改良したい場合、Strategyインターフェースを変更せず、アルゴリズムをだけを追加、修正すればいいことになります
本日もお疲れ様です😊
【Java】Observer パターン【デザインパターン】
目次
Observerパターンについて
Observerパターンとは
- Observerパターンは、観察対象の状態が変化すると、観察者に対して通知が行われる方式です。
- 状態変化に応じた処理を記述するときに有効です
サンプルプログラム
- Observerパターンを使って、数を沢山生成するオブジェクトを観察者が観察して、その値を表示する例を取り上げます
- 表示の方法は、観察者によって変わります
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Observer | 観察者を表すインターフェース |
NumberGenerator(Subject) | 数を生成するオブジェクトを表す抽象クラス |
RandomNumberGenerator(ConcreteSubject) | ランダムに数を生成するクラス |
DigitObserver(ConcreteObserver) | 数字で数を表す行うクラスで、Observerインターフェースを実装する |
GraphObserver(ConcreteObserver) | 簡易グラフで数を表す行うクラスで、Observerインターフェースを実装 |
Observerインターフェース
- updateメソッドを呼びだすのは、数を生成するNumberGeneratorです
- 自分の内容が変更されたことをObserberに伝えます
public interface Observer { public abstract void update(NumberGenerator generator); }
NumberGeneratorクラス
- 数を生成する抽象クラスです
- 実際の数の生成と、取得はサブクラスが実装します
import java.util.ArrayList; import java.util.Iterator; public abstract class NumberGenerator { private ArrayList observers = new ArrayList(); // Observerたちを保持 public void addObserver(Observer observer) { // Observerを追加 observers.add(observer); } public void deleteObserver(Observer observer) { // Observerを削除 observers.remove(observer); } public void notifyObservers() { // Observerへ通知 Iterator it = observers.iterator(); while (it.hasNext()) { Observer o = (Observer)it.next(); o.update(this); } } public abstract int getNumber(); // 数を取得する public abstract void execute(); // 数を生成する }
RandomNumberGeneratorクラス
- 乱数を生成します
import java.util.Random; public class RandomNumberGenerator extends NumberGenerator { private Random random = new Random(); // 乱数生成機 private int number; // 現在の数 public int getNumber() { // 数を取得する return number; } public void execute() { for (int i = 0; i < 20; i++) { number = random.nextInt(50); notifyObservers(); } } }
DigitObserverクラス
* 観察した値を数字で表示するためのものです
public class DigitObserver implements Observer { public void update(NumberGenerator generator) { System.out.println("DigitObserver:" + generator.getNumber()); try { Thread.sleep(100); } catch (InterruptedException e) { } } }
GraphObserverクラス
- 観察した値を*****のように簡易グラフで表示します
public class GraphObserver implements Observer { public void update(NumberGenerator generator) { System.out.print("GraphObserver:"); int count = generator.getNumber(); for (int i = 0; i < count; i++) { System.out.print("*"); } System.out.println(""); try { Thread.sleep(100); } catch (InterruptedException e) { } } }
Mainクラスで動作確認
- NumberGeneratorのインスタンスを一個、その観察者を二個作ります
public class Main { public static void main(String[] args) { NumberGenerator generator = new RandomNumberGenerator(); Observer observer1 = new DigitObserver(); Observer observer2 = new GraphObserver(); generator.addObserver(observer1); //観察者登録 generator.addObserver(observer2); generator.execute(); //数を生成 } }
Observerパターンのメリット
- 状態を持っているRandomNumberGeneratorクラスと、状態変化を通知してもらうDigitObserverクラス、GraphObserverクラスが登場します
- その2つの役目を、ObserverインターフェースとNumberGeneratorクラスが繋ぎます
- RandomNumberGeneratorクラスは、自分が現在監視しているの(通知する相手)が、DigitObserver/GraphObserverインスタンスなのかを知りません。
- しかし、observersフィールドに格納されているインスタンスが、Observerインターフェースを継承していることは知っており、updateメソッドを呼び出せることが保証されています。
- 一方、DigitObserverクラス、GraphObserverクラスは、自分を観察しているのがRandomNumberGeneratorインスタンスなのか、他のXXXNumberGeneratorインスタンスなのかを知らず、NumberGeneratorのサブクラスのインスタンスであり、getNumberメソッドを持っていることは知っています。
- 具象クラスの交換可能性を高くするポイントは以下の2点です
- MVCモデルの中の、ModelとViewの関係は、 ObserverパターンのSubjectとObserverの関係に相当します
- Modelは、"表示形式に依存しない内部モデルを操作する"役割で、Viewは "Modelをどのように見せるか" 管理します。一般的に、1つのModelに対して、複数のViewが対応します。
今日のポイント
- Observerパターンは、観察対象の状態が変化すると、観察者に対して通知が行われる方式です
- 状態を持っているクラス(ConcreteSubject)と、状態変化を通知してもらうクラスConcreteObserver)が登場します
- お互いに通知する相手は知らないので、独立性を高めることができます。
- 具象クラスの交換可能性を高くするには、以下が重要です
- MVCモデルの中の、ModelとViewの関係は、 ObserverパターンのSubjectとObserverの関係に相当します
本日もお疲れ様です😊
【Java】Bridgeパターン【デザインパターン】
目次
Bridgeパターンについて
Bridgeパターンとは
- 機能のクラス階層と、実装のクラス階層を、橋渡しするパターンです
- 機能のクラス階層と実装のクラス階層が階層構造の中に混在しないように、2つの独立したクラス階層に分離して、それぞれの階層の橋渡しをします
機能のクラス階層
- クラスに新しい機能を追加したい場合は、クラス階層の中から、目的に近いクラスを探し、そのサブクラスを作成します
- スーパークラスでは基本的な機能を持っている
- サブクラスでは新しい機能を追加する
- 他の機能を追加する場合は、さらに新しいサブクラスを用意して、階層構造にします
実装のクラス階層
- 抽象メソッドでインターフェースを規定し、サブクラスで実装します
- 役割分担をすることで部品として交換可能性が高いクラスを作ることができます
- 新しいメソッドを追加するためではなく、役割分担のために、クラス階層を使います
- スーパークラスは抽象メソッドでインターフェースを規定する
- サブクラスは具象メソッドでインターフェースを実装する
サンプルプログラム
- Bridgeパターンを使って、抽象的な何かを表じする例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Display (Abstraction) | <機能のクラス階層> 表示するクラス |
CountDisplay (RefinedAbstration) | <機能のクラス階層> 指定回数だけ表示する機能を追加したクラス |
DisplayImpl (Implementor) | <実装のクラス階層> 表示するクラス |
StringDisplayImpl (ConcreteImplementor) | <実装のクラス階層> 文字列を使って表示するクラス |
<機能のクラス階層> Displayクラス
- 抽象的な何かを表示するクラスで、機能のクラスの最上位にいるクラスです
- コンストラクタには、実装を表すクラスのインスタンスを渡します
- 引数で渡されたインスタンスはimplのフィールドに保存され、2つのクラス階層の橋になります
- open,print,closeメソッドは、Displayクラスのインターフェースで、implフィールドの実装メソッドを用います
- Displayのインターフェースが、DisplayImplに変換されていることになります
public class Display { private DisplayImpl impl; //実装を表すインスタンス public Display(DisplayImpl impl) { this.impl = impl; } public void open() { impl.rawOpen(); } public void print() { impl.rawPrint(); } public void close() { impl.rawClose(); } public final void display() { open(); print(); close(); } }
<機能のクラス階層> CountDisplayクラス
- Displayクラスの機能に、指定回数表示する、という機能を追加したクラスです
public class CountDisplay extends Display { public CountDisplay(DisplayImpl impl) { super(impl); } public void multiDisplay(int times) { // times回繰り返して表示する open(); for (int i = 0; i < times; i++) { print(); } close(); } }
<実装のクラス階層> DisplayImplクラス
- 実装のクラス階層の最上位に位置する抽象クラスです
- 抽象メソッドを3つ持ち、それぞれDisplayクラスのメソッド(open,print,close)に対応します
public abstract class DisplayImpl { public abstract void rawOpen(); public abstract void rawPrint(); public abstract void rawClose(); }
<実装のクラス階層> StringDisplayImplクラス
- 文字列を表示するクラスで、DisplayImplクラスのサブクラスとして抽象メソッドを実装します
public class StringDisplayImpl extends DisplayImpl { private String string; private int width; public StringDisplayImpl(String string) { this.string = string; this.width = string.getBytes().length; } public void rawOpen() { printLine(); } public void rawPrint() { System.out.println("|" + string + "|"); } public void rawClose() { printLine(); } private void printLine() { System.out.print("+"); for (int i = 0; i < width; i++) { System.out.print("-"); } System.out.println("+"); } }
Mainクラスで動作確認
- 4クラスを組み合わせて文字列を表示します
- Displayクラス、CountDisplayクラスも共に、StringDisplayImplクラスのインスタンスが実装を担っています
public class Main { public static void main(String[] args) { Display d1 = new Display(new StringDisplayImpl("Hello, Japan.")); //Displayクラスのインスタンス代入 Display d2 = new CountDisplay(new StringDisplayImpl("Hello, World.")); //CountDisplayクラスのインスタンス代入 CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe.")); d1.display(); d2.display(); d3.display(); d3.multiDisplay(5); } }
Bridgeパターンのメリット
- 機能と実装のクラスを分けておくことで、それぞれのクラス階層を独立に拡張できます
- 機能を追加したい場合、機能のクラス階層にクラスを追加します。実装のクラス階層は変更する必要がありません
- しかも、追加した機能は全ての実装で使用できます
- クラス間が硬い結びつきになる"継承"ではなく、緩い結びつきになる、"委譲"を行います
- Abstraction (Display) クラスでは、implフィールドに実装のインスタンスを保持し、処理は実装クラスに任せて、たらい回しにしています
- 実装の変更を行いたい場合は、別のConcreteImplementerのインスタンスをAbstraction やRefinedAbstration クラスに渡したらいいので、Mainクラス以外変更する必要がありません
今日のポイント
- 機能のクラス階層と、実装のクラス階層を、橋渡しするパターンです
- 機能のクラス階層と実装のクラス階層が階層構造の中に混在しないように、2つの独立したクラス階層に分離して、それぞれの階層の橋渡しをします
- 機能と実装のクラスを分けておくことで、それぞれのクラス階層を独立に拡張できます
- 機能を追加する場合、機能のクラス階層にクラスを追加します。
- 実装のクラス階層は変更する必要がなく、追加した機能は全ての実装で使用できます
- 実装の変更を行いたい場合は、別のConcreteImplementerのインスタンスをAbstraction やRefinedAbstration クラスに渡すだけで良く、Mainクラス以外変更する必要がありません
- 機能を追加する場合、機能のクラス階層にクラスを追加します。
本日もお疲れ様です😊