ブリッジモードを理解するには2つの方法があります。理解する最初の方法は、「抽象化と実装を分離して、それらを独立して開発できるようにする」ことです。この理解方法は非常に特別であり、多くのアプリケーションシナリオはありません。「組み合わせは継承よりも優れている」という設計原則と同様に、別の理解方法はより単純です。この理解方法はより一般的であり、より多くのアプリケーションシナリオがあります。どちらの理解方法でも、コード構造は同じであり、クラス間の組み合わせ関係です。
JavaIOクラスの「奇妙な」使用法
Java IOクラスライブラリは非常に大きく複雑で、IOデータの読み取りと書き込みを担当するクラスが数十あります。Java IOクラスを分類すると、次の2つの次元から4つのカテゴリに分類できます。詳細は以下のとおりです。
さまざまな読み取りおよび書き込みシナリオのために、JavaIOはこれらの4つの親クラスに基づいて多くのサブクラスを拡張しました。詳細は以下のとおりです。
私が最初にJavaを学んだとき、次のコードのように、JavaIOのいくつかの使用法について大きな疑問を抱いていました。test.txtファイルを開き、そこからデータを読み取ります。その中で、InputStreamは抽象クラスであり、FileInputStreamはファイルストリームを読み取るために特別に使用されるサブクラスです。BufferedInputStreamは、データ読み取りの効率を向上させることができるバッファリングをサポートするデータ読み取りクラスです。
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
上記のコードを一見すると、Java IOの使用がより厄介であることがわかります。最初にFileInputStreamオブジェクトを作成してから、それをBufferedInputStreamオブジェクトに渡して使用する必要があります。Java IOがFileInputStreamを継承し、キャッシングをサポートするBufferedFileInputStreamクラスを設計しなかったのはなぜだろうと考えていました。このようにして、次のコードのようにBufferedFileInputStreamクラスオブジェクトを直接作成し、ファイルを開いてデータを読み取ることができます。使いやすいのではないでしょうか。
InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
継承に基づく設計スキーム
InputStreamにサブクラスFileInputStreamが1つしかない場合は、FileInputStreamに基づいて孫クラスBufferedFileInputStreamを設計できます。これも受け入れられます。結局のところ、継承構造はかなり単純です。しかし実際には、InputStreamを継承する多くのサブクラスがあります。InputStreamの各サブクラスを指定してから、キャッシュ読み取りをサポートするサブクラスを引き続き導出する必要があります。
キャッシュ読み取りのサポートに加えて、他の側面で関数を拡張する必要がある場合、たとえば、次のDataInputStreamクラスは、基本的なデータタイプ(int、boolean、longなど)に従ってデータの読み取りをサポートします。
この場合、継承によって実装を続ける場合は、DataFileInputStream、DataPipedInputStream、およびその他のクラスを引き続き派生させる必要があります。基本タイプに従ってデータのキャッシュと読み取りの両方をサポートするクラスがまだ必要な場合は、BufferedDataFileInputStreamやBufferedDataPipedInputStreamなどのn個の複数のクラスを引き続き派生させる必要があります。これは2つの拡張機能の追加のみです。さらに拡張機能を追加する必要がある場合、組み合わせが爆発的に増加し、クラス継承構造が非常に複雑になり、コードの拡張や保守が容易になりません。これが、セクション10で継承の使用を推奨しなかった理由です。
デコレータパターンに基づく設計スキーム
また、「構成は継承よりも優れている」、「継承の代わりに構成を使用する」こともできます。継承構造が複雑すぎるという問題を解決するには、継承関係を構成関係に変更することで解決できます。次のコードは、JavaIOのこの設計アイデアを示しています。ただし、コードを簡略化し、必要なコード構造のみを抽象化しました。興味がある場合は、JDKソースコードを直接確認できます。
public abstract class InputStream {
//...
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
//...
}
public long skip(long n) throws IOException {
//...
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {
}
public synchronized void mark(int readlimit) {
}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
}
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
//...实现基于缓存的读数据接口...
}
public class DataInputStream extends InputStream {
protected volatile InputStream in;
protected DataInputStream(InputStream in) {
this.in = in;
}
//...实现读取基本类型数据的接口
}
上記のコードを読んだ後、デコレータパターンは単に「継承を構成に置き換える」のでしょうか。もちろん違います。Java IOの設計の観点から、デコレータパターンには、単純な組み合わせ関係と比較して2つの特別な機能があります。
**最初の特別な場所は、デコレータクラスと元のクラスが同じ親クラスを継承することです。これにより、複数のデコレータクラスを元のクラスに「ネスト」できます。**たとえば、次のコードでは、FileInputStreamの2つのデコレータクラスであるBufferedInputStreamとDataInputStreamをネストしているため、キャッシュの読み取りと基本的なデータタイプに応じたデータの読み取りの両方をサポートします。
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
** 2番目に特別な場所は次のとおりです。デコレータクラスは関数の拡張であり、デコレータモードアプリケーションシナリオの重要な機能でもあります。**実際には、プロキシモデル、ブリッジモデル、現在のデコレータモデルなど、「組み合わせ関係」のコード構造に準拠したデザインパターンが多数あります。それらのコード構造は非常に似ていますが、各設計パターンの意図は異なります。より類似したプロキシモードとデコレータモードを使用します。プロキシモードでは、プロキシクラスは元のクラスとは関係のない関数をアタッチしますが、デコレータモードでは、デコレータクラスは元のクラスに関連する拡張機能をアタッチします。特徴。
// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A impelements IA {
public void f() {
//... }
}
public class AProxy implements IA {
private IA a;
public AProxy(IA a) {
this.a = a;
}
public void f() {
// 新添加的代理逻辑
a.f();
// 新添加的代理逻辑
}
}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A implements IA {
public void f() {
//... }
}
public class ADecorator implements IA {
private IA a;
public ADecorator(IA a) {
this.a = a;
}
public void f() {
// 功能增强代码
a.f();
// 功能增强代码
}
}
実際、JDKのソースコードを見ると、BufferedInputStreamとDataInputStreamはInputStreamから継承されているのではなく、FilterInputStreamと呼ばれる別のクラスであることがわかります。では、そのようなクラスを導入するための設計意図は何ですか?
BufferedInputStreamクラスのコードをもう一度見てみましょう。InputStreamはインターフェイスではなく抽象クラスであり、その関数のほとんど(read()、available()など)にはデフォルトの実装があります。論理的に言えば、BufferedInputStreamクラスのキャッシュ関数を増やす必要がある関数を再実装するだけで済みます。それだけです。他の関数は、InputStreamのデフォルトの実装を継承します。しかし、実際には、それは機能しません。
キャッシュ関数を増やす必要のない関数の場合でも、BufferedInputStreamを再実装する必要があり、InputStreamオブジェクトへの関数呼び出しをラップするだけです。具体的なコード例を以下に示します。再実装されていない場合、BufferedInputStreamクラスは、渡されたInputStreamオブジェクトにデータを読み取る最後のタスクを委任して完了することはできません。この部分は少しわかりにくいので、自分で考えてみてください。
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
// f()函数不需要增强,只是重新调用一下InputStream in对象的f()
public void f() {
in.f();
}
}
実際、DataInputStreamにもBufferedInputStreamと同じ問題があります。コードの重複を避けるために、JavaIOはデコレータの親クラスFilterInputStreamを抽象化しました。コードの実装を以下に示します。InputStreamのすべてのデコレータクラス(BufferedInputStream、DataInputStream)は、このデコレータ親クラスを継承します。このように、デコレータクラスは、拡張する必要のあるメソッドを実装するだけでよく、他のメソッドは、デコレータの親クラスのデフォルトの実装を継承します。
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}
}