mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】Interpreter パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Interpreterパターンについて

Interpreterパターンとは

  • Interpreterパターンは、何らかの形式で書かれたファイルの中身を、「通訳」の役目を果たすプログラムで解析・表現するパターンです
  • プログラムが解決しようとしている問題を簡単なミニ言語で表現し、ミニ言語で書かれたミニプログラムで表現します
    • ミニムログラム単体では動かないので、Java言語で通訳する(interpreter) 役目が必要です
    • 通訳は、ミニ言語を理解し、ミニプログラムを解釈、実行します
  • 問題が変更した際には、ミニプログラムを書き換える事で対処します
  • こうする事で、Java言語で書かれたコードの修正が不要になります

ミニ言語の文法

  • 今回の実装例では、ラジコンで車を動かす言語を考えます
  • プログラムの開始と終了がわかるように、programとendで挟みます
  • ミニ言語の文法ではBNF記法(Backus-Naur Form)の変形バージョンを使います
<program> ::= program <command list>
<command list> ::= <command>* end
<command> ::= <repeat command> | <primitive command>
<repeat command> ::= repeat <number> <command list>
<primitive command> ::= go | right | left
名前 役割
<program> トークンprogram の後にコマンドの列<command list>が続いたもの
<command list> <command>が0個以上繰り返したあとトークン end が続いたもの
<command> 繰り返しコマンド<repeat command> または基本コマンド <primitive command>のいずれか
<repeat command> トークン repeat のあとに繰り返し回数<number>が続き,さらにコマンドの列<command list>が続いたもの
<primitive command> go または right または left
  • <primitive command>のようにこれ以上展開されない表現を、"terminal expression" と言います

サンプルプログラム

  • Interpreterパターンを使って、テキストファイルに書かれた、先ほどのミニ言語の構文を解析する例を取り上げます
  • 各クラスの役割は以下のようになっています
名前 役割
Node(AbstractException) 構文木の「ノード」となるクラス
ProgramNode(NonterminalExpression) <program>に対応するクラス
CommandListNode(NonterminalExpression) <command list>に対応するクラス
CommandNode(NonterminalExpression) <command>に対応するクラス
RepeatCommandNode(NonterminalExpression) <repeat command>に対応するクラス
PrimitiveCommandNode(TerminalExpression) <primitive command>に対応するクラス
Context 構文解析のための前後関係を表すクラス
ParseException 構文解析中の例外クラス

f:id:mohuNeko:20210103125322p:plain

Nodeクラス

  • 構文木の各部分を構成する最上位のクラスです
  • parseメソッドは構文解析という処理を行うためのメソッドです
public abstract class Node {
    public abstract void parse(Context context) throws ParseException;
}

ProgramNodeクラス

  • Node型のCommandListNodeフィールドに、自分の後に続く<command list>に対応するノードを保持します
  • <command list>に対応したCommandListNodeのインスタンスを生成し、そのインスタンスのparseメソッドを呼びます
// <program> ::= program <command list>
public class ProgramNode extends Node {
    private Node commandListNode;
    public void parse(Context context) throws ParseException {
        context.skipToken("program"); //読み飛ばす
        commandListNode = new CommandListNode(); 
        commandListNode.parse(context);  
    }
    public String toString() {
        return "[program " + commandListNode + "]";
    }
}

CommandListNodeクラス

  • 0回以上繰り返す<command>を保持するためにArrayList型のフィールドlistを持っています
    • この中に<command>に対応したCommandNodeクラスのインスタンスを格納します
  • parseメソッドで、現在注目しているトークン(context.currentToken)がnullなら、ミニプログラムを最後まで読んだことになります
    • 最後のトークンがendなら、endを飛ばしてwhileループを抜けます
    • endでは無かったら、CommandNodeのインスタンスを作ってparseし、CommandListNodeのlistフィールドにaddします
import java.util.ArrayList;

// <command list> ::= <command>* end
public class CommandListNode extends Node {
    private ArrayList list = new ArrayList();
    public void parse(Context context) throws ParseException {
        while (true) {
            if (context.currentToken() == null) {
                throw new ParseException("Missing 'end'");
            } else if (context.currentToken().equals("end")) {
                context.skipToken("end");
                break;
            } else {
                Node commandNode = new CommandNode();
                commandNode.parse(context);
                list.add(commandNode);
            }
        }
    }
    public String toString() {
        return list.toString();
    }
}

CommandNodeクラス

  • nodeフィールドに、<repeat command>に対応するRepeatCommandNodeクラスのインスタンス<primitive command>に対応するPrimitiveCommandNodeクラスのインスタンスを格納します
// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {
    private Node node;
    public void parse(Context context) throws ParseException {
        if (context.currentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node = new PrimitiveCommandNode();
            node.parse(context);
        }
    }
    public String toString() {
        return node.toString();
    }
}

RepeatCommandNodeクラス

  • RepeatCommandNodeクラスは<repeat command>に対応します
  • 各クラスで以下のように再帰的にparseメソッドを呼び出します
    • RepeatCommandNodeのparseメソッドの中で
    • CommandListNodeのparseメソッドの中で
    • CommandNodeのparseメソッドの中で
    • RepeatCommandNodeのparseメソッドの中で
      • (略)
  • 最終的に、"terminal expression" が来ると、RepeatCommandNodeではなく、PrimitiveCommandNodeを作ります
  • PrimitiveCommandNodeのparseメソッドの中では、他のparseメソッドを呼びません
// <repeat command> ::= repeat <number> <command list>
public class RepeatCommandNode extends Node {
    private int number;
    private Node commandListNode;
    public void parse(Context context) throws ParseException {
        context.skipToken("repeat");
        number = context.currentNumber();
        context.nextToken();
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }
    public String toString() {
        return "[repeat " + number + " " + commandListNode + "]";
    }
}

PrimitiveCommandNodeクラス

  • 他のparseメソッドを呼びません
// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {
    private String name;
    public void parse(Context context) throws ParseException {
        name = context.currentToken();
        context.skipToken(name);
        if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
            throw new ParseException(name + " is undefined");
        }
    }
    public String toString() {
        return name;
    }
}

Contextクラス

  • java.util.StringTokenizer tokenizerを使い、区切り文字で、与えられた文字列をトークンに分割します
import java.util.*;

public class Context {
    private StringTokenizer tokenizer;
    private String currentToken;
    public Context(String text) {
        tokenizer = new StringTokenizer(text);
        nextToken();
    }
    public String nextToken() {
        if (tokenizer.hasMoreTokens()) {
            currentToken = tokenizer.nextToken();
        } else {
            currentToken = null;
        }
        return currentToken;
    }
    public String currentToken() {
        return currentToken;
    }
    public void skipToken(String token) throws ParseException {
        if (!token.equals(currentToken)) {
            throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found.");
        }
        nextToken();
    }
    public int currentNumber() throws ParseException {
        int number = 0;
        try {
            number = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            throw new ParseException("Warning: " + e);
        }
        return number;
    }
}

ParseException クラス

public class ParseException extends Exception {
    public ParseException(String msg) {
        super(msg);
    }
}

Mainクラスで動作確認

  • program.txtのファイルを読み、一行ずつミニプログラムを構文解析し、その結果を文字列で表示します
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("program.txt"));
            String text;
            while ((text = reader.readLine()) != null) { //ミニプログラム
                System.out.println("text = \"" + text + "\"");
                Node node = new ProgramNode(); //構文解析後の表示
                node.parse(new Context(text));
                System.out.println("node = " + node);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • program.txt
program end
program go end
program go right go right go right go right end
program repeat 4 go right end end
program repeat 4 repeat 3 go right go left end right end end
  • 実行結果
text = "program end"
node = [program []]
text = "program go end"
node = [program [go]]
text = "program go right go right go right go right end"
node = [program [go, right, go, right, go, right, go, right]]
text = "program repeat 4 go right end end"
node = [program [[repeat 4 [go, right]]]]
text = "program repeat 4 repeat 3 go right go left end right end end"
node = [program [[repeat 4 [[repeat 3 [go, right, go, left]], right]]]]

Interpreterパターンのメリット

  • Interpreterパターンを利用すると、規則の追加や変更が容易になります
    • Interpreterパターンの特徴の1つは、"1つの規則を1つのクラスで表す" 事です
    • つまり、新しい規則を追加・修正する場合は AbstractException(Node)クラスのサブクラスを追加・修正するだけで良くなります

今日のポイント

  • Interpreterパターンは、何らかの形式で書かれたファイルの中身を、「通訳」の役目を果たすプログラムで解析・表現するパターンです
  • プログラムが解決しようとしている問題を簡単なミニ言語で表現し、ミニ言語で書かれたミニプログラムで表現します
  • 問題が変更した際には、ミニプログラムを書き換える事で対処する事で、Java言語で書かれたコードの修正が不要になります
  • Interpreterパターンを利用して、"1つの規則を1つのクラスで表す" 事で、新しい規則を追加・修正する場合は AbstractException(Node)クラスのサブクラスを追加・修正するだけで良くなります

 本日もお疲れ様です😊  

 

 

【Java】Proxy パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Proxy パターンについて

Proxy パターンとは

  • Proxyパターンは、忙しくて仕事ができない"本人"オブジェクトの代わりに、"代理人"オブジェクトが一部の仕事をこなすパターンです
  • オブジェクト指向では "本人" も "代理人"もオブジェクトとなります
  • 代理人ができる仕事の範囲を超える仕事がきたら、代理人が本人に相談します

サンプルプログラム

  • Proxy パターンを使って、名前付きのプリンタで、画面に文字列を表示する例を取り上げます
  • 各クラスの役割は以下のようになっています
名前 役割
Printable(Subject) PrinterとPrinterProxy共通のインターフェース
PrinterProxy(Proxy) 名前つきのプリンタを表すクラス(代理人)
Printer(RealSubject) 名前つきのプリンタを表すクラス(本人)

f:id:mohuNeko:20210102164846p:plain

Printerクラス

  • コンストラクタで、ダミーの重い仕事を行なっています
public class Printer implements Printable {
    private String name;
    public Printer() {
        heavyJob("Printerのインスタンスを生成中");
    }
    public Printer(String name) {                   // コンストラクタ
        this.name = name;
        heavyJob("Printerのインスタンス(" + name + ")を生成中");
    }
    public void setPrinterName(String name) {       // 名前の設定
        this.name = name;
    }
    public String getPrinterName() {                // 名前の取得
        return name;
    }
    public void print(String string) {              // 名前付きで表示
        System.out.println("=== " + name + " ===");
        System.out.println(string);
    }
    private void heavyJob(String msg) {             // 重い作業(のつもり)
        System.out.print(msg);
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.print(".");
        }
        System.out.println("完了。");
    }
}

Printableクラス

  • PrinterとPrinterProxyを同一視するためのインターフェースです
public interface Printable {
    public abstract void setPrinterName(String name);   // 名前の設定
    public abstract String getPrinterName();            // 名前の取得
    public abstract void print(String string);          // 文字列表示(プリントアウト)
}

PrinterProxyクラス

  • 代理人の役割を果たします
  • setPrinterNameで新しく名前を設定します
  • もしrealがnullでないなら(本人がすでに作られているなら)、本人の名前もnameに設定します
  • printメソッドは、代理人の守備範囲外なので、realizeメソッドで、本人(Printerクラスのインスタンス)を生成し、real.printを呼びます
    • 代理人が本人に、処理を委譲しているということになります
    • 本人が生成されているかどうかは、PrinterProxyの利用者には全くわかりません
  • PrinterクラスはPrinterProxyの存在を知りません
    • 自分が直接呼ばれているのか、PrinterProxy経由で呼ばれているのか、知らなくていいという事です
public class PrinterProxy implements Printable {
    private String name;            // 名前
    private Printer real;           // 「本人」
    public PrinterProxy() {
    }
    public PrinterProxy(String name) {      // コンストラクタ
        this.name = name;
    }
    public synchronized void setPrinterName(String name) {  // 名前の設定
        if (real != null) {
            real.setPrinterName(name);  // 「本人」にも設定する
        }
        this.name = name;
    }
    public String getPrinterName() {    // 名前の取得
        return name;
    }
    public void print(String string) {  // 表示
        realize();
        real.print(string);
    }
    private synchronized void realize() {   // 「本人」を生成
        if (real == null) {
            real = new Printer(name);
        }
    }
}

Mainクラスで動作確認

  • PrinterProxy経由でPrinterを利用するクラスです
  • 最初にPrinterProxyを生成し、getPrinterNameで名前を表示し、setPrinterNameで名前を設定します
  • Printer(本人)のインスタンスは、printメソッドを呼び出してから、生成されています
public class Main {
    public static void main(String[] args) {
        Printable p = new PrinterProxy("Alice");
        System.out.println("名前は現在" + p.getPrinterName() + "です。");
        p.setPrinterName("Bob");
        System.out.println("名前は現在" + p.getPrinterName() + "です。");
        p.print("Hello, world.");
    }
}
  • 実行結果
名前は現在Aliceです。
名前は現在Bobです。
Printerのインスタンス(Bob)を生成中.....完了。
=== Bob ===
Hello, world.

Proxy パターンのメリット

  • Proxyパターンでは、Proxyが代理人となって、できるだけ処理を肩代わりします
    • 上記の例では、Proxy役を使うことによって、実際にprintするときまで、重い処理 (インスタンス生成) を遅らせることができました :* 初期化に時間がかかる機能が多く存在するようなシステムの場合、起動時に利用しない機能まで全て初期化してしまうと、アプリケーションの起動に時間がかかります
  • PrinterProxyクラスとPrinterクラスに分けることで、プログラムの部品化が進み、個別に機能を加えることができます(分割統治)
    • PrinterProxyクラスの実装を変えたら、何を代理人が処理し、何を本人が処理するかを変更することができます

今日のポイント

  • Proxyパターンは、忙しくて仕事ができない"本人"オブジェクトの代わりに、"代理人"オブジェクトが一部の仕事をこなすパターンです
  • Subjectのおかげで、ClientはProxyとRealSubjectの違いを意識する必要がありません(Proxyが透過的である)
  • RealSubjectはProxyの存在を知る必要がありません
  • RealSubjecと Proxyに分けることで、プログラムの部品化が進み、個別に機能を加えることができます(分割統治)

 本日もお疲れ様です😊  

 

 

【Java】Flyweight パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Flyweightパターンについて

Flyweightパターンとは

  • Flyweightパターンは、インスタンスをできるだけ共有させて、メモリ使用量を軽くするパターンです
    • Flyweightという英単語は、"フライ級"という意味で、ボクシングで最も体重が軽い階級のことです🥊
    • Flyweightパターンにおいての重さとは、メモリ使用量のことです

サンプルプログラム

  • Flyweightパターンを使って、ファイルを読んで、大きな文字(重いインスタンスを作る)を表現するクラスの処理について取り上げます
  • 各クラスの役割は以下のようになっています
名前 役割
BigChar(Flyweight 大きな文字を表すクラス
BigCharFactory(FlyweightFactory) BigCharのインスタンスを共有しながら生成するクラス
BigString(Client) BigCharを集めて作った「大きな文字列」を表すクラス

f:id:mohuNeko:20210102144529p:plain

BigCharクラス

  • コンストラクタで、引数で与えられた文字の大きな文字を作成します
  • 文字列は、fontdataフィールドに格納します
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BigChar {
    // 文字の名前
    private char charname;
    // 大きな文字を表現する文字列('#' '.' '\n'の列)
    private String fontdata;
    // コンストラクタ
    public BigChar(char charname) {
        this.charname = charname;
        try {
            BufferedReader reader = new BufferedReader(
                new FileReader("big" + charname + ".txt")
            );
            String line;
            StringBuffer buf = new StringBuffer();
            while ((line = reader.readLine()) != null) {
                buf.append(line);
                buf.append("\n");
            }
            reader.close();
            this.fontdata = buf.toString();
        } catch (IOException e) {
            this.fontdata = charname + "?";
        }
    }
    // 大きな文字を表示する
    public void print() {
        System.out.print(fontdata);
    }
}

BigCharFactoryクラス

import java.util.HashMap;

public class BigCharFactory {
    // すでに作ったBigCharのインスタンスを管理
    private HashMap pool = new HashMap();
    // Singletonパターン
    private static BigCharFactory singleton = new BigCharFactory();
    // コンストラクタ
    private BigCharFactory() {
    }
    // 唯一のインスタンスを得る
    public static BigCharFactory getInstance() {
        return singleton;
    }
    // BigCharのインスタンス生成(共有)
    public synchronized BigChar getBigChar(char charname) {
        BigChar bc = (BigChar)pool.get("" + charname);  //文字に対応するいインスタンスがあるか確認
        if (bc == null) {
            bc = new BigChar(charname); // ここでBigCharのインスタンスを生成
            pool.put("" + charname, bc); //poolに登録
        }
        return bc;
    }
}

BigStringクラス

  • BigCharを集めて作った「大きな文字列」を表すクラスです
  • 新しくインスタンスをnewするのではなく、bigchars[i] = factory.getBigChar(string.charAt(i))で、インスタンスを保持しています
  • BigCharFactoryを使うことで、同じ文字でできるBigCharのインスタンスを共有します
    • 例えば、1212123というBigCharsの場合、1,2,3のBigCharインスタンスを共有して、使いまわします
public class BigString {
    // 「大きな文字」の配列
    private BigChar[] bigchars;
    // コンストラクタ
    public BigString(String string) {
        bigchars = new BigChar[string.length()];
        BigCharFactory factory = BigCharFactory.getInstance();
        for (int i = 0; i < bigchars.length; i++) {
            bigchars[i] = factory.getBigChar(string.charAt(i));
        }
    }
    // 表示
    public void print() {
        for (int i = 0; i < bigchars.length; i++) {
            bigchars[i].print();
        }
    }
}

Mainクラスで動作確認

  • 引数で与えられた文字列を元に、BigStringのインスタンスを作り、表示します
public class Main {
    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Usage: java Main digits");
            System.out.println("Example: java Main 1212123");
            System.exit(0);
        }
        BigString bs = new BigString(args[0]);
        bs.print();
    }
}

Flyweightパターンのメリット

  • インスタンスを共有すると、毎回newする必要がなく、メモリ使用量が少なくなります
  • 一般的に言えば、インスタンスを共有すると、インスタンスを生成するのに必要なリソースの量を減らすことができます。
  • 時間もリソースの一種になります。Flyweightパターンを使ってインスタンスを共有すれば、インスタンスをnewする数を減らすことができるので、プログラムのスピードを上げることができます。

Flyweightパターンの特徴

  • 共有しているインスタンスを変更すると、それを使っている箇所全部に影響が及ぶので、共有すべき情報と、共有しない情報を区別する必要があります
  • 共有させる情報を、intrindicな情報と言います
    • どんな状況下でも、変わることのない情報や、状態に依存しない情報です
    • BigChar のフォントデータは変わることがないので、intrinsicな情報です
  • 共有すべきでない情報を、extrinsicな情報と言います
    • 状況によって変化する情報、状態依存する情報です
    • BigCharのインスタンスが、BigStringの何番目か、という情報は extrinsicな情報になります
  • また、管理されているインスタンスは、ガベージコレクションされません

今日のポイント

 本日もお疲れ様です😊  

 

 

【Java】State パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Stateパターンについて

Stateパターンとは

  • Stateパターンは、状態をクラスとして表現し、クラスを切り替えることによって「状態の変化」を表すパターンです。
  • 状態をクラスとして表せば、クラスを切り替えることで、「状態の変化」が表現でき、新しい状態を追加しやすくなります

サンプルプログラム

  • Stateパターンを使って、昼、夜の状態によって警備の状態が変わる、金庫管理のプログラム例を取り上げます
  • 出来事のメソッドの中のif文で、状態(昼間/夜間)を調べ、処理を変更するではなく、昼間/夜間、という状態を表すクラスを用意して、出来事に応じたメソッドを定義します
    • こうすることで、状態確認のためのif文は不要になります
  • 各クラスの役割は以下のようになっています
名前 役割
Context 金庫の状態変化を管理し、警備センターとの連絡をとるインターフェース
SafeFrame Contextインターフェース実装クラス。ボタンや画面表示などのUIを持つ。
State 金庫の状態を表すインターフェース
DayState(ConcreteState) Stateインターフェース実装クラス(昼間の状態)
NightStateクラス(ConcreteState) Stateインターフェース実装クラス(夜間の状態)

f:id:mohuNeko:20210102122553p:plain

Stateインターフェース

  • 金庫の状態を表すインターフェースです
  • 出来事に対応して呼び出されるインターフェースを規定します
    • ここで規定されているメソッドは、状態に応じて処理が変化する、状態依存のメソッドの集合です
public interface State {
    public abstract void doClock(Context context, int hour);    // 時刻設定
    public abstract void doUse(Context context);                // 金庫使用
    public abstract void doAlarm(Context context);              // 非常ベル
    public abstract void doPhone(Context context);              // 通常通話
}

DayStateクラス

  • 昼間の状態を表すクラスです
  • 状態を表すクラスでは、メモリを節約するために、一個ずつしかインスタンスを生成しないようにします(Singletonパターン
  • doClockメソッドの中で、時刻に応じて状態の遷移が起こります
public class DayState implements State {
    private static DayState singleton = new DayState();
    private DayState() {                                // コンストラクタはprivate
    }
    public static State getInstance() {                 // 唯一のインスタンスを得る
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 時刻設定
        if (hour < 9 || 17 <= hour) {
            context.changeState(NightState.getInstance()); //状態遷移
        }
    }
    public void doUse(Context context) {                // 金庫使用
        context.recordLog("金庫使用(昼間)");
    }
    public void doAlarm(Context context) {              // 非常ベル
        context.callSecurityCenter("非常ベル(昼間)");
    }
    public void doPhone(Context context) {              // 通常通話
        context.callSecurityCenter("通常の通話(昼間)");
    }
    public String toString() {                          // 文字列表現
        return "[昼間]";
    }
}

NightStateクラス

  • 夜間の状態を表すクラスです
    • Singletonパターンを使います
public class NightState implements State {
    private static NightState singleton = new NightState();
    private NightState() {                              // コンストラクタはprivate
    }
    public static State getInstance() {                 // 唯一のインスタンスを得る
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 時刻設定
        if (9 <= hour && hour < 17) {
            context.changeState(DayState.getInstance());
        }
    }
    public void doUse(Context context) {                // 金庫使用
        context.callSecurityCenter("非常:夜間の金庫使用!");
    }
    public void doAlarm(Context context) {              // 非常ベル
        context.callSecurityCenter("非常ベル(夜間)");
    }
    public void doPhone(Context context) {              // 通常通話
        context.recordLog("夜間の通話録音");
    }
    public String toString() {                          // 文字列表現
        return "[夜間]";
    }
}

Contextインターフェース

  • 状態を管理したり、警備センターの呼び出しを行います
public interface Context {

    public abstract void setClock(int hour);                // 時刻の設定
    public abstract void changeState(State state);          // 状態変化
    public abstract void callSecurityCenter(String msg);    // 警備センター警備員呼び出し
    public abstract void recordLog(String msg);             // 警備センター記録
}

SafeFrameクラス

  • GUIを使って金庫管理システムを実現します
    • stateフィールドだけ、金庫の現在の情報を表し、昼間の状態で初期化されます
  • ボタンが押されたら、状態を調べることなく、state.doUse(this);を呼びます
  • doClockメソッドでchangeStateを呼び出します
    • this.state = state;で、現在の状態を表しているフィールドに、状態を表すクラスのインスタンスを代入することが、状態遷移に相当します
import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.Button;
import java.awt.TextField;
import java.awt.TextArea;
import java.awt.Panel;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class SafeFrame extends Frame implements ActionListener, Context {
    private TextField textClock = new TextField(60);        // 現在時刻表示
    private TextArea textScreen = new TextArea(10, 60);     // 警備センター出力
    private Button buttonUse = new Button("金庫使用");      // 金庫使用ボタン
    private Button buttonAlarm = new Button("非常ベル");    // 非常ベルボタン
    private Button buttonPhone = new Button("通常通話");    // 通常通話ボタン
    private Button buttonExit = new Button("終了");         // 終了ボタン

    private State state = DayState.getInstance();           // 現在の状態

    // コンストラクタ
    public SafeFrame(String title) {
        super(title);
        setBackground(Color.lightGray);
        setLayout(new BorderLayout());
        // textClockを配置
        add(textClock, BorderLayout.NORTH);
        textClock.setEditable(false);
        // textScreenを配置
        add(textScreen, BorderLayout.CENTER);
        textScreen.setEditable(false);
        // パネルにボタンを格納
        Panel panel = new Panel();
        panel.add(buttonUse);
        panel.add(buttonAlarm);
        panel.add(buttonPhone);
        panel.add(buttonExit);
        // そのパネルを配置
        add(panel, BorderLayout.SOUTH);
        // 表示
        pack();
        show();
        // リスナーの設定
        buttonUse.addActionListener(this);
        buttonAlarm.addActionListener(this);
        buttonPhone.addActionListener(this);
        buttonExit.addActionListener(this);
    }
    // ボタンが押されたらここに来る
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        if (e.getSource() == buttonUse) {           // 金庫使用ボタン
            state.doUse(this);
        } else if (e.getSource() == buttonAlarm) {  // 非常ベルボタン
            state.doAlarm(this);
        } else if (e.getSource() == buttonPhone) {  // 通常通話ボタン
            state.doPhone(this);
        } else if (e.getSource() == buttonExit) {   // 終了ボタン
            System.exit(0);
        } else {
            System.out.println("?");
        }
    }
    // 時刻の設定
    public void setClock(int hour) {
        String clockstring = "現在時刻は";
        if (hour < 10) {
            clockstring += "0" + hour + ":00";
        } else {
            clockstring += hour + ":00";
        }
        System.out.println(clockstring);
        textClock.setText(clockstring);
        state.doClock(this, hour); //doClockでchangeStateを呼び出します
    }
    // 状態変化
    public void changeState(State state) {
        System.out.println(this.state + "から" + state + "へ状態が変化しました。");
        this.state = state; //状態を変化
    }
    // 警備センター警備員呼び出し
    public void callSecurityCenter(String msg) {
        textScreen.append("call! " + msg + "\n");
    }
    // 警備センター記録
    public void recordLog(String msg) {
        textScreen.append("record ... " + msg + "\n");
    }
}

Mainクラスで動作確認

public class Main {
    public static void main(String[] args) {
        SafeFrame frame = new SafeFrame("State Sample");
        while (true) {
            for (int hour = 0; hour < 24; hour++) {
                frame.setClock(hour);   // 時刻の設定
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}

Stateパターンのメリット

  • 分割して統合せよ(divide and conquer)という方針は、複雑で大規模なプログラミングによく登場します。
    • 大きくてややこしい問題を解くときは、小さい問題に分けましょう、という方針です。
  • Stateパターンでは、個々の具体的な状態を、別々のクラスとして表現して、複雑な問題を分割しています
    • DayStateを実装している最中、プログラマは他のクラス(NightState)を意識する必要はなくなります。
  • 状態の数がもっと増えた場合に、Stateパターンは一層強みを発揮します。
  • Stateインターフェースのメソッドは、全て"状態に依存した処理"になっています
  • Stateパターンでは、状態に依存した処理を、以下のように表現しています
    • 抽象メソッドで宣言し、インターフェースにする
    • 具象メソッドで実装し、個々のクラスにする
  • 新しい状態を追加するには、Stateインターフェースを実装したクラスを用意し、必要なメソッドを書けばOKです

今日のポイント

  • Stateパターンは、状態をクラスとして表現し、クラスを切り替えることによって「状態の変化」を表すパターンです。
    • 状態をクラスとして表せば、クラスを切り替えることで、「状態の変化」が表現でき、新しい状態を追加しやすくなります
  • Stateパターンでは、個々の具体的な状態を、別々のクラスとして表現して、複雑な問題を分割しています(divide and conquer)
  • Stateパターンでは、状態に依存した処理を、"抽象メソッドで宣言しインターフェースにして、それを具象メソッドとして実装し個々のクラスにする" ことで、表現しています

 本日もお疲れ様です😊  

 

 

【Java】Memento パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Mementoパターンについて

Mementoパターンとは

サンプルプログラム

  • Mementoパターンを使って、"サイコロの目に応じて、お金やフルーツ獲得する" 例を取り上げます
  • お金が貯まった時点で、Mementoクラスのインスタンスを作って、現在の状態(お金とフルーツ)を保存します
  • お金が減ってきたら、終了しないように、保存していたインスタンスを使って、以前の状態を復元します
  • 各クラスの役割は以下のようになっています
名前 役割
Gamer(Originator) Gameを行う主人公のクラス。Mementoのインスタンスを作る
Memento Gemerの状態を表すクラス
Main(Caretaker) Gameを進行させるクラス。Mementoのインスタンスを保存し、必要に応じてGamerの状態を復元する

f:id:mohuNeko:20210102003531p:plain

Mementoクラス

  • 主人公であるGemerの状態を表すクラスです。
  • money、fruitsは同じパッケージのGamerクラスから自由にアクセスできるようにするために、privateにしません
  • Mementoのコンストラクタにはpublicがついておらず(package private)、同じパッケージのクラス(Gamer)からしか使うことができません
package game;
import java.util.*;

public class Memento {
    int money;                // 所持金
    ArrayList fruits;       // フルーツ
    public int getMoney() {        // 所持金を得る(narrow interface)
        return money;
    }

    Memento(int money) {   // コンストラクタ(wide interface)
        this.money = money;
        this.fruits = new ArrayList();
    }
    void addFruit(String fruit) {   // フルーツを追加する(wide interface)
        fruits.add(fruit);
    }
    List getFruits() {     // フルーツを得る(wide interface)
         return (List)fruits.clone();
    }
}

Gamerクラス

  • Gameを行う主人公のクラスです。Mementoのインスタンスを作成します。
  • betメソッドで、もし主人公が破産していなかったら、サイコロをふって、その目で所持金やフルーツの個を変化させます
  • createMementoメソッドで、現在の情報のスナップショットをとります
    • 現在の状態をもとにMementoインスタンスを一個つくり、現在のGamerのインスタンスの状態を表現します
    • フルーツは美味しいものだけを保存しています
  • restoreMementoメソッドはUndoを行います
    • 与えられたMementoインスタンスを元に、自分の状態を復元します(ケアルガ! )
package game;
import java.util.*;

public class Gamer {
    private int money;                          // 所持金
    private List fruits = new ArrayList();      // フルーツ
    private Random random = new Random();       // 乱数発生器
    private static String[] fruitsname = {      // フルーツ名の表
        "リンゴ", "ぶどう", "バナナ", "みかん",
    };
    public Gamer(int money) {    // コンストラクタ
        this.money = money;
    }
    public int getMoney() {   // 現在の所持金を得る
        return money;
    }
    public void bet() {       // ゲームの進行
        int dice = random.nextInt(6) + 1;           // サイコロを振る
        if (dice == 1) {             // 1:所持金が増える
            money += 100;
            System.out.println("所持金が増えました。");
        } else if (dice == 2) {         // 2:所持金が半分になる
            money /= 2;
            System.out.println("所持金が半分になりました。");
        } else if (dice == 6) {       // 6:フルーツをもらう
            String f = getFruit();
            System.out.println("フルーツ(" + f + ")をもらいました。");
            fruits.add(f);
        } else {           // それ以外:何も起きない
            System.out.println("何も起こりませんでした。");
        }
    }
    public Memento createMemento() {                // スナップショットをとる
        Memento m = new Memento(money);
        Iterator it = fruits.iterator();
        while (it.hasNext()) {
            String f = (String)it.next();
            if (f.startsWith("おいしい")) {         // フルーツはおいしいものだけ保存
                m.addFruit(f);
            }
        }
        return m;
    }
    public void restoreMemento(Memento memento) {   // アンドゥを行う
        this.money = memento.money;
        this.fruits = memento.getFruits();
    }
    public String toString() {                      // 文字列表現
        return "[money = " + money + ", fruits = " + fruits + "]";
    }
    private String getFruit() {                     // フルーツを1個得る
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "おいしい";
        }
        return prefix + fruitsname[random.nextInt(fruitsname.length)];
    }
}

Mainクラスで動作確認

  • ゲームを進行させ、Mementoのインスタンスを保存しておき、必要に応じてGamerの状態を復元します。
  • mementoに、ある時点でのGamerの状態が保存されています
  • 所持金が減ったら、restoreMementoにmementoを与えて、所持金を元に戻します
import game.Memento;
import game.Gamer;

public class Main {
    public static void main(String[] args) {
        Gamer gamer = new Gamer(100);               // 最初の所持金は100
        Memento memento = gamer.createMemento();    // 最初の状態を保存しておく
        for (int i = 0; i < 100; i++) {
            System.out.println("==== " + i);        // 回数表示
            System.out.println("現状:" + gamer);    // 現在の主人公の状態表示

            gamer.bet();    // ゲームを進める

            System.out.println("所持金は" + gamer.getMoney() + "円になりました。");

            // Mementoの取り扱いの決定
            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("    (だいぶ増えたので、現在の状態を保存しておこう)");
                memento = gamer.createMemento();
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("    (だいぶ減ったので、以前の状態に復帰しよう)");
                gamer.restoreMemento(memento);
            }

            // 時間待ち
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("");
        }
    }
}
  • 実行結果
==== 0
現状:[money = 100, fruits = []]
何も起こりませんでした。
所持金は100円になりました。

==== 1
現状:[money = 100, fruits = []]
フルーツ(バナナ)をもらいました。
所持金は100円になりました。

==== 2
現状:[money = 100, fruits = [バナナ]]
所持金が増えました。
所持金は200円になりました。
    (だいぶ増えたので、現在の状態を保存しておこう)

//中略

==== 10
現状:[money = 150, fruits = [バナナ, みかん]]
所持金が半分になりました。
所持金は75円になりました。
    (だいぶ減ったので、以前の状態に復帰しよう)

//中略

==== 98
現状:[money = 650, fruits = [おいしいみかん, おいしいリンゴ, おいしいぶどう, バナナ, おいしいぶどう, おいしいバナナ, おいしいぶどう, おいしいリンゴ, おいしいリンゴ]]
何も起こりませんでした。
所持金は650円になりました。

==== 99
現状:[money = 650, fruits = [おいしいみかん, おいしいリンゴ, おいしいぶどう, バナナ, おいしいぶどう, おいしいバナナ, おいしいぶどう, おいしいリンゴ, おいしいリンゴ]]
所持金が増えました。
所持金は750円になりました。

wide interfaceとnarrow interface

  • Mementoが、二種類のインターフェースを使い分けることで、オブジェクトのカプセル化の崩壊を防ぎます
  • wide interfaceは、"オブジェクトの状態を元に戻すための必要な情報が全て得られるメソッドの集合" です
    • Mementoの内部状態を全部公開してしまうので、使えるのはOriginator(Gamerクラス)のみです
    • OriginatorとMementoは非常に密接な関係にあります
  • narrow interfaceは、外部のCaretaker(Mainクラス)に公開するAPIです
    • 狭い、というのは、"内部状態の操作可能範囲が狭い" という意味です
    • 外部に公開する分、できることに制限をつけます
    • Caretakerは、Mementoの内部情報にアクセスできないので、作ってもらったMementoをブラックボックスとして、丸ごと保存します
    • Mementoのコンストラクタにもアクセスできないので、Mainの中でMementoのインスタンスを生成できません
      • Mementoの状態保存の際には、Gamerクラスを介して新しいインスタンスを生成します

Mementoパターンのメリット

  • Mementoパターンを使うと、undo、redohistory(作業履歴の作成)、snapshot(現在状態の保存) を行うことができます。
  • Caretaker(Mainクラス)とOriginator(Gamerクラス)では、以下のように役割分担を行っています
    • Caretaker では、"どのタイミングでsnapshot を撮るか"、"いつUndoするか"を決め、Mementoを保持します
    • 一方、Originator では、Mementoを作り与えられたMementoを使って自分の状態を戻します
  • 役割分担をしておけば、
    • 複数ステップのUndoを行うように変更したい
    • Undoだけでなく、現在の状態をファイルに保存したい
  • という修正を行いたいときにも、Originator を変更する必要はなくなります。

今日のポイント

  • Mementoパターンは、インスタンスの状態を表す役割を導入して、二種類のインターフェースを使い分けることで、カプセル化の破壊に陥ることなく保存/復元を行うパターンです
  • Mementoパターンを使うと、undo、redohistory(作業履歴の作成)、snapshot(現在状態の保存) を行うことができます。
  • Caretaker(Mainクラス)とOriginator(Gamerクラス)では、Mementoのインターフェースを利用して、役割分担を行うことで、高凝集かつ低結合な設計をすることができます

 本日もお疲れ様です😊  

 

 

【Java】Mediator パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Mediatorパターンについて

Mediatorパターンとは

  • Mediatorパターンは、相談役を通して行動を起こすようにしてもらうパターンです。
  • Mediatorという英単語は"相談役"と考えると分かりやすいです。
  • メンバーが、互いに指示しあっていたら、作業は混乱します。
  • そんなとき、立場の違う相談役を立て、相談役だけから、メンバーへの指示を行うようにします

サンプルプログラム

  • Mediator パターンを使って、ログインダイアログを表示し、名前やパスワードを入力するテキストフィールドや、ボタンの有効/無効状態を、制御する例を取り上げます
  • これらのテキストフィールドやボタンのロジックを各クラスに分散させると、それぞれのオブジェクトは関係し合っているので、お互いをコントロールし合うことになり、大変なことになります
  • このように、多数のオブジェクト間の調整を行う必要があるときに、Mediatoパターンを使います
  • 個々のオブジェクトが互いに通信するのではなく、"相談役"のみと通信し、"相談役"のみに、表示のロジックを記述します
  • 各クラスの役割は以下のようになっています
名前 役割
Mediator 相談役のインターフェースを定めるインターフェース
Colleague メンバのインターフェースを定めるインターフェース
ColleagueButton(ConcreteColleague) Colleagueインターフェースを実装し、ボタンを表すクラス
ColleagueCheckbox(ConcreteColleague) Colleagueインターフェースを実装し、ラジオボタンを表すクラス
ColleagueTextField (ConcreteColleague) Colleagueインターフェースを実装し、テキスト入力を表すクラス
LoginFrame(ConcreteMediator) Mediatorインターフェースを実装し、ログインダイアログを表すクラス

f:id:mohuNeko:20201231171309p:plain

Mediatorインターフェース

  • 相談役を表現するインターフェースです
  • 具体的な相談役(LoginFrame)は、このインターフェースを実装します
  • createColleaguesメソッドで、メンバ(必要なボタンやテキストフィールド)を生成します
  • colleagueChangedメソッドは、メンバから呼ばれるメソッドです。相談役への"相談"に当たります。
public interface Mediator {
    public abstract void createColleagues();  //メンバを生成
    public abstract void colleagueChanged(); //相談役への"相談"
}

Colleagueインターフェース

  • 相談役に相談を持ちかけるメンバを表すインターフェースです
  • 具体的なメンバはこのクラスを実装します
  • setMediatorメソッドは、Mediatorインターフェースを実装したLoginFrameが最初に呼ぶメソッド
  • setColleagueEnabledメソッドは、相談役からの指示に当たります
    • 自分自身を有効/無効に設定するのは、自分ではなく、相談役の判断で決めます
public interface Colleague {
    public abstract void setMediator(Mediator mediator); //相談役を設定
    public abstract void setColleagueEnabled(boolean enabled); //相談役からの指示
}

ColleagueButtonクラス

*ボタンを表すクラスです。Colleagueインターフェースを実装し、LoginFrameクラスと協調動作を行います * setEnabledがtrueなら、ボタンが押せる状態です

import java.awt.Button;

public class ColleagueButton extends Button implements Colleague {
    private Mediator mediator;
    public ColleagueButton(String caption) {
        super(caption);
    }
    public void setMediator(Mediator mediator) {            // Mediatorを保持
        this.mediator = mediator;
    }
    public void setColleagueEnabled(boolean enabled) {      // Mediatorから有効/無効が指示される
        setEnabled(enabled);
    }
}

ColleagueCheckboxクラス

  • チェックボックス(ここではラジオボタン)を表すクラスです。Colleagueインターフェースを実装します。
  • java.awt.event.ItemListenerを実装し、ボタンの状態の変化を、itemStateChangedメソッドでキャッチします
import java.awt.Checkbox;
import java.awt.CheckboxGroup;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;

public class ColleagueCheckbox extends Checkbox implements ItemListener, Colleague {
    private Mediator mediator;
    public ColleagueCheckbox(String caption, CheckboxGroup group, boolean state) {  // コンストラクタ
        super(caption, group, state);
    }
    public void setMediator(Mediator mediator) {            // Mediatorを保持
        this.mediator = mediator;
    }
    public void setColleagueEnabled(boolean enabled) {      // Mediatorから有効/無効が指示される
        setEnabled(enabled);
    }
    public void itemStateChanged(ItemEvent e) {             // 状態が変化したらMediatorに通知
        mediator.colleagueChanged();
    }
}

ColleagueTextFieldクラス

  • テキストボックスを表すクラスです。Colleagueインターフェースを実装します。
  • java.awt.event.TextListenerを実装し、textValueChangedメソッドでテキストの内容の変化をキャッチします
    • テキストの内容に変更があった場合に、AWTのフレームワーク側から呼び出されます
    • colleagueChangedメソッドを呼び出して、自分の文字列内容が変化したことを報告しています
import java.awt.TextField;
import java.awt.Color;
import java.awt.event.TextListener;
import java.awt.event.TextEvent;

public class ColleagueTextField extends TextField implements TextListener, Colleague {
    private Mediator mediator;
    public ColleagueTextField(String text, int columns) {   //コンストラクタ
        super(text, columns);
    }
    public void setMediator(Mediator mediator) {            // Mediator保持
        this.mediator = mediator;
    }
    public void setColleagueEnabled(boolean enabled) {      // Mediatorから有効、無効が指示される
        setEnabled(enabled);
        setBackground(enabled ? Color.white : Color.lightGray);
    }
    public void textValueChanged(TextEvent e) {             // 文字列が変化したらMediatorに追加
        mediator.colleagueChanged();
    }
}

LoginFrameクラス

  • ログインダイアログを表すクラスです。Mediatorインターフェースを実装します
  • GUIIアプリケーションを作るためのクラスであるjava.awt.Frameのサブクラスです
  • コンストラクタの中で、背景色・レイアウトマネージャ設定・Colleagueの生成・配置・初期状態の設定・表示を行います
  • colleagueChangedメソッドで、各Colleageの有効/無効を判定します
    • 相談役への相談が、colleagueChangedメソッドに集約しています
import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.CheckboxGroup;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class LoginFrame extends Frame implements ActionListener, Mediator {
    private ColleagueCheckbox checkGuest;
    private ColleagueCheckbox checkLogin;
    private ColleagueTextField textUser;
    private ColleagueTextField textPass;
    private ColleagueButton buttonOk;
    private ColleagueButton buttonCancel;

    // コンストラクタ。
    // Colleagueたちを生成し、配置した後に表示を行う。
    public LoginFrame(String title) {
        super(title);
        setBackground(Color.lightGray);
        // レイアウトマネージャを使って4×2のグリッドを作る
        setLayout(new GridLayout(4, 2));
        // Colleagueたちの生成
        createColleagues();
        // 配置
        add(checkGuest);
        add(checkLogin);
        add(new Label("Username:"));
        add(textUser);
        add(new Label("Password:"));
        add(textPass);
        add(buttonOk);
        add(buttonCancel);
        // 有効/無効の初期指定
        colleagueChanged();
        // 表示
        pack();
        show();
    }

    // Colleagueたちを生成する。
    public void createColleagues() {
        // 生成
        CheckboxGroup g = new CheckboxGroup();
        checkGuest = new ColleagueCheckbox("Guest", g, true);
        checkLogin = new ColleagueCheckbox("Login", g, false);
        textUser = new ColleagueTextField("", 10);
        textPass = new ColleagueTextField("", 10);
        textPass.setEchoChar('*');
        buttonOk = new ColleagueButton("OK");
        buttonCancel = new ColleagueButton("Cancel");
        // Mediatorのセット
        checkGuest.setMediator(this);
        checkLogin.setMediator(this);
        textUser.setMediator(this);
        textPass.setMediator(this);
        buttonOk.setMediator(this);
        buttonCancel.setMediator(this);
        // Listenerのセット
        checkGuest.addItemListener(checkGuest);
        checkLogin.addItemListener(checkLogin);
        textUser.addTextListener(textUser);
        textPass.addTextListener(textPass);
        buttonOk.addActionListener(this);
        buttonCancel.addActionListener(this);
    }

    // Colleageからの通知で各Colleageの有効/無効を判定する。
    public void colleagueChanged() {
        if (checkGuest.getState()) { // Guest mode
            textUser.setColleagueEnabled(false);
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(true);
        } else { // Login mode
            textUser.setColleagueEnabled(true);
            userpassChanged();
        }
    }
    // textUserまたはtextPassの変更があった。
    // 各Colleageの有効/無効を判定する。
    private void userpassChanged() {
        if (textUser.getText().length() > 0) {
            textPass.setColleagueEnabled(true);
            if (textPass.getText().length() > 0) {
                buttonOk.setColleagueEnabled(true);
            } else {
                buttonOk.setColleagueEnabled(false);
            }
        } else {
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(false);
        }
    }
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        System.exit(0);
    }
}

Mainクラスで動作確認

import java.awt.*;
import java.awt.event.*;

public class Main {
    static public void main(String args[]) {
        new LoginFrame("Mediator Sample");
    }
}

Mediatorパターンのメリット

  • 表示の有効/無効に関するロジックは複雑になりますが、LoginFrameクラスに集約されているので、改修する場合はLoginFrameクラスのみ修正すればよいことになります
  • 通信経路を少なくすることで、インスタンスが多くなっても、シンプルな設計にできます
  • アプリケーションへの依存性が高い部分は、LoginFrameクラスに集約されているので、LoginFrameクラスの再利用性は低いです

今日のポイント

  • Mediatorパターンは、相談役を通して行動を起こすようにしてもらうパターンです。
  • 関係し合う、多数のオブジェクト間の調整を行う必要があるときに、Mediatoパターンを使います
  • 表示の有効/無効に関するロジックは複雑になりますが、LoginFrameクラスに集約されているので、改修する場合はLoginFrameクラスのみ修正すればよいことになります
  • アプリケーションへの依存性が高い部分は、LoginFrameクラスに集約されているので、LoginFrameクラスの再利用性は低いです

 本日もお疲れ様です😊  

 

 

【Java】Facade パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Facadeパターンについて

Facade パターンとは

  • Facadeパターンは、複雑なシステムに対してシンプルな窓口(インターフェース)を用意するパターンです
  • 大きなプログラムを使って処理を行うためには、関係しあっているたくさんのクラスを適切に制御しなければなりません
  • Facadeパターンで、複雑な詳細をまとめ、高レベルでシンプルなインターフェースを提供することで、その処理を行うための"窓口"を用意します
  • こうすることで、たくさんのクラスを個別に制御しなくてよくなります

サンプルプログラム

  • Facade パターンを使って、ユーザのWebページを作成する例を取り上げます
  • 各クラスの役割は以下のようになっています
名前 役割
Database メールアドレスからユーザ名を得る
HtmlWriter HTMLファイルを作成する
PageMaker (Facade役) メールアドレスからユーザのwebサイトを作成する

f:id:mohuNeko:20201231120500p:plain

Databaseクラス

  • データベース名を指定して、それに対応したPropertiesを作成するクラスです
  • このクラスではインスタンスを作らず、getPropertiesというstaticメソッドを介してPropertiesのインスタンスを得ています
package pagemaker;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Database {
    private Database() {    // newでインスタンス生成させないためにprivate宣言
    }

    public static Properties getProperties(String dbname) { // データベース名からPropertiesを得る
        String filename = dbname + ".txt";
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(filename));
        } catch (IOException e) {
            System.out.println("Warning: " + filename + " is not found.");
        }
        return prop;
    }
}
neko@cat.com = Neko
kiki@deliveryService.com = Kiki
nausicaa@theValleyOfTheWind.com= Nausicaa
chihiro@spiritedAway.com= Ogino Chihiro

HtmlWriterクラス

  • インスタンス作成時にWriterを与え、そのWriterに対してHTMLを出力します
  • このクラスでは、titleメソッドを最初に呼ばないといけないという制約が隠れていて、窓口であるPageMakerクラスはその制約を守るように書かれています
package pagemaker;

import java.io.Writer;
import java.io.IOException;

public class HtmlWriter {
    private Writer writer;
    public HtmlWriter(Writer writer) {  // コンストラクタ
        this.writer = writer;
    }

    public void title(String title) throws IOException {    // タイトルの出力
        writer.write("<html>");
        writer.write("<head>");
        writer.write("<title>" + title + "</title>");
        writer.write("</head>");
        writer.write("<body>\n");
        writer.write("<h1>" + title + "</h1>\n");
    }
    public void paragraph(String msg) throws IOException {  // 段落の出力
        writer.write("<p>" + msg + "</p>\n");
    }
    public void link(String href, String caption) throws IOException {  // リンクの出力
        paragraph("<a href=\"" + href + "\">" + caption + "</a>");
    }
    public void mailto(String mailaddr, String username) throws IOException {   // メールアドレスの出力
        link("mailto:" + mailaddr, username);
    }
    public void close() throws IOException {    // 閉じる
        writer.write("</body>");
        writer.write("</html>\n");
        writer.close();
    }
}

PageMakerクラス

  • publicなメソッドはmakeWelcomePageのみです
    • このメソッドにメールアドレスと出力ファイル名を指定したら、Webページが作成されます
  • HtmlWriterクラスのメソッドを呼ぶところは、このPageMakerクラスが引き受けて、外部に対しては一個のmakeWelcomePageメソッドのみを見せます
  • こうすることで、"シンプルな窓口"を実現できます
package pagemaker;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

public class PageMaker {
    private PageMaker() {   // インスタンスは作らないのでprivate宣言する
    }

    public static void makeWelcomePage(String mailaddr, String filename) {  //シンプルな窓口
        try {
            Properties mailprop = Database.getProperties("maildata");  //maildata.txtを指定
            String username = mailprop.getProperty(mailaddr);
            HtmlWriter writer = new HtmlWriter(new FileWriter(filename));
            writer.title("Welcome to " + username + "'s page!");
            writer.paragraph(username + "のページへようこそ");
            writer.paragraph("宅急便のご依頼お待ちしております");
            writer.mailto(mailaddr, username);
            writer.close();
            System.out.println(filename + " is created for " + mailaddr + " (" + username + ")");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Mainクラスで動作確認

  • pagemakerパッケージのPageMakerクラスを利用します
  • 引数で指定したメールアドレスから名前を調べ、ファイルを作成します
import pagemaker.PageMaker;

public class Main {
    public static void main(String[] args) {
        PageMaker.makeWelcomePage("kiki@deliveryService.com", "welcome.html");
    }
}
  • 実行結果

f:id:mohuNeko:20201231122134p:plain

Facadeパターンのメリット

  • Facadeパターンを使うことで、インターフェースを少なくし、複雑なものを単純に見せることができます
  • インターフェースの数を少なくすることで、外部と 疎結合 な関係にすることができます
  • つまり、パッケージを部品として再利用しやすくしてくれます。

今日のポイント

  • Facadeパターンは、複雑なシステムに対してシンプルな窓口(インターフェース)を用意するパターンです
  • 大きなプログラムの複雑な詳細をまとめ、"高レベルでシンプルなインターフェース" を提供することで、たくさんのクラスを個別に制御しなくてよくなります
  • インターフェースの数を少なくすることで、外部と 疎結合 な関係にすることができます
  • つまり、パッケージを部品として再利用しやすくしてくれます

 本日もお疲れ様です😊