デザインパターン-行動パターン(中央)

より転載https://mp.weixin.qq.com/s?__biz=MzI4Mzc5NDk4MA==&mid=2247488016&idx=1&sn=71d1d9d024d6c5a657cb2d6f2fab76d4&chksm=eb84195bdcf3904d97024fa8659d1f84a90b125d0d1c542e0b98f5e8b5940e05f0d404b6370e&scene=178&cur_album_id=1461125104968318982#rd

デザインパターンに関する一連の記事では、5つの建設的パターンと7つの構造的パターンを紹介しました。では 前回の記事 :、我々は2つの行動モード導入している責任モードのチェーン と コマンドモードを

この記事では、4つの行動パターンを紹介します。

  • 通訳モード

  • イテレーターモード

  • 中間モデル

  • メモモード

 

通訳モード

私の国のIT業界は常に中国のプログラミングの夢を持っていました。すべての関係者が中国のプログラミングについて議論してきましたが、それは国の意味の高さまで上昇しました。この記事ではその正誤については説明していませんが、試してみるのもよいでしょう。それと簡単な中国語のプログラミング文法を定義します。

デザインモードでは、インタプリタモードを使用して文法をカスタマイズします。その定義は次のとおりです。

インタプリタパターン:言語を指定して、その文法の表現を定義し、その表現を使用して言語の文を解釈するインタプリタを定義します。

通訳モードはもっとわかりにくく、理解するのが難しいですが、この記事では、簡単な例で通訳モードを学習します。中国語を使用して、10以内で足し算と引き算の数式を記述します。といった:

  • 「1プラス1」を入力し、結果2を出力します。

  • 「1プラス1プラス1」を入力し、結果3を出力します。

  • 「2プラス5マイナス3」と入力し、結果を出力します4

  • 「7マイナス5プラス4マイナス1」と入力し、結果5を出力します。

  • 「9マイナス5プラス3マイナス1」と入力し、結果6を出力します。

この要求を見ると、入力文字列を1文字に分割し、switch-caseを使用して数値を数値に変換し、演算子を使用して加算するか減算するかを決定し、対応する方法を簡単に考えることができます。足し算と引き算の計算。、そして最後に結果を返します。

この計画は確かに実行可能ですが、プロセス指向ではありません。ご存知のとおり、プロセス指向プログラミングには、結合度が高く、拡張が容易ではないなどの欠点があります。次に、この関数をオブジェクト指向の方法で実装しようとします。

オブジェクト指向プログラミングのアイデアによれば、式内のさまざまなタイプの要素に対応するオブジェクトを作成する必要があります。次に、最初に式のメンバーを分析します。

  • 番号:零到九 対応 0 ~ 9

  • 電卓:加、减 対応 +、-

数式にはこの2つの要素しかありませんが、その中でも、switch-case 中国の名前をアラビア数字に変換するだけで、数字の処理は比較的簡単 です。

オペレーターへの対応方法は?計算記号の左側と右側は、単一の数値または別の計算式にすることができます。しかし、それが数値であろうと数式であろうと、どちらにも共通点が1つあります。つまり、両方とも整数を返します。数値はそれ自体に戻り、数式はその計算結果を返します。

したがって、この共通点に基づいて整数を返すインターフェイスを抽出でき、数値と計算機の両方がインターフェイスの実装クラスとして使用されます。計算では、スタック構造を使用してデータを格納し、数値と計算シンボルをこのインターフェイスの実装クラスとして統合し、計算のためにスタックにプッシュします。

話は安いです、コードを見せてください。

数値と電卓の共通インターフェース:

interface Expression {
    int intercept();
}

前述のように、数値と演算子はどちらも式の一部であり、共通点はすべて整数を返すことです。式から整数を計算するプロセスは解释(インターセプト)と呼ばれ ます。

数値クラスの説明は、実装が比較的簡単です。

public class Number implements Expression {
    int number;

    public Number(char word) {
        switch (word) {
            case '零':
                number = 0;
                break;
            case '一':
                number = 1;
                break;
            case '二':
                number = 2;
                break;
            case '三':
                number = 3;
                break;
            case '四':
                number = 4;
                break;
            case '五':
                number = 5;
                break;
            case '六':
                number = 6;
                break;
            case '七':
                number = 7;
                break;
            case '八':
                number = 8;
                break;
            case '九':
                number = 9;
                break;
            default:
                break;
        }
    }

    @Override
    public int intercept() {
        return number;
    }
}

Numberクラスのコンストラクターで、最初に着信文字を対応する数値に変換します。説明するときは、変換された数値を返すだけです。

加算であろうと減算であろうと、それらは左右の式で動作するため、計算演算子の共通の抽象親クラスを抽出できます。

abstract class Operator implements Expression {
    Expression left;
    Expression right;

    Operator(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
}

この抽象親クラスには、演算子の左側と右側の式を表す2つの変数が格納されています。

追加クラスは次のように実装されます。

class Add extends Operator {

    Add(Expression left, Expression right) {
        super(left, right);
    }

    @Override
    public int intercept() {
        return left.intercept() + right.intercept();
    }
}

減算クラス:

class Sub extends Operator {

    Sub(Expression left, Expression right) {
        super(left, right);
    }

    @Override
    public int intercept() {
        return left.intercept() - right.intercept();
    }
}

加算クラスと減算クラスはどちらもOperatorクラスを継承しています。それらを説明するときは、左右の式で説明されている値を加算または減算します。

数値クラスと計算記号の両方が定義されています。現時点では、それらを統合して統合計算を実行するために、別の計算クラスを作成するだけで済みます。

計算クラス:

class Calculator {
    int calculate(String expression) {
        Stack<Expression> stack = new Stack<>();
        for (int i = 0; i < expression.length(); i++) {
            char word = expression.charAt(i);
            switch (word) {
                case '加':
                    stack.push(new Add(stack.pop(), new Number(expression.charAt(++i))));
                    break;
                case '减':
                    stack.push(new Sub(stack.pop(), new Number(expression.charAt(++i))));
                    break;
                default:
                    stack.push(new Number(word));
                    break;
            }
        }
        return stack.pop().intercept();
    }
}

計算クラスでは、スタック構造を使用して操作の各ステップを保存します。式の式をトラバースします。

  • 数字に遭遇すると、スタックにプッシュされます。

  • 電卓に遭遇したら、最初にスタックの一番上の要素をポップし、次にそれを次の数値と一緒に電卓のコンストラクターに渡して電卓の数式を作成し、スタックにプッシュします。

スタックプロセスは実際の計算を実行せず、スタック操作は式をネストされたクラスオブジェクトにアセンブルするだけであることに注意してください。といった:

  • 「ワンプラスワン」式、スタックおよびポップ操作の後、生成されたオブジェクトは new Add(new Number('一'), new Number('一'))

  • 「2プラス5マイナス3」式、スタックおよびポップ操作の後、生成されるオブジェクトはnew Sub(new Add(new Number( '二')、new Number( '五'))、new Number( '三'))です。 `

最後のステップはstack.pop().intercept()、スタックの最上位要素を ポップして実行すること intercept() です。その後、実際の計算が実行されます。計算するとき、漢数字と演算子はそれぞれ、コンピューターが理解できる命令として解釈されます。

テストカテゴリ:

public class Client {
    @Test
    public void test() {
        Calculator calculator = new Calculator();
        String expression1 = "一加一";
        String expression2 = "一加一加一";
        String expression3 = "二加五减三";
        String expression4 = "七减五加四减一";
        String expression5 = "九减五加三减一";
        // 输出:一加一 等于 2
        System.out.println(expression1 + " 等于 " + calculator.calculate(expression1));
        // 输出:一加一加一 等于 3
        System.out.println(expression2 + " 等于 " + calculator.calculate(expression2));
        // 输出:二加五减三 等于 4
        System.out.println(expression3 + " 等于 " + calculator.calculate(expression3));
        // 输出:七减五加四减一 等于 5
        System.out.println(expression4 + " 等于 " + calculator.calculate(expression4));
        // 输出:九减五加三减一 等于 6
        System.out.println(expression5 + " 等于 " + calculator.calculate(expression5));
    }
}

これが通訳モードです。中国語の数式をコンピューターに解釈すると、コンピューターが正しい結果を計算します。

この例の式の構成を分析すると、いくつかの明らかな特性を見つけることができます。

  • 数値を分割することはできず、計算の最小単位に属します。

  • 足し算と引き算は、2つの数値(または2つの数式)と計算記号に分割できます。これらは計算の最小単位ではありません。

インタプリタモードでは、分割できない最小単位を終端式と呼び、分割できる式を非終端式と呼びます。

インタープリターモードにはある程度の拡張性があります。他の演算子を追加する必要がある場合は、Operatorのサブクラスを追加することで完了できます。ただし、追加後は、計算の優先度に応じて計算ルールを変更する必要があります。完全なインタプリタモードは非常に複雑であり、実際の開発でインタプリタをカスタマイズする必要はほとんどないことがわかります。

インタプリタパターンには一般的な用途があります。通常、文字列を照合する場合、使用される正規表現はインタプリタです。正規表現では、1文字を表す式は終端式に属し、終端式を除くすべての式は非終端式に属します。

 

イテレーターモード

シナリオを想像してみてください。クラスにリストがあります。このリストにアクセスするには、外部クラスに提供する必要がありますが、外部クラスがその中のデータを変更することは望ましくありません。

public class MyList {
    private List<String> data = Arrays.asList("a", "b", "c");
}

一般的に、外部クラスにメンバー変数を提供する方法は2つあります。

  • このリストをパブリック変数として設定します。

  • このリストを返すには、getData()メソッドを追加します。

ただし、これら2つのメソッドには致命的な欠点があり、外部クラスがデータを変更しないことを保証できません。外部クラスがデータオブジェクトを取得した後、リストの内部要素を自由に変更できるため、セキュリティ上の大きなリスクが発生します。

それで、より良い方法はありますか?外部クラスがこのリスト内のデータのみを読み取ることができ、セキュリティを確保するためにデータを変更できないようにします。

分析は、2つの方法を提供することによってこの効果を達成できることを示しています。

  • String next() 外部クラスがデータを1つずつ順番に読み取ることができるようにメソッドを提供し ます。

  • boolean hasNext() 別のデータがあるかどうかを外部クラスに通知するメソッドを提供し ます。

コードは次のように実装されています。

public class MyList {
    private List<String> data = Arrays.asList("a", "b", "c");
    private int index = 0;

    public String next() {
        // 返回数据后,将 index 加 1,使得下次访问时返回下一条数据
        return data.get(index++);
    }

    public boolean hasNext() {
        return index < data.size();
    }
}

クライアントは、whileループを使用してこのリストにアクセスできます。

public class Client {
    @Test
    public void test() {
        MyList list = new MyList();
        // 输出:abc
        while (list.hasNext()) {
            System.out.print(list.next());
        }
    }
}

データメンバー変数は外部クラスに公開されていないため、データが安全であることを保証できます。

ただし、この実装には別の問題があります。トラバーサルが完了すると、hasNext()メソッドは常にfalseを返し、再度トラバースすることはできないため、適切な場所でインデックスを0にリセットする必要があります。

適切なリセットはどこにありますか?実際、next()メソッドとhasNext()メソッドを使用してリストをトラバースすることは完全に普遍的なメソッドであり、Iteratorという名前のインターフェイスを作成できます。Iteratorはイテレーターを意味し、反復は繰り返しフィードバックを意味します。これは、リスト内の要素を順番に。

public interface Iterator {

    boolean hasNext();

    String next();
}

次に、MyListクラスでは、トラバースされるたびにイテレーターが生成され、インデックス変数がイテレーターに配置されます。各イテレータは新しく生成されるため、各トラバーサル中にインデックスは自然に0にリセットされます。コードは次のように表示されます。

public class MyList {
    private List<String> data = Arrays.asList("a", "b", "c");

    // 每次生成一个新的迭代器,用于遍历列表
    public Iterator iterator() {
        return new Itr();
    }

    private class Itr implements Iterator {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < data.size();
        }

        @Override
        public String next() {
            return data.get(index++);
        }
    }
}

クライアントがこのリストにアクセスするためのコードは、次のように変更されます。

public class Client {
    @Test
    public void test() {
        MyList list = new MyList();
        // 获取迭代器,用于遍历列表
        Iterator iterator = list.iterator();
        // 输出:abc
        while (iterator.hasNext()) {
            System.out.print(iterator.next());
        }
    }
}

これは、「デザインパターン」という本で次のように定義されているイテレータパターンです。

イテレーターパターン:オブジェクトの内部詳細を公開せずに、コンテナーオブジェクトのさまざまな要素にアクセスする方法を提供します。

イテレータパターンの中核は、next()メソッドとhasNext()メソッドを定義することです。これにより、外部クラスはこれら2つのメソッドを使用してリストをトラバースし、リストの内部詳細を非表示にすることができます。

実際、JavaにはIteratorインターフェースが組み込まれており、ソースコードでジェネリックを使用すると、このインターフェースの用途が広がります。

public interface Iterator<E> {
    boolean hasNext();
    E next();
}

さらに、この例で使用されているイテレータモードは、ArrayListのソースコードを模倣することによって実装されています。ArrayListソースコードでイテレータモードを使用しているコードの部分は次のとおりです。

public class ArrayList<E> {
    ...
    
    public Iterator<E> iterator() {
        return new Itr();
    }
    
    private class Itr implements Iterator<E> {
        protected int limit = ArrayList.this.size;
        int cursor;
        
        public boolean hasNext() {
            return cursor < limit;
        }

        public E next() {
            ...
        }
    }
}

通常のfor-eachループは、イテレータパターンのアプリケーションでもあります。Javaでは、Iterableインターフェースを実装するクラスはすべて反復可能と見なされます。Iterableにはコアメソッドが1つだけあります。それは、イテレータを取得するためにMyListクラスに実装したiterator()メソッドです。

public interface Iterable<T> {
    Iterator<T> iterator();
}

このインターフェイスを継承するようにMyListクラスを変更する限り、for-eachを使用してデータに繰り返しアクセスできます。

public class MyList implements Iterable<String> {
    private List<String> data = Arrays.asList("a", "b", "c");

    @NonNull
    @Override
    public Iterator<String> iterator() {
        // 每次生成一个新的迭代器,用于遍历列表
        return new Itr();
    }

    private class Itr implements Iterator<String> {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < data.size();
        }

        @Override
        public String next() {
            return data.get(index++);
        }
    }
}

クライアントはfor-eachアクセスを使用します:

public class Client {
    @Test
    public void test() {
        MyList list = new MyList();
        // 输出:abc
        for (String item : list) {
            System.out.print(item);
        }
    }
}

これはイテレータパターンです。基本的に、すべての言語がソースコードレベルですべてのリストのイテレータを提供します。直接使用するだけで済みます。これは比較的単純で一般的に使用されるデザインパターンです。

 

中間モデル

名前が示すように、名前の仲介者は私たちにはあまりにも馴染みがあります。歩いて通勤するときは、よくいろいろな不動産屋さんに会います。彼らの仕事は、買い手と売り手が互いに直接取引する必要をなくすことであり、取引を完了するために仲介者と別々に取引する必要があるだけです。コンピューターの観点からは、それは結合の程度を減らします。

クラス間の関係がネットワークの形をしている場合、仲介者を導入することで、クラス間の関係を星の形にすることができます。各クラスと複数のクラスの間の結合関係は、各クラスとメディエーターの間の結合関係に単純化されます。

たとえば、麻雀をするとき、2人ごとに勝ち負けの関係があるかもしれません。すべてのトランザクションが敗者から勝者に直接送信される場合、ネットワーク結合関係があります。

プログラムを使用して、このプロセスをシミュレートします。

プレイヤーカテゴリー:

class Player {
    // 初始资金 100 元
    public int money = 100;

    public void win(Player player, int money) {
        // 输钱的人扣减相应的钱
        player.money -= money;
        // 自己的余额增加
        this.money += money;
    }
}

このクラスには、残高を表すお金の変数があります。プレーヤーのお金を獲得したら、winメソッドを呼び出して、敗者と自分の残高を変更します。

勝ち方では敗者の残高が差し引かれているので、お金を失う必要はないことに注意してください。

クライアントコード:

public class Client {
    @Test
    public void test() {
        Player player1 = new Player();
        Player player2 = new Player();
        Player player3 = new Player();
        Player player4 = new Player();
        // player1 赢了 player3 5 元
        player1.win(player3, 5);
        // player2 赢了 player1 10 元
        player2.win(player1, 10);
        // player2 赢了 player4 10 元
        player2.win(player4, 10);
        // player4 赢了 player3 7 元
        player4.win(player3, 7);

        // 输出:四人剩余的钱:105,120,88,97
        System.out.println("四人剩余的钱:" + player1.money + "," + player2.money + "," + player3.money + "," + player4.money);
    }
}

クライアント端末では、2人のプレーヤーごとにトレードする必要がある場合、プログラムの結合が増加します。これは、すべてのプレーヤーが他のすべてのプレーヤーに対処する必要があるのと同じです。これは悪い習慣です。

この時点で、敗者が失ったお金をWeChatグループに送金し、勝者がWeChatグループから対応する金額を受け取る限り、中間タイプのWeChatグループを導入できます。メッシュ結合構造はスター構造になります。

現時点では、WeChatグループが仲介役を務め、すべての人に対応する責任があります。各プレイヤーはWeChatグループに対応するだけで済みます。

WeChatグループカテゴリ:

class Group {
    public int money;
}

このクラスには、グループの残高を表すお金の変数が1つだけあります。

プレーヤークラスは次のように変更されます。

class Player {
    public int money = 100;
    public Group group;

    public Player(Group group) {
        this.group = group;
    }

    public void change(int money) {
        // 输了钱将钱发到群里 或 在群里领取自己赢的钱
        group.money += money;
        // 自己的余额改变
        this.money += money;
    }
}

新しいコンストラクターがプレーヤークラスに追加され、メディエーターがコンストラクターに渡されます。勝ち負けの場合は、グループに送金するか、グループで当選したお金を受け取って、残高を変更するだけです。

クライアントコードの対応する変更は次のとおりです。

public class Client {
    @Test
    public void test(){
        Group group = new Group();
        Player player1 = new Player(group);
        Player player2 = new Player(group);
        Player player3 = new Player(group);
        Player player4 = new Player(group);
        // player1 赢了 5 元
        player1.change(5);
        // player2 赢了 20 元
        player2.change(20);
        // player3 输了 12 元
        player3.change(-12);
        // player4 输了 3 元
        player4.change(-3);

        // 输出:四人剩余的钱:105,120,88,97
        System.out.println("四人剩余的钱:" + player1.money + "," + player2.money + "," + player3.money + "," + player4.money);
    }
}

ご覧のとおり、仲介者を導入することで、クライアントコードがより明確になります。誰もがお互いに対処する必要はもうありません。すべてのトランザクションは仲介者を介して完了することができます。

実際、このコードにはまだいくつかの欠点があります。前提を見落としているため、WeChatグループのお金はマイナスにはなりません。つまり、勝者がWeChatグループに送金してお金を受け取る前に、敗者は最初にWeChatグループに送金する必要があります。この関数は、Programmer's Adventuresの「MultithreadKingdom」で学習た待機/通知メカニズムによって完了でき 、中間モデルとは関係がないため、関連するコードはここに記載されていません。興味のある読者は、自分自身。

全体として、メディエーターパターンは 、クラス間多对多关系 の関係を単純化するために 使用される 多对一、一对多关系デザインパターンです。その定義は次のとおりです。

メディエーターパターン:一連のオブジェクト間の相互作用をカプセル化し、元のオブジェクト間の結合を緩め、それらの間の相互作用を個別に変更するメディエーターオブジェクトを定義します。

中間モデルの欠点も明らかです。すべての責任を中間クラスに移すためです。つまり、中間クラスはすべてのクラス間の調整作業を処理する必要があり、これにより中間クラスがスーパークラスに進化する可能性があります。したがって、中間モデルを使用する場合は、長所と短所を比較検討する必要があります。

 

メモモード

メモモードの最も一般的な実装は、ゲーム内の保存機能と読み取り機能です。保存と読み取りにより、いつでも以前の状態に戻すことができます。

ゲームをプレイしているとき、ビッグボスに当たる前に、通常はゲームの進行状況を保存して保存します。ボスに勝てない場合は、ファイルをもう一度読み取って状態を復元できます。

プレイヤーカテゴリー:

class Player {
    // 生命值
    private int life = 100;
    // 魔法值
    private int magic = 100;

    public void fightBoss() {
        life -= 100;
        magic -= 100;
        if (life <= 0) {
            System.out.println("壮烈牺牲");
        }
    }

    public int getLife() {
        return life;
    }

    public void setLife(int life) {
        this.life = life;
    }

    public int getMagic() {
        return magic;
    }

    public void setMagic(int magic) {
        this.magic = magic;
    }
}

プレイヤーには、体力と魔法の2つの属性を定義しました。fightBoss()メソッドがあり、ボスに当たるたびに100ポイントの体力が差し引かれます。ヘルス値が0以下の場合、ユーザーは「英雄的に犠牲にする」ように求められます。

クライアントは次のように実装されます。

public class Client {
    @Test
    public void test() {
        Player player = new Player();
        // 存档
        int savedLife = player.getLife();
        int savedMagic = player.getMagic();

        // 打 Boss,打不过,壮烈牺牲
        player.fightBoss();

        // 读档,恢复到打 Boss 之前的状态
        player.setLife(savedLife);
        player.setMagic(savedMagic);
    }
}

クライアントでは、fightBoss()の前に現在のヘルスとマナを保存します。ボスをプレイし、犠牲になったことを発見したら、戻ってファイルを読み、ボスの前の状態に戻ります。

これがメモモードです…?正確には、物事はそれほど単純ではありません。

プロトタイプモードで購入した、ジェイチョウとまったく同じミルクティーを覚えていますか?最初に、ミルクティーのカップを複製するために、ミルクティーの属性をジェイ・チョウが購入したミルクティーのカップと同じにするように割り当てました。しかし、これには欠点があります。1000人のファンに対して1つずつ1000の割り当てを書くことはできません。そのため、最終的に、ミルクティークラス内にCloneableインターフェイスを実装し、clone()メソッドを定義して、すべての属性をコピーするコード行を実現しました。

覚書モデルでも同様のアプローチを採用する必要があります。個々の属性に1つずつアクセスしてファイルを読み取ったりアーカイブしたりしないでください。より良いアプローチは、アーカイブとファイルの読み取りを、アーカイブする必要のあるクラスに任せることです。

新しいメモクラス:

class Memento {
    int life;
    int magic;

    Memento(int life, int magic) {
        this.life = life;
        this.magic = magic;
    }
}

このカテゴリでは、アーカイブする必要のあるデータを管理します。

プレーヤークラスで、メモクラスを介してファイルを保存および読み取ります。

class Player {
    ...

    // 存档
    public Memento saveState() {
        return new Memento(life, magic);
    }

    // 读档
    public void restoreState(Memento memento) {
        this.life = memento.life;
        this.magic = memento.magic;
    }
}

クライアントクラスの対応する変更は次のとおりです。

public class Client {
    @Test
    public void test() {
        Player player = new Player();
        // 存档
        Memento memento = player.saveState();

        // 打 Boss,打不过,壮烈牺牲
        player.fightBoss();

        // 读档
        player.restoreState(memento);
    }
}

これは完全なメモモードです。このデザインパターンの定義は次のとおりです。

メモモード:カプセル化を破棄せずに、別のオブジェクトの内部状態のスナップショットがメモオブジェクトを介して保存され、このオブジェクトは将来の適切な時点で保存された状態に復元されます。

メモモードの利点は次のとおりです。

  • ユーザーが特定の履歴状態に簡単に戻ることができるように、状態を復元できるメカニズムをユーザーに提供します

  • ユーザーが状態保存の詳細を気にする必要がないように、情報のカプセル化を実現します

弱点は次のとおりです。

  • リソースを消費するクラスのメンバー変数が多すぎると、必然的に比較的大きなリソースを消費し、保存するたびに一定量のメモリが消費されます。

一般的に、メモモードには欠点よりも利点が多いため、多くのプログラムがユーザーにバックアップソリューションを提供します。たとえば、IDEでは、ユーザーは設定をzipとしてエクスポートし、設定を復元する必要がある場合は、エクスポートされたzipファイルをインポートできます。この機能の内部原理はメモモードです。

おすすめ

転載: blog.csdn.net/qq_37381177/article/details/109278905