10. デザインパターン(1/x)

テーマとキーワード:

デザインパターン

構造に関するパターン Composite, (Adapter, , Decorator)

振る舞いに関するパターン Command, Iterator, Memento, (Strategy)

オブジェクト指向とクラス図(UML) 参照

デザインパターン GoFの提唱したデザインパターン。パターンのカタログ。良いコードの作成パターンに名前を付けて概念共有の促進を目指したもの。

本講義では、生成に関するパターン、構造に関するパターン、振る舞いに関するパターン から幾つか代表的なものを取り上げ、Javaのプログラム課題として取り組む。

デザインパターン導入のメリット

・コード修正時のコードの複雑化を抑える。

if や for など制御構造や変数を駆使した個別のプログラムが必要な量が減る

・よく用いられているパターンを知ることでデザインパターン導入のコードの挙動を把握しやすくなる。

・よく用いられているパターンをコードに導入することで、他のデザインパターンを使用したコードとスムーズに結合できる。※言語標準のライブラリーなど。

デザインパターン導入のデメリット

・クラスが増える

・クラス間の依存が増える

既存のコードの改良、もしくはコードを継続的に運用して修正・変更が加わることが導入の前提。

デザインパターン導入で増えたコードによる複雑化以上のメリットがない場合もある。

・コードの動作を捉えにくい

どのデザインパターンを使っているか知らないと、導入したクラスの役割や効果を理解しにくい。

関数型プログラミング と オブジェクト指向

関数型のプログラミング言語や、それ由来の言語仕様を導入したモダンなプログラム言語では、デザインパターンのうち幾つかは特に意識してコードを作成しなくても、自然な形でコードを作成できるようになっている。

型推論や関数(クロージャ)を使用したプログラミングについて、第12回、第13回で軽く触れる予定。

Commandパターン

Mementoパターン

メメント・モリ

参考 Command と Memento パターンを利用した お絵かきアプレットの例)

2013年応用プログラミングI(旧カリキュラム)提出課題1311092

オブジェクト指向やめましょう http://www.slideshare.net/nowokay/ss-41730024

紹介:

Visitorパターンで遊んでみたよ http://codezine.jp/article/detail/6829

GoFのデザインパターンについて、短文で要点解説、参考資料への豊富なリンクあり GoFの23のデザインパターンを,Javaで活用するための一覧表 (パターンごとの要約コメント付き)

Javaとデザインパターンに関する近年の動向:

Java SE 8 時代のデザインパターン考察

GoF の古典的な23個のデザインパターンに対して、 Java8 以降のコードで重要性が低くなるパターンについて考察されている。

※ interface のデフォルト実装と ラムダ(lambda)記法の導入が大きく影響している。

※※ Java 8 新機能つまみぐい Java8のラムダ記法についてはこちらの記事に簡潔な解説がある。

関数型の考え方: 関数型のデザイン・パターン、第 1 回

OOPのデザインパターンと関数型のデザインパターンを比較しながら、関数型プログラミング言語のコード作成について解説されている。JavaVMで動く関数型言語 Scala や同じくJVMで動くスクリプト言語Groovyを用い、解説している。

ラムダ式でCommandパターンで書かれたコードをシンプルにする ~ ラムダ式使いへの第一歩

その他:

ゲームプログラミングとデザインパターン http://marupeke296.com/DP_main.html

2018演習

プログラミング教育環境 Scratch の様に、キャラクターに画像・サウンドを登録して、画面上で複数のキャラクターのアニメーションを制御するアプリを作成する。

アプリ作成の過程で遭遇するデザインパターン:

インタラクティブに画面上のキャラクターの操作を行うUIを作成 Compositeパターン、Observerパターン

オーディオクリップとイメージオブジェクトの生成 Factory Methodパターン

キャラクターに階層的に別のキャラクターを追加してグループとして扱う Composite パターン

キャラクターの操作と操作のUndo機能 Commandパターン

複数のキャラクターの処理 Iteratorパターン

画面の状況のスナップショットを記録 Memento パターン(Save と Load 機能)

準備:

プロジェクト名 java5を作成する。

L10 クラスを作成する。

このページに記載したコードをコピペして修正していく。

※パッケージは指定しない java5 と入力済みの箇所を削除して デフォルトパッケージ を利用することにする。

JavaによるJFrame(ウィンドウ)アプリケーションの作成:

参照サイト: https://www.javadrive.jp/tutorial/jframe/index6.html

基本形:

import javax.swing.JFrame;

class L10 {

public static void main(String args[]) {

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setVisible(true); // ウインドウを表示状態にする

}

}

ボタンを追加する&ウインドウを閉じるボタンでアプリを終了する様に設定する。

import java.awt.BorderLayout;

import javax.swing.JButton;

import javax.swing.JFrame;

class L10 {

public static void main(String args[]) {

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定

JButton btn = new JButton("ボタン1"); // ボタンオブジェクトの作成と追加

add(btn, BorderLayout.NORTH); // オブジェクトをウインドウの上部に追加

setVisible(true); // ウインドウを表示状態にする

}

}

ボタンをウインドウの上端に複数配置する。

import java.awt.BorderLayout;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JPanel;

class L10 {

public static void main(String args[]) {

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定

JPanel bp = new JPanel(); // コンポーネント追加用パネル(コンテナ)

JButton btn1 = new JButton("ボタン1"); // ボタンオブジェクトの作成と追加

bp.add(btn1); // オブジェクトをパネルの上部に追加

JButton btn2 = new JButton("ボタン"); // ボタンオブジェクトの作成と追加

bp.add(btn2); // オブジェクトをパネルの上部に追加

JButton btn3 = new JButton("ボタン"); // ボタンオブジェクトの作成と追加

bp.add(btn3); // オブジェクトをパネルの上部に追加

add(bp,BorderLayout.NORTH); // パネルをウインドウの上端に設置

setVisible(true); // ウインドウを表示状態にする

}

}

※このコードでは、Compositeパターンが確認できる。JFrameは、GUIコンポーネントを格納するコンテナであり、コンテナ内に再帰的に別のコンポーネントを配置するためのJPanelコンテナを配置して階層構造でGUIコンポーネントを管理している。

ボタンを押すと、サウンドが再生されるようにアクションを設定する。

再生用のサウンドファイルは、適当に用意して(レポートフォルダ → プログラム演習3(こばし)コピーしてもOK)、L10クラスと同じ階層に配置する。

import java.applet.Applet;

import java.applet.AudioClip;

import java.awt.BorderLayout;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JPanel;

class L10 {

static AudioClip ac1, ac2, ac3;

public static void main(String args[]) {

ac1 = Applet.newAudioClip(L10.class.getResource("gun02.wav")); //音声ファイル読み込み

ac2 = Applet.newAudioClip(L10.class.getResource("nc52380.wav")); //音声ファイル読み込み

ac3 = Applet.newAudioClip(L10.class.getResource("se_ymc01.wav")); //音声ファイル読み込み

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定

JPanel bp = new JPanel(); // コンポーネント追加用パネル(コンテナ)

JButton btn1 = new JButton("ボタン1"); // ボタンオブジェクトの作成と追加

bp.add(btn1); // オブジェクトをパネルの上部に追加

JButton btn2 = new JButton("ボタン2"); // ボタンオブジェクトの作成と追加

bp.add(btn2); // オブジェクトをパネルの上部に追加

JButton btn3 = new JButton("ボタン3"); // ボタンオブジェクトの作成と追加

bp.add(btn3); // オブジェクトをパネルの上部に追加

btn1.addActionListener(event -> { L10.ac1.play(); }); //音声ファイル再生

btn2.addActionListener(event -> { L10.ac2.play(); });

btn3.addActionListener(event -> { L10.ac3.play(); });

add(bp,BorderLayout.NORTH); // パネルをウインドウの上端に設置

setVisible(true); // ウインドウを表示状態にする

}

}

※上記のコードで、L10.class の部分をみると L10 のclass 定義には class フィールドがないので一見バグに見える。

Javaの全クラスの祖先となる Object クラスに class フィールドが存在するので、L10.class は Objectクラスの class フィールドを示している。

Javaの全クラスは自動的に Objectクラスのサブクラスとなる。

※このコードでは、Factory Methodパターンが確認できる。オーディオクリップ用のオブジェクトの生成の部分。newAudioClip メソッドでオブジェクト生成

※このコードでは、Observerパターンが確認できる。ボタンを押したときのアクションのコードを、イベントの発生を監視するオブジェクトに登録している。

参照サイト: https://www.javadrive.jp/tutorial/jframe/index6.html では、古いスタイルのコードが例示されている。

他のページに暗黙のクラス(無名クラス)を利用した、新しいスタイルの解説がある。

旧方式に比べ、オブザーバー用のオブジェクトを(ActionPerformedインターフェースを実装して)class定義する必要がなくなった。

参照:https://www.javadrive.jp/tutorial/event/index1.html

上記のコードのイベント処理はさらに新しい Java8 で導入された関数インターフェース(ラムダ式)を利用して書かれている。

event -> { L10.ac1.play(); }

入力は イベントオブジェクト1つ

処理結果は void

のコードであり、関数インターフェースの条件を満たしている。

関数インターフェースでは、引数 event の型やイベント処理用のメソッド名は書かずに省略できる。

event -> { L10.ac1.play(); }

の部分で、 event が関数インターフェースの引数(型の指定は省略)で、{ } の中がイベント処理用のコード。

{ } の内部には for や if で利用するコードのブロック同様に、コードを複数記述可能。

for や if のコードブロックと異なり、関数インターフェースのブロックはコードに登場した部分で即時実行されずに遅延評価(必要に応じて実行。場合によっては繰り返し実行)される。

参照:https://qiita.com/oohira/items/9c13f92815266cc5112c

JFrameの中央部分にJPanelを配置する。

import java.applet.Applet;

import java.applet.AudioClip;

import java.awt.BorderLayout;

import java.awt.Color;

import java.awt.Graphics;

import java.awt.Graphics2D;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JPanel;

class L10 {

static AudioClip ac1, ac2, ac3;

public static void main(String args[]) {

ac1 = Applet.newAudioClip(L10.class.getResource("gun02.wav")); // 音声ファイル読み込み

ac2 = Applet.newAudioClip(L10.class.getResource("nc52380.wav")); // 音声ファイル読み込み

ac3 = Applet.newAudioClip(L10.class.getResource("se_ymc01.wav")); // 音声ファイル読み込み

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定

JPanel bp = new JPanel(); // コンポーネント追加用パネル(コンテナ)

JButton btn1 = new JButton("ボタン1"); // ボタンオブジェクトの作成と追加

bp.add(btn1); // オブジェクトをパネルの上部に追加

JButton btn2 = new JButton("ボタン2"); // ボタンオブジェクトの作成と追加

bp.add(btn2); // オブジェクトをパネルの上部に追加

JButton btn3 = new JButton("ボタン3"); // ボタンオブジェクトの作成と追加

bp.add(btn3); // オブジェクトをパネルの上部に追加

btn1.addActionListener(event -> {

L10.ac1.play();

}); // 音声ファイル再生

btn2.addActionListener(event -> {

L10.ac2.play();

});

btn3.addActionListener(event -> {

L10.ac3.play();

});

add(bp, BorderLayout.NORTH); // パネルをウインドウの上端に設置


add(new JPanel() {

@Override

protected void paintComponent(Graphics g) {

super.paintComponent(g);

Graphics2D g2 = (Graphics2D) g.create();

g2.setColor(new Color(255,0,0));

g2.fillRect(0, 0, getWidth(), getHeight());

g2.dispose();

}

});

setVisible(true); // ウインドウを表示状態にする

}

}

※ここで配置するJPanel は暗黙のクラス定義を利用して、paintComponet メソッド(コンポーネント自身の描画方法。ここでは背景色を描画)をオーバーライドしてカスタマイズしている。

Graphics クラスについて: 参照 https://codezine.jp/article/detail/2543

グラフィックスコンテキスト g は、描画の指示(コマンド) と 描画に必要な詳細情報 を分離して扱えるようにしたもの。

グラフィックスコンテキストにより、直線を描く(コマンド) と 線の太さやスタイル色 を決めるコマンドを独立して提供できる。

デフォルトの線やスタイルを提供できる。

グラフィクスコンテキストは、描画対象(イメージデータ、パネル)から取得して利用する。

描画対象に描画するためのツールがグラフィクスコンテキストと考えてもよい。

画像を表示するコードを追加する。 参照: http://maedakobe.rw.xsi.jp/java2/image.htm

import java.applet.Applet;

import java.applet.AudioClip;

import java.awt.BorderLayout;

import java.awt.Color;

import java.awt.Graphics;

import java.awt.Graphics2D;

import java.awt.Image;

import java.io.IOException;

import javax.imageio.ImageIO;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JPanel;

class L10 {

static AudioClip ac1, ac2, ac3;

static Image img1, img2;

public static void main(String args[]) {

ac1 = Applet.newAudioClip(L10.class.getResource("gun02.wav")); // 音声ファイル読み込み

ac2 = Applet.newAudioClip(L10.class.getResource("nc52380.wav")); // 音声ファイル読み込み

ac3 = Applet.newAudioClip(L10.class.getResource("se_ymc01.wav")); // 音声ファイル読み込み

try {

img1 = ImageIO.read(L10.class.getResource("kohashi.jpg")); // 画像ファイル読み込み

img2 = ImageIO.read(L10.class.getResource("photo1-2.jpg")); // 画像ファイル読み込み

} catch (IOException e) {

e.printStackTrace();

}

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定

JPanel bp = new JPanel(); // コンポーネント追加用パネル(コンテナ)

JButton btn1 = new JButton("ボタン1"); // ボタンオブジェクトの作成と追加

bp.add(btn1); // オブジェクトをパネルの上部に追加

JButton btn2 = new JButton("ボタン2"); // ボタンオブジェクトの作成と追加

bp.add(btn2); // オブジェクトをパネルの上部に追加

JButton btn3 = new JButton("ボタン3"); // ボタンオブジェクトの作成と追加

bp.add(btn3); // オブジェクトをパネルの上部に追加

btn1.addActionListener(event -> {

L10.ac1.play();

}); // 音声ファイル再生

btn2.addActionListener(event -> {

L10.ac2.play();

});

btn3.addActionListener(event -> {

L10.ac3.play();

});

add(bp, BorderLayout.NORTH); // パネルをウインドウの上端に設置

add(new JPanel() {

@Override

protected void paintComponent(Graphics g) {

super.paintComponent(g);

Graphics2D g2 = (Graphics2D) g.create();

g2.setColor(new Color(255,0,0));

g2.fillRect(0, 0, getWidth(), getHeight());

g2.drawImage(L10.img1, 0, 0, this);

g2.drawImage(L10.img2, 300, 300, 200, 100, this);

g2.dispose();

}

});

setVisible(true); // ウインドウを表示状態にする

}

}

※ここで用いた画像描画メソッド drawImage は、

画像のオリジナルのサイズで指定した (x, y) 座標 (0, 0) に配置する方法と、

(x, y) 座標 (300, 300) に配置して画像の幅を 200 に、高さを 100 に変更して表示する方法、

の2通りで示した。

パネルに画像表示用のパネル DrawPanel を追加する。

画像を切り替えるボタンを作成する。

ボタン4で画像1を表示

ボタン5で画像2を表示

上のコードで画像を表示していたパネルは削除する。

import java.applet.Applet;

import java.applet.AudioClip;

import java.awt.BorderLayout;

import java.awt.Color;

import java.awt.Graphics;

import java.awt.Graphics2D;

import java.awt.Image;

import java.io.IOException;

import javax.imageio.ImageIO;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JPanel;

class L10 {

static AudioClip ac1, ac2, ac3;

static Image img1, img2;

public static void main(String args[]) {

ac1 = Applet.newAudioClip(L10.class.getResource("gun02.wav")); // 音声ファイル読み込み

ac2 = Applet.newAudioClip(L10.class.getResource("nc52380.wav")); // 音声ファイル読み込み

ac3 = Applet.newAudioClip(L10.class.getResource("se_ymc01.wav")); // 音声ファイル読み込み

try {

img1 = ImageIO.read(L10.class.getResource("kohashi.jpg")); // 画像ファイル読み込み

img2 = ImageIO.read(L10.class.getResource("photo1-2.jpg")); // 画像ファイル読み込み

} catch (IOException e) {

e.printStackTrace();

}

MyFrame myframe = new MyFrame();

}

}

class MyFrame extends JFrame {

Image img;

DrawPanel drawpanel;

MyFrame() {

setTitle("課題10"); // ウインドウのタイトル設定

setSize(800, 600); // ウインドウのサイズ

setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定

JPanel bp = new JPanel(); // コンポーネント追加用パネル(コンテナ)

JButton btn1 = new JButton("ボタン1"); // ボタンオブジェクトの作成と追加

bp.add(btn1); // オブジェクトをパネルの上部に追加

JButton btn2 = new JButton("ボタン2"); // ボタンオブジェクトの作成と追加

bp.add(btn2); // オブジェクトをパネルの上部に追加

JButton btn3 = new JButton("ボタン3"); // ボタンオブジェクトの作成と追加

bp.add(btn3); // オブジェクトをパネルの上部に追加

btn1.addActionListener(event -> {

L10.ac1.play();

}); // 音声ファイル再生

btn2.addActionListener(event -> {

L10.ac2.play();

});

btn3.addActionListener(event -> {

L10.ac3.play();

});

bp.add(new JButton("ボタン4") {

{ //初期化ブロックでボタンにコードを登録

addActionListener(event -> {

img = L10.img1;

drawpanel.repaint();

});

}

});

bp.add(new JButton("ボタン5") {

{

addActionListener(event -> {

img = L10.img2;

drawpanel.repaint();

});

}

});

add(bp, BorderLayout.NORTH); // パネルをウインドウの上端に設置

drawpanel = new DrawPanel();

add(drawpanel);

// add(new JPanel() {

// @Override

// protected void paintComponent(Graphics g) {

// super.paintComponent(g);

// Graphics2D g2 = (Graphics2D) g.create();

// g2.setColor(new Color(255,0,0));

// g2.fillRect(0, 0, getWidth(), getHeight());

//

// g2.drawImage(L10.img1, 0, 0, this);

// g2.drawImage(L10.img2, 300, 300, 200, 100, this);

//

// g2.dispose();

// }

// });

setVisible(true); // ウインドウを表示状態にする

}

class DrawPanel extends JPanel {

@Override

protected void paintComponent(Graphics g) {

super.paintComponent(g);

Graphics2D g2 = (Graphics2D) g.create();

g2.setColor(new Color(255, 0, 0));

g2.fillRect(0, 0, getWidth(), getHeight());

g2.drawImage(img, 0, 0, this);

g2.dispose();

}

}

}

※ボタン4とボタン5の作成では、JButtonオブジェクトを生成して変数に格納せずに addメソッドの引数として直接使用している。

さらに、初期化ブロックを使用し、ボタンのインスタンス生成時にイベントリスナーを登録している。初期化ブロックのコードはオブジェクトの生成時に実行される。static な初期化ブロックなら、クラスのロード時に実行される(JavaのClassLoader参照)。

このコードではボタンを押すと img の画像オブジェクトが切り替わる。再描画をパネルに対して指示して、画面を更新する必要がある。

※ボタンとパネルを連動させるために、パネルに名前が必要となる。

暗黙のクラスで生成していたJPanelを、L10クラスの内部専用の インナークラス DrawPanel として定義し、インスタンス化したものをフレームに追加するようにコードを変更した。

今回はここまで。続きは次回。

今回実装できた項目:

インタラクティブに画面上のキャラクターの操作を行うUIを作成 Compositeパターン、Observerパターン

オーディオクリップとイメージオブジェクトの生成 Factory Methodパターン

次回実装する項目:

キャラクターに階層的に別のキャラクターを追加してグループとして扱う Composite パターン

キャラクターの操作と操作のUndo機能 Commandパターン

複数のキャラクターの処理 Iteratorパターン

画面の状況のスナップショットを記録 Memento パターン(Save と Load 機能)

おまけ:

マウスでJPanelに描画するコードの例)

インナークラス DrawPanel に以下の修正を加える。

class DrawPanel extends JPanel {

// オフスクリーンイメージ:マウスで描きこむためのメモリ(画像データ)

Image offimg = new BufferedImage(500, 500, BufferedImage.TYPE_4BYTE_ABGR);

// オフスクリーン用のグラフィックスコンテキスト

Graphics2D offg = (Graphics2D)offimg.getGraphics();

DrawPanel() {

addMouseMotionListener(new MouseAdapter() {

@Override

public void mouseDragged(MouseEvent e) {

System.out.println(e);

offg.setColor(Color.BLUE);

offg.fillOval(e.getX(), e.getY(), 10, 10);

repaint();

}

});

}

@Override

protected void paintComponent(Graphics g) {

super.paintComponent(g);

Graphics2D g2 = (Graphics2D) g.create();

g2.setColor(new Color(255, 0, 0));

g2.fillRect(0, 0, getWidth(), getHeight());

// ファイルの画像をマウスの描画の上に描く

//Graphics2D offg = (Graphics2D)offimg.getGraphics();

//offg.drawImage(img, 0, 0, this);

// ファイルの画像の上にマウスの描画を描く

g2.drawImage(img, 0, 0, this);

// マウスの描きこみを描画する

g2.drawImage(offimg,0,0, this);

g2.dispose();

}

}