デザインパターン_動作パターン - 「テンプレートメソッドパターン」
ダークホースプログラマによるJavaデザインパターンの詳細解説、23のJavaデザインパターン(図表+フレームワークソースコード解析+実戦)をノートとしてまとめています。
動作パターンは、実行時のプログラムの複雑なフロー制御を記述するために使用され、つまり、複数のクラスまたはオブジェクトが相互に連携して、単一のオブジェクトだけでは完了できないタスクを完了する方法を記述するために使用されます。これには、アルゴリズム間の責任の割り当てが含まれます。そしてオブジェクト。
类行为模式
動作パターンはとに分類され对象行为模式
、前者は継承メカニズムを使用してクラス間で動作を分散し、後者は合成または集約を使用してオブジェクト間で動作を分散します。結合関係や集約関係は、継承関係に比べて結合度が低く、「複合再利用の原則」を満たすため、オブジェクトの動作パターンはクラスの動作パターンに比べて柔軟性が高い。
行動パターンは次のように分類されます。
- テンプレートメソッドパターン
- 戦略パターン
- コマンドモード
- 責任連鎖モデル
- ステートモード
- オブザーバーパターン
- メディエーターパターン
- イテレータパターン
- 訪問者のパターン
- メモモード
- 通訳モード
上記の11の行動パターンは、 です除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式
。
概要
オブジェクト指向プログラミングのプロセスにおいて、プログラマーは、システムを設計するときに、アルゴリズムに必要な主要なステップを知っており、これらのステップの実行順序を決定しますが、一部のステップの具体的な実装がまだ不明であるという状況に遭遇することがあります。言い換えれば、特定の手順の実装は特定の環境に関連しています。
例えば、銀行に行って業務を行う場合、一般的に「番号を聞く」「列に並ぶ」「特定の業務を処理する」「行員を評価する」という4つのプロセスを経るが、その中でも「番号を聞く」「列に並ぶ」「行員を評価する」という業務は重要である。親クラスでも同じことを実装できますが、具体的な業務は人によって異なり、入金、出金、送金など、サブクラスに遅れる可能性があります。
意味
- 操作でアルゴリズムのスケルトンを定義し、アルゴリズムの一部のステップをサブクラスに延期します。これにより、サブクラスは、アルゴリズムの構造を変更せずにアルゴリズムの特定のステップを再定義できます。
構造
テンプレート メソッド パターンには、次の主な役割が含まれています。
- 抽象クラス: アルゴリズムの概要と骨格を与える役割を果たします。これは、テンプレート メソッドといくつかの基本メソッドで構成されます。
- テンプレート メソッド: アルゴリズムのスケルトンを定義し、アルゴリズムに含まれる基本メソッドを特定の順序で呼び出します。
- 基本メソッド: アルゴリズムの各ステップを実現するメソッドであり、テンプレート メソッドの不可欠な部分です。基本的な方法は 3 つあります。
- 抽象メソッド: 抽象メソッドは抽象クラスによって宣言され、その具象サブクラスによって実装されます。
- 具象メソッド:具象メソッドは抽象クラスまたは具象クラスによって宣言および実装され、そのサブクラスは上書きまたは直接継承できます。
- フックメソッド: 判定用の論理メソッドやサブクラスで書き換える必要がある空メソッドなどを抽象クラスに実装しています。
(一般的なフックメソッドは論理的に判定するメソッドです。この種のメソッド名は一般的にisXxx、戻り値の型はboolean型となります)
- 具象クラス: 抽象クラスで定義された抽象メソッドとフック メソッドを実装します。これらはトップレベル ロジックのステップです。
事例の実現
【例】炒め物
調理の手順は決まっており、油を注ぐ、油を加熱する、野菜を注ぐ、調味料を注ぐ、炒めるといったステップに分かれています。これは、テンプレート メソッド パターンを通じてコードでシミュレートされるようになりました。クラス図は次のとおりです。
コードは以下のように表示されます:
-
抽象クラス (テンプレート メソッドと基本メソッドの定義)
public abstract class AbstractClass { // 模板方法(声明为final 禁止子类修改) public final void cookProcess() { // 第一步:倒油 this.pourOil(); // 第二步:热油 this.heatOil(); // 第三步:倒蔬菜 this.pourVegetable(); // 第四步:倒调味料 this.pourSauce(); // 第五步:翻炒 this.fry(); } // =======基本方法======= // 第一步:倒油是一样的,所以直接实现-具体方法 public void pourOil() { System.out.println("倒油"); } // 第二步:热油是一样的,所以直接实现-具体方法 public void heatOil() { System.out.println("热油"); } // 第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)-抽象方法 // 抽象方法 public abstract void pourVegetable(); // 第四步:倒调味料是不一样的(一个放辣椒,一个放蒜蓉)-抽象方法 public abstract void pourSauce(); // 第五步:翻炒是一样的,所以直接实现-具体方法 public void fry() { System.out.println("炒啊炒啊炒到熟啊"); } }
-
具体的なサブクラス
// 蔬菜类-包菜 public class ConcreteClass_BaoCai extends AbstractClass { @Override public void pourVegetable() { System.out.println("下锅的蔬菜是包菜"); } @Override public void pourSauce() { System.out.println("下锅的酱料是辣椒"); } } // 蔬菜类-菜心 public class ConcreteClass_CaiXin extends AbstractClass { @Override public void pourVegetable() { System.out.println("下锅的蔬菜是菜心"); } @Override public void pourSauce() { System.out.println("下锅的酱料是蒜蓉"); } }
-
テストクラス
public class Client { public static void main(String[] args) { // 炒手撕包菜 ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai(); baoCai.cookProcess(); // 炒蒜蓉菜心 ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin(); caiXin.cookProcess(); } }
出力
倒油 热油 下锅的蔬菜是包菜 下锅的酱料是辣椒 炒啊炒啊炒到熟啊 倒油 热油 下锅的蔬菜是菜心 下锅的酱料是蒜蓉 炒啊炒啊炒到熟啊
注: 悪意のある操作を防ぐために、
final
一般的なテンプレート メソッドにはキーワードが追加されています。
長所と短所
アドバンテージ
- コードの再利用性の向上
- コードの同じ部分を抽象親クラスに配置し、異なるコードを異なるサブクラスに配置します。
- リバースコントロール
- 親クラスを通じてそのサブクラスの操作を呼び出し、サブクラスの特定の実装を通じてさまざまな動作を拡張することで、逆制御が実現され、「開閉原則」に準拠します。
欠点がある
- 異なる実装ごとにサブクラスを定義する必要があり、クラスの数が増加し、システムが大きくなり、設計がより抽象化されます。
- 親クラスの抽象メソッドはサブクラスによって実装され、サブクラスの実行結果が親クラスの結果に影響を与えるため、制御構造が逆になり、コードの読み取りが困難になります。
該当シーン
- アルゴリズムの全体的なステップは固定されていますが、個々の部分が揮発性である場合、この時点でテンプレート メソッド パターンを使用して、サブクラスが実装する揮発性部分を抽象化できます。
- 親クラスに対するサブクラスの逆制御を実現するには、親クラスのアルゴリズムのステップがサブクラスを介して実行されるかどうかを決定する必要があります。
JDKソースコード解析-InputStream
InputStream クラスは、テンプレート メソッド パターンを使用します。read()
次のように、InputStream クラスに複数のメソッドが定義されています。
public abstract class InputStream implements Closeable {
// 抽象方法,要求子类必须实现
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read(); // 调用了无参的read方法(也就是子类的read方法,这就是反向控制,模板方法模式的思想)该方法是每次读取一个字节数据
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}
上記のコードからわかるように、パラメーターのないメソッドはread()
抽象メソッドであり、サブクラスによって実装する必要があります。メソッドはメソッドread(byte b[])
を呼び出すread(byte b[], int off, int len)
ため、ここで注目するメソッドは 3 つのパラメーターを持つメソッドです。
このメソッドの 18 行目と 27 行目では、パラメータなしの抽象メソッドが呼び出されていることがわかりますread()
。
InputStream は抽象クラスで、パラメータが 3 つあるメソッドはread(byte b[], int off, int len)
アルゴリズムの枠組みを定義するテンプレートメソッドで、read()
パラメータなしでメソッドを複数回呼び出し、その都度取得したバイトデータを配列に格納します。したがって、read()
メソッドは抽象メソッドであり、サブクラスはそれを実装する必要があります。
要約:
バイト配列データを読み取る方法は、InputStream 親クラスで定義されており、一度に 1 バイトを読み取り、それを配列の最初のインデックス位置に格納し、len バイトのデータを読み取ります。具体的にはどうやってバイトデータを読み取るのでしょうか? サブクラスによって実装されます。
AQS でのテンプレート メソッドの設計
Java のテンプレート メソッド設計パターンの典型的なアプリケーションの 1 つは、AQS の設計ですAQS就是一个抽象类,我们在自定义一些同步器时需要继承AQS(比如ReentrantLock等)并实现共享资源state如何获取和释放,至于具体线程入队阻塞以及如何被唤醒出队的逻辑,AQS已经帮我们实现好了
。
-
例: AQS テンプレート メソッドを見てください。
acquire()
// 1.acquire()方法 /* * tryAcquire() 留给子类实现 * acquireQueued() AQS已经实现好了,即获取同步状态失败时的入队阻塞逻辑。 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // AQS.tryAcquire() 没有设计成抽象方法,而是直接抛出异常,具体是让子类实现如何获取同步状态。 protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
-
ReentrantLock がどのように機能するかを見てみましょう
// ReentrantLock内部定义一个内部类Sync实现AQS abstract static class Sync extends AbstractQueuedSynchronizer { // 重写了AQS留给我们的tryAcquire() protected final boolean tryAcquire(int acquires) { // 具体的实现 ... } }
-
AQS のテンプレート メソッドのサブクラスのカスタム実装用に予約されているメソッド
// 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS操作设置同步状态 protected boolean tryAcquire(int arg); // 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 protected boolean tryRelease(int arg) // 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之获取失败 protected int tryAcquireShared(int arg) // 共享式释放同步状态 protected boolean tryReleaseShared(int arg) // 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占。 protected boolean isHeldExclusively()