【Java】Command パターン【デザインパターン】
目次
Commandパターンについて
Commandパターンとは
- Commandパターンは、命令を表すクラスのインスタンスを、1つのものとして表現するパターンです
- 命令の履歴を管理したいときは、そのインスタンスの集まりを管理すればいいことになります
- 命令の集まりを保存しておけば、同じ命令を実行したり、複数の命令をまとめて新しい命令として再利用したりできます
サンプルプログラム
- Commandパターンを使って、簡単なお絵かきプログラム例を取り上げます
- マウスをドラッグすれば赤の点線が描画され、clearを押すと、点が消えます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Command | 命令を表現するインターフェース |
MacroCommand(ConcreteCommand) | 「複数の命令をまとめた命令」を表現するクラス |
DrawCommand(ConcreteCommand) | 「点の描画命令」を表現するクラス |
Drawable | 「描画対象」を表現するインターフェース |
DrawCanvas(Receiver) | 「描画対象」を実装したクラス |
Command インターフェース
- 命令を表現するインターフェースでexecuteメソッドのみを持ちます
- 何が起こるかはMacroCommand・DrawCommandクラスが実装します
package command; public interface Command { public abstract void execute(); }
MacroCommandクラス
- 複数の命令をまとめた命令を表します
- commandsフィールドに複数のCommand実装クラスのインスタンスを集めておきます
- appendメソッドで、Commandを実装したクラスのインスタンスを追加します
- 無限ループにならないよう、ifで自分自身(this)ではないことをチェックします
- undoメソッドで、commandsの最後の命令を削除します
package command; import java.util.Stack; import java.util.Iterator; public class MacroCommand implements Command { // 命令の集合 private Stack commands = new Stack(); // 実行 public void execute() { Iterator it = commands.iterator(); while (it.hasNext()) { ((Command)it.next()).execute(); } } // 追加 public void append(Command cmd) { if (cmd != this) { commands.push(cmd); } } // 最後の命令を削除 public void undo() { if (!commands.empty()) { commands.pop(); } } // 全部削除 public void clear() { commands.clear(); } }
DrawCommandクラス
- 点の描画命令を表現します
- Pointは
java.awt.Point
で定められているクラスで、X,Y座標で二次元平面上の位置を表します - コンストラクタでDrawableインターフェース実装クラスのインスタンスと、Pointクラスのインスタンスを引数に渡して、フィールドに代入します
- "この位置に点を描け"という命令を表しています
package drawer; import command.Command; import java.awt.Point; public class DrawCommand implements Command { // 描画対象 protected Drawable drawable; // 描画位置 private Point position; // コンストラクタ public DrawCommand(Drawable drawable, Point position) { this.drawable = drawable; this.position = position; } // 実行 public void execute() { drawable.draw(position.x, position.y); } }
Drawableインターフェース
- 描画対象を表します
- drawは描画するメソッドです
package drawer; public interface Drawable { public abstract void draw(int x, int y); }
DrawCanvasクラス
- Drawableインターフェース実装クラスで、java.awt.Canvas クラスのサブクラスです
- 自分が描画すべき命令の集合は、MacroCommand型のhistoryフィールドに保持します
- 履歴全体を再描画する時は、paintメソッドで
history.execute()
を呼ぶだけで、保持されている命令の集まりが再実行されます
package drawer; import command.*; import java.util.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class DrawCanvas extends Canvas implements Drawable { // 描画色 private Color color = Color.red; // 描画する点の半径 private int radius = 6; // 履歴 private MacroCommand history; // コンストラクタ public DrawCanvas(int width, int height, MacroCommand history) { setSize(width, height); setBackground(Color.white); this.history = history; } // 履歴全体を再描画 public void paint(Graphics g) { history.execute(); } // 描画 public void draw(int x, int y) { Graphics g = getGraphics(); g.setColor(color); g.fillOval(x - radius, y - radius, radius * 2, radius * 2); } }
Mainクラスで動作確認
- hostoryで描画履歴を保持し、DrawCanvas に渡します
- 描画履歴はMainとDrawCanvasで共有されているということになります
import command.*; import drawer.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener { // 描画履歴 private MacroCommand history = new MacroCommand(); // 描画領域 private DrawCanvas canvas = new DrawCanvas(400, 400, history); // 消去ボタン private JButton clearButton = new JButton("clear"); // コンストラクタ public Main(String title) { super(title); this.addWindowListener(this); canvas.addMouseMotionListener(this); clearButton.addActionListener(this); Box buttonBox = new Box(BoxLayout.X_AXIS); //横にボタンを並べるボックス buttonBox.add(clearButton); Box mainBox = new Box(BoxLayout.Y_AXIS); //縦にボタンを並べるボックス mainBox.add(buttonBox); mainBox.add(canvas); getContentPane().add(mainBox); ..コンテナにコンポーネントを載せる pack(); show(); } // ActionListener用 public void actionPerformed(ActionEvent e) { if (e.getSource() == clearButton) { history.clear(); canvas.repaint(); } } // MouseMotionListener用 public void mouseMoved(MouseEvent e) { } public void mouseDragged(MouseEvent e) { Command cmd = new DrawCommand(canvas, e.getPoint()); history.append(cmd); //実行履歴に追加 cmd.execute(); //即実行 } // WindowListener用 public void windowClosing(WindowEvent e) { System.exit(0); } public void windowActivated(WindowEvent e) {} public void windowClosed(WindowEvent e) {} public void windowDeactivated(WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowIconified(WindowEvent e) {} public void windowOpened(WindowEvent e) {} public static void main(String[] args) { new Main("Command Pattern Sample"); } }
Commandパターンのメリット
- 命令をオブジェクトとして表現することで、命令の履歴をとったり、命令の再実行を行うことができます
- 新しい命令を追加したい場合は、Commandインターフェース実装クラスを作成すればよく、機能拡張が行いやすくなります
- ConcreteCommand自身が、Receiver(描画対象)も知っていることで、ConcreteCommandを誰が管理してもいつでもexecuteできるようになります
アダプターについて
- プログラミングを簡略化するために、アダプターというクラス群が
java.awt.event
パッケージに用意されています(Adapterパターンの一例)- MouseMotionListenerインターフェースに対して、MouseMotionAdapterクラスなど
- MouseMotionAdapterクラスでは、MouseMotionListenerインターフェースを実装し、要求するメソッドを全部提供します *しかし、提供されているインターフェースの実装の中身は空になっているので、MouseMotionAdapterクラスのサブクラスを作り、必要なメソッドを実装する事で、目的を達成できます
- 匿名インナークラス機構を組み合わせてアダプターを使うと、スマートに記述できます
- MouseMotionAdapterクラスの名前なしサブクラスを作り、そのインスタンスを生成できます
- オーバーライドしたいメソッドのみ実装すれば、他は書く必要がありません
今日のポイント
- Commandパターンは、命令を表すクラスのインスタンスを、1つのものとして表現するパターンです
- 命令をオブジェクトとして表現することで、命令の履歴をとったり、命令の再実行を行うことができます
- 新しい命令を追加したい場合は、Commandインターフェース実装クラスを作成すればよく、機能拡張が行いやすくなります
本日もお疲れ様です😊
【Java】Builderパターン【デザインパターン】
目次
Builderパターンについて
Builderパターンとは
- 全体を構成している、各部分部分の処理を積上げていくためのパターンです
サンプルプログラム
- Builderパターンを使って、タイトル、文字列、箇条項目を含む文章を作成する例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Builder | 文章を構成するメソッドを定めた抽象クラス 表現形式を決定する |
Director | 一つの文章を作るクラス 作成過程を決定する |
TextBuilder | プレーンテキストで文章を作るクラス |
HTMLBuilder | HTMLファイルでで文章を作るクラス |
Builderクラス
- 文章を作るメソッドを宣言する抽象クラスです
public abstract class Builder { public abstract void makeTitle(String title); public abstract void makeString(String str); public abstract void makeItems(String[] items); public abstract void close(); }
Directorクラス
- Builderクラスで宣言されているメソッドで文章を作ります
- Builderクラスは抽象クラスなので、インスタンスを生成できません
- Directorクラスのコンストラクタに渡されるのは、Builderクラスのサブクラスのインスタンスです
- 与えられたインスタンスの種類によって、Direntorで作るインスタンスの形式が決まります
public class Director { private Builder builder; public Director(Builder builder) { // 与えられたBuilderのサブクラスのインスタンスを、builderフィールドに保持 this.builder = builder; } public void construct() { // 文書構築メソッド builder.makeTitle("Greeting"); // タイトル builder.makeString("朝から昼にかけて"); // 文字列 builder.makeItems(new String[]{ // 箇条書き "おはようございます。", "こんにちは。", }); builder.makeString("夜に"); // 別の文字列 builder.makeItems(new String[]{ // 別の箇条書き "こんばんは。", "おやすみなさい。", "さようなら。", }); builder.close(); // 文書を完成させる } }
TextBuilderクラス
- Builderクラスのサブクラスです
public class TextBuilder extends Builder { private StringBuffer buffer = new StringBuffer(); public void makeTitle(String title) { buffer.append("==============================\n"); buffer.append("『" + title + "』\n"); buffer.append("\n"); } public void makeString(String str) { buffer.append('■' + str + "\n"); buffer.append("\n"); } public void makeItems(String[] items) { for (int i = 0; i < items.length; i++) { buffer.append(" ・" + items[i] + "\n"); } buffer.append("\n"); } public void close() { buffer.append("==============================\n"); } public String getResult() { // 完成した文書 return buffer.toString(); // StringBufferをStringに変換 } }
HTMLBuilderクラス
- Builderクラスのサブクラスです
- 文章を構築し、HTMLファイル名を返します
import java.io.*; public class HTMLBuilder extends Builder { private String filename; // 作成するファイル名 private PrintWriter writer; // ファイルに書き込むPrintWriter public void makeTitle(String title) { filename = title + ".html"; try { writer = new PrintWriter(new FileWriter(filename)); } catch (IOException e) { e.printStackTrace(); } writer.println("<html><head><title>" + title + "</title></head><body>"); writer.println("<h1>" + title + "</h1>"); } public void makeString(String str) { writer.println("<p>" + str + "</p>"); } public void makeItems(String[] items) { writer.println("<ul>"); for (int i = 0; i < items.length; i++) { writer.println("<li>" + items[i] + "</li>"); } writer.println("</ul>"); } public void close() { writer.println("</body></html>"); writer.close(); } public String getResult() { // 完成した文書のファイル名を返す return filename; } }
Mainクラスで動作確認
public class Main { public static void main(String[] args) { if (args.length != 1) { usage(); System.exit(0); } if (args[0].equals("plain")) { TextBuilder textbuilder = new TextBuilder(); Director director = new Director(textbuilder); director.construct(); String result = textbuilder.getResult(); System.out.println(result); } else if (args[0].equals("html")) { HTMLBuilder htmlbuilder = new HTMLBuilder(); Director director = new Director(htmlbuilder); director.construct(); String filename = htmlbuilder.getResult(); System.out.println(filename + "が作成されました。"); } else { usage(); System.exit(0); } } public static void usage() { System.out.println("Usage: java Main plain プレーンテキストで文書作成"); System.out.println("Usage: java Main html HTMLファイルで文書作成"); } }
Builderパターンのメリット
- MainはDirectorクラスのconstructメソッドのみを呼び出し、
- DirectorクラスはConcreteBuilderクラスに依存せず、Builderクラスのメソッドのみを利用します
- →ConcreteBuilderクラスは必要に応じて変更することができます
- Builderの抽象メソッドにどのような動作が期待されているのかを知っておくのが重要です
今日のポイント
- 全体を構成している、各部分部分の処理を積上げていくためのパターンです
- DirectorクラスはConcreteBuilderクラスに依存しないので、必要に応じて変更することができます
- 例えば、同じ文書を要求に合わせて、異なる表現形式で出力することができます
本日もお疲れ様です😊
【Java】Prototypeパターン【デザインパターン】
目次
Prototypeパターンについて
Prototypeパターンとは
サンプルプログラム
- Prototypeパターンを使って、文字列を枠線で囲んだり、下線を引いて表示する例を取り上げます
- 各クラスやインターフェースの役割は以下のようになっています
名前 | 役割 |
---|---|
Product (Prototype) | 抽象メソッドuse,createCloneを宣言するインターフェース |
Manager (Client) | createCloneを使ってインスタンスを複製するクラス |
ManagerBox (ConcretePrototype) | 文字列を枠線で囲って表示するクラス use,createCloneを実装 |
UnderlinePen (ConcretePrototype) | 文字列に下線を引いて表示するクラス use,createCloneを実装 |
Productインターフェース
java.lang.Cloneable
インターフェースを継承し、cloneメソッドで自動的に複製を行います- Cloneableインターフェースはマーカーインターフェースです
package framework; import java.lang.Cloneable; public interface Product extends Cloneable { public abstract void use(String s); public abstract Product createClone(); }
Managerクラス
- インスタンスの複製を行います
- showcaseフィールドに、インスタンスの名前とインスタンスの対応関係をHashMapで保存します
- registerメソッドでは、製品の名前とProductインターフェースを与えられたら、showcaseに登録します
- 引数のProductインターフェース(proto)は、Productインターフェースを実装したクラスのインスタンスで、use,createCloneメソッドを呼ぶことができます
- ProductインターフェースやManagerクラスにはManagerBoxやUnderlinePenクラス名は使用しません
- つまり、ProductインターフェースやManagerクラスは、独立に修正することができます(疎結合)
- Productインターフェースが、Managerクラスと他のクラスを紐づけています
package framework; import java.util.*; public class Manager { private HashMap showcase = new HashMap(); public void register(String name, Product proto) { showcase.put(name, proto); } public Product create(String protoname) { Product p = (Product)showcase.get(protoname); return p.createClone(); } }
MessageBoxクラス
- Productインターフェースを実装します
- createCloneメソッドで呼び出しているcloneメソッドは自分自身の複製を行うメソッドで、インスタンスが持つフィールド値も新しいインスタンスにコピーします
- cloneメソッドは浅いコピー(shallow copy)で、参照のみコピーします
- cloneメソッドでコピーできるのは、
java.lang.Cloneable;
インターフェース実装クラスのみです - インターフェースが実装されていないと、CloneNotSupportedExceptionが投げられるので、try...catch構文で例外処理を実装します
- MessageBoxクラスで実装しているProductインターフェースで、Cloneableインターフェースを実装しているので、例外は投げられません
- cloneメソッドは自分とそのサブクラスからしか呼べないので、別のメソッド(createClone)で包む必要があります
import framework.*; public class MessageBox implements Product { private char decochar; //囲み文字(*など) public MessageBox(char decochar) { this.decochar = decochar; } public void use(String s) { int length = s.getBytes().length; for (int i = 0; i < length + 4; i++) { System.out.print(decochar); } System.out.println(""); System.out.println(decochar + " " + s + " " + decochar); for (int i = 0; i < length + 4; i++) { System.out.print(decochar); } System.out.println(""); } public Product createClone() { Product p = null; try { p = (Product)clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return p; } }
UnderlinePenクラス
- Productインターフェースを実装します
- 与えられた文字列を引用符で括り、ulcharで与えられた文字で下線を引きます
import framework.*; public class UnderlinePen implements Product { private char ulchar; public UnderlinePen(char ulchar) { this.ulchar = ulchar; } public void use(String s) { int length = s.getBytes().length; System.out.println("\"" + s + "\""); System.out.print(" "); for (int i = 0; i < length; i++) { System.out.print(ulchar); } System.out.println(""); } public Product createClone() { Product p = null; try { p = (Product)clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return p; } }
Mainクラスで動作確認
import framework.*; public class Main { public static void main(String[] args) { // 準備 Manager manager = new Manager(); UnderlinePen upen = new UnderlinePen('~'); MessageBox mbox = new MessageBox('*'); MessageBox sbox = new MessageBox('/'); manager.register("strong message", upen); manager.register("warning box", mbox); manager.register("slash box", sbox); // 生成 Product p1 = manager.create("strong message"); p1.use("Hello, world."); Product p2 = manager.create("warning box"); p2.use("Hello, world."); Product p3 = manager.create("slash box"); p3.use("Hello, world."); } }
Prototypeパターンのメリット
- ManagerクラスはcreateCloneを呼びますが、どのクラスのインスタンスを複製するかは知らなくとも、Productインターフェースを実装するクラスであれば、そのインスタンスを複製できます
- Productインターフェースを実装するクラスのインスタンスを作成し、Managerに登録しておけば好きな時に複製できます
- →こうすることで雛形をいくらでも増やすことができます(
~
で文字列に下線を引いたり、*
や/
で文字列に枠をつける等)- それぞれ別のクラスにしていたら、クラスの数が多くなり管理が大変になります。。
- インスタンスの複製(clone)をframeworkパッケージに閉じ込めています
- →インスタンスを生成するフレームワークを、特定のインスタンスに依存しない様に作ることができます
今日のポイント
- Prototypeパターンは、クラスからインスタンスを生成するのではなく、模型となるインスタンスから別のインスタンスを複製(clone)して生成するパターンです
- PrototypeのインターフェースやClientクラスにはConcretePrototypeクラス名は使用しません(=疎結合である)
- つまり、PrototypeのインターフェースやClientクラスは、切り離して部品として再利用、修正することができます
- インスタンスの複製(clone)をframeworkパッケージに閉じ込めることで、インスタンスを生成するフレームワークを、特定のインスタンスに依存しない様に作ることができます
本日もお疲れ様です😊
【Java】Singletonパターン【デザインパターン】
目次
Singletonパターンについて
Singletonパターンとは
サンプルプログラム
名前 | 役割 |
---|---|
Singleton | インスタンスが1つしかないクラス |
Main | 動作テスト用クラス |
Singletonクラス
- staticフィールド(クラス変数)としてsingletonを定義し、Singletonクラスのインスタンスで初期化します
- コンストラクタをprivateにすることで、Singletonクラスの外でコンストラクタの呼び出しを禁止します
- これで、インスタンスが1つしか生成されないことを保証することができます
- 唯一のインスタンスを得るためのstasticメソッド(getInstance)で同じインスタンスを返します
public class Singleton { private static Singleton singleton = new Singleton(); private Singleton() { System.out.println("インスタンスを生成しました。"); } public static Singleton getInstance() { return singleton; } }
Mainクラスで動作確認
- SingletonクラスのgetInstanceメソッドを使ってSingletonのインスタンスを得ています
- ここで、obj1,obj2のように二回インスタンスを得ていますが、同一性の判定結果は、"obj1とobj2は同じインスタンスです" と表示されます
- Singletonクラスのコンストラクタは一回しか呼ばれていないので、getInstanceしても同じインスタンスが帰されることになります!
public class Main { public static void main(String[] args) { System.out.println("Start."); Singleton obj1 = Singleton.getInstance(); Singleton obj2 = Singleton.getInstance(); if (obj1 == obj2) { System.out.println("obj1とobj2は同じインスタンスです。"); } else { System.out.println("obj1とobj2は同じインスタンスではありません。"); } System.out.println("End."); } }
Singletonパターンのメリット
今日のポイント
- 生成するインスタンスの数を1つに制限するデザインパターンです
- Singletonクラスのコンストラクタをprivateにすることで、Singletonクラスの外でコンストラクタの呼び出しを禁止します
- これによって、インスタンスが1つしか生成されないことを保証することができます
- Singletonクラス内の、唯一のインスタンスを得るためのstasticメソッド(getInstance)で同じインスタンスを返します
- Singleton利用クラスから、Singletonクラスのインスタンスを複数回得る(getInstance)場合でも、 Singletonクラスのコンストラクタは一回しか呼ばれないので、同一のインスタンスが帰されることになります
本日もお疲れ様です😊
【Java】Factory Methodパターン【デザインパターン】
目次
Factory Methodパターンについて
Factory Methodパターンとは
- インスタンスの作り方をスーパークラスで定め、サブクラスで具体的に記述するパターンです
- インスタンスを生成する工場(factory)をTemplate Mathodパターンで構成する、という事です
- (参考)Template Mathodパターン
- インスタンス生成の枠組みと、実際のインスタンス生成のクラスとを分けて考えることができるようになります
サンプルプログラム
- Factory Methodパターンを使って、身分証明書カードを作成する例を取り上げます
- 各クラスの役割は以下のようになっています
- frameworkパッケージは、インスタンス生成の枠組みの役割を担います
- idcardパッケージは、インスタンスの具体的な実装を行います
名前 | 役割 |
---|---|
Product (frameworkパッケージ) | useメソッドのみ定義されている抽象クラス |
Factory (frameworkパッケージ) | createメソッドを実装している抽象クラス |
IDCard (idcardパッケージ) | useメソッド実装クラス |
IDCardFactory (idcardパッケージ) | createProduct、registerProductメソッド実装クラス |
Productクラス
- 製品を表現しています
- ここでの製品とは、"何であれuseできるもの"と、定めています
- useメソッドの宣言のみ行っていて、具体的な実装は全てサブクラスに任せます
package framework; public abstract class Product { public abstract void use(); }
Factoryクラス
- Template Method パターンが使われています
- createメソッドがテンプレートメソッドです
- 抽象メソッドcreateProduct、registerProduct(製品を作る、登録する)はサブクラスで実装します
- Factoryクラスは、"createメソッドでProductインスタンスを生成し、createProductして、registerProductする"、という手順で実装されています
package framework; public abstract class Factory { public final Product create(String owner) { Product p = createProduct(owner); registerProduct(p); return p; } protected abstract Product createProduct(String owner); //製品を作る protected abstract void registerProduct(Product product); //製品を登録する }
IDCardクラス
- 認証番号をカードを表します
- Productクラスで宣言されたuseメソッドを実装します
package idcard; import framework.*; public class IDCard extends Product { private String owner; IDCard(String owner) { System.out.println(owner + "のカードを作ります。"); this.owner = owner; } public void use() { System.out.println(owner + "のカードを使います。"); } public String getOwner() { return owner; } }
IDCardFactoryクラス
- Factoryクラスで宣言されたcreateProduct、registerProductメソッドを実装します
- createProductでIDcardのインスタンスを生成します
- registerProductでIDcardのownerをownersフィールドに追加しています
package idcard; import framework.*; import java.util.*; public class IDCardFactory extends Factory { private List owners = new ArrayList(); protected Product createProduct(String owner) { return new IDCard(owner); } protected void registerProduct(Product product) { owners.add(((IDCard)product).getOwner()); } public List getOwners() { return owners; } }
Mainクラスで動作確認
- createメソッドでIDcardを作って、useメソッドで使います
import framework.*; import idcard.*; public class Main { public static void main(String[] args) { Factory factory = new IDCardFactory(); Product card1 = factory.create("ナウシカ"); Product card2 = factory.create("キキ"); Product card3 = factory.create("千尋"); card1.use(); card2.use(); card3.use(); } }
Factory Methodパターンのメリット
- Creater(Factoryクラス)は、Product(Productクラス)と、インスタンス生成のメソッド(createProduct、registerProductメソッド)を知っていればProduct(IDcard)を生成することができます
- newによるインスタンス生成を、 インスタンス生成のためのメソッド呼び出しに変えることで、具体的なクラス名(IDCardFactoryクラス)に束縛されません
- frameworkパッケージは、idcardパッケージに依存していません
- →他の製品を作りたい場合、
import framework.*;
した別のパッケージを作れば良いだけで、frameworkパッケージの中身を変更する必要がありません
今日のポイント
- インスタンスの作り方をスーパークラスで定め、サブクラスで具体的に記述するパターンです
- インスタンス生成の枠組みと、実際のインスタンス生成のクラスとを分けて考えることができるようになります
- インスタンス生成の枠組みを記述する(framework)パッケージは、インスタンスの具体的な実装を行うパッケージに依存していません
- →他のProductを生成したい場合、
import framework.*;
した別のパッケージを作れば良いだけで、frameworkパッケージの中身を変更する必要がありません
本日もお疲れ様です😊
【Java】Template Methodパターン【デザインパターン】
目次
- Template Methodパターンについて
- Template Methodパターンとは
- Template Methodパターンのメリット
- LSP (The Liskov Substitution Principle) とは
- 今日のポイント
Template Methodパターンについて
Template Methodパターンとは
- スーパークラスで処理の枠組みを定め、サブクラスでその具体的内容を定めるデザインパターンです
- スーパークラスでは、テンプレートとなる抽象メソッドを定義します
- テンプレートとは、
文書などのコンピュータデータを作成する上で雛形となるデータ
- テンプレートとは、
- のことで、例えば、カリグラフィーなど文字を綺麗に書くためのテンプレート板などがあります
- テンプレート板を使えば、どんなペンでなぞっても同じデザインの文字を描くことができます
- オブジェクト指向言語では、同じような処理を1つのクラスとして纏めるように設計するので、Template Methodパターンに則っているということですね〜💡
サンプルプログラム
- Template Methodパターンを使って、文字や文字列を5回繰り返して表示する例を取り上げます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
AbstractDisplay (AbstractClass) | displayメソッドのみ実装されている抽象クラス |
CharDisplay (ConcreteClass) | open、print、closeを実装しているクラス 文字表示をする |
StringDisplay (ConcreteClass) | open、print、closeを実装しているクラス 文字列表示をする |
AbstractDisplayクラス
- 抽象クラスAbstractDisplayでは、 サブクラスに実装をまかせる抽象メソッド (open,print,close) を用意します
- AbstractDisplayクラスでは、displayメソッドを実装します
- まずopenして、5回printを繰り返して、最後にcloseします
- displayメソッドがテンプレートメソッドになります
public abstract class AbstractDisplay { public abstract void open(); public abstract void print(); public abstract void close(); public final void display() { open(); for (int i = 0; i < 5; i++) { print(); } close(); } }
CharDisplayクラス
- CharDisplayクラスは、AbstractDisplayのサブクラスです
- コンストラクタで渡された文字chを、フィールドに保存します
- スーパークラスの抽象メソッド (open,print,close) を、オーバーライドして実装します
- printメソッドでは、フィールドに記憶しておいた文字を1個表示します
- AbstractDisplayクラスのdisplayメソッドから繰り返して呼び出されます
- スーパークラスで宣言したメソッドをサブクラスで実装する際には、実装したメソッドが呼ばれるタイミングを理解しておきましょう
public class CharDisplay extends AbstractDisplay { private char ch; public CharDisplay(char ch) { this.ch = ch; } public void open() { System.out.print("<<"); } public void print() { System.out.print(ch); } public void close() { System.out.println(">>"); } }
StringDisplayクラス
- StringDisplayクラスは、AbstractDisplayのサブクラスです
- コンストラクタで渡された文字列stringと、バイト単位の幅widthを、フィールドに記憶します
- スーパークラスの抽象メソッド (open,print,close) を、オーバーライドして実装します
- open、closeメソッドでは、StringDisplayクラスのメソッドprintLineで線を引きます
- printメソッドでは、フィールドに記憶しておいた文字列の前後に"|"をつけて表示します
- printLineメソッドはprivateなので、このクラスの中だけで使われます
public class StringDisplay extends AbstractDisplay { private String string; // 表示するべき文字列 private int width; // バイト単位で計算した文字列の幅 public StringDisplay(String string) { this.string = string; this.width = string.getBytes().length; } public void open() { printLine(); } public void print() { System.out.println("|" + string + "|"); } public void close() { printLine(); } private void printLine() { System.out.print("+"); for (int i = 0; i < width; i++) { System.out.print("-"); } System.out.println("+"); } }
Mainクラスで動作確認
- CharDisplay、StringDisplayのインスタンスを1個作ります
- 作成したインスタンス(d1,d2,d3)は、AbstractDisplayのサブクラスのインスタンスなので、 継承したdisplayメソッドを呼び出すことができます
- 実際の動作は個々のクラスCharDisplayやStringDisplayで定まります
public class Main { public static void main(String[] args) { AbstractDisplay d1 = new CharDisplay('H'); AbstractDisplay d2 = new StringDisplay("Hello, world."); AbstractDisplay d3 = new StringDisplay("こんにちは。"); d1.display(); d2.display(); d3.display(); } }
Template Methodパターンのメリット
- スーパークラスのテンプレートメソッドでアルゴリズムが記述されているので、サブクラスではアルゴリズムを記述しなくて良いです
- アルゴリズムがが各クラスに散らばっていないので、テンプレートメソッドにバグがあった場合に改修がしやすいです
- スーパークラス型の変数に、サブクラスのインスタンスが代入されているので、サブクラスの種類を特定しなくても動くコードになります
LSP (The Liskov Substitution Principle) とは
- クラス同士の継承関係が正しいか、を検証する原則で、リスコフの置換原則ともいわれます
- Template Methodパターンに限らない、継承の一般的な原則です
- 具体的には、スーパークラスとサブクラスを定義するとき、サブクラス別のサブクラスに置き換えても、スーパークラスがは正常に動作しないといけないというルールです
- 継承関係でバグが発生しないように実装するときに気をつけるべきは、以下の点です
今日のポイント
- Template Methodパターンは、スーパークラスで処理の枠組みを定め、サブクラスでその具体的内容を定めるデザインパターンです
- 同じような流れの処理を共通化したい時に、Template Methodパターンを適応することができます
- スーパークラス(抽象クラス)では、 サブクラスに実装をまかせる抽象メソッド を用意します
- スーパークラス型の変数に 、サブクラスのインスタンスを代入することで、サブクラスの種類を特定しなくても動くコードになります
- よって、リスコフの置換原則に則った設計ができているということになります
本日もお疲れ様です😊
【Java】Adapterパターン(委譲)【デザインパターン】
目次
Adapterパターンについて
Adapterパターンとは
- "すでに提供されているもの"がそのまま使えない時に、"必要なもの"とのずれを埋めて、利用できるようにするためのデザインパターンです
- Wrapper(包む)パターンと呼ばれることもあります
- クラスによるAdaoterパターン(継承)と、インスタンスによるAdapterパターン(委譲)があります
インスタンスによるAdaoterパターン(委譲)
- 委譲とは、あるメソッドの処理を他のインスタンスのメソッドに任せることです
サンプルプログラム
- Adaoterパターンを使って、与えられた文字列を()や*で括る例を取り上げます
- 各クラスの役割は以下のようになっています
- ()にACアダプターが交流100Vを直流12Vに変換する例を示しています
名前 | 役割 |
---|---|
Bannerクラス(Adaptee) | 提供されているもの (例:交流100V) |
PrintBannerクラス(Adapter) | 変換装置 (例:アダプター) |
Printクラス(Target) | 必要なもの (例:直流12V) |
Manクラス(Client) | 必要なものを使って仕事を行う (例:直流12Vで動くノートパソコン) |
Bannerクラス
- あらかじめ提供されているクラスです
public class Banner { private String string; public Banner(String string) { this.string = string; } public void showWithParen() { //文字列を()で括る System.out.println("(" + string + ")"); } public void showWithAster() { //文字列を**で括る System.out.println("*" + string + "*"); } }
Printクラス
- 必要とされているものを表します
public abstract class Print { public abstract void printWeak(); public abstract void printStrong(); }
PrintBannerクラス
- 提供されているものと、必要なものの間を埋めるアダプターの役割を担います
- bannerフィールドでBannerクラスのインスタンスを保持します
- PrintBannerクラスのコンストラクタで生成しています
- printWeak、printStringメソッドではbannerフィールドを介してshowWithPare、showWithAsterメソッドを呼びます
- 継承するのではなく、フィールド経由で呼び出している、ということです
- printWeakを実行する時に、自分で処理するのではなく、BannerインスタンスのshowWithParenメソッドに委譲しています
public class PrintBanner extends Print { private Banner banner; public PrintBanner(String string) { this.banner = new Banner(string); } public void printWeak() { banner.showWithParen(); } public void printStrong() { banner.showWithAster(); } }
Mainクラスで動作確認
- アダプターの役割であるPrintBannerクラスを呼んで文字列を()で括る(=弱く)、**で括る(=強く)操作を行います
- ここで、
Print p = new PrintBanner("Hello");
のようにPrintBannerのインスタンスをPrintインターフェース型に代入しています - つまり、PrintクラスのprintWeakとprintStrongを使ってプログラミングをしています
public class Main { public static void main(String[] args) { Print p = new PrintBanner("Hello"); p.printWeak(); p.printStrong(); } }
Adaoterパターン(委譲)のメリット
- 既存のクラスに一皮被せて、必要とするクラスを作ることで、必要なメソッドを素早く作ることができます
- 既存のクラスを修正することなく、必要なクラスを作ることで、再利用し易く、バージョンの互換性を高めることができます
- 既存のクラスにはバグがないと分かっていれば、Adapterのデバッグに集中して行えば良いため、開発や改修がし易くなります
今日のポイント
- Adaoterパターン(Wrapperパターン)は、"すでに提供されているもの"がそのまま使えない時に、"必要なもの"とのずれを埋めて、利用できるようにするためのデザインパターンです
- クラスによるAdaoterパターン(継承)と、インスタンスによるAdapterパターン(委譲)があります
- 委譲とは、あるメソッドの処理を他のインスタンスのメソッドに任せることです
- アダプターのフィールドで、提供されている (Bannerクラス) インスタンスを保持することで、提供されているメソッドをフィールド経由で呼びます
- アダプターのメソッドを実行する時に、自分で処理するのではなく、提供されているメソッドに処理を委譲しています
- クラスによるAdapterパターンでは、Adapterは継承を使ってAdapteeを利用します
- インスタンスによるAdapterパターンでは、委譲を使ってAdapteeを利用します
本日もお疲れ様です😊