なぜ副官が必要なのでしょうか?
このパターンは、オブジェクトへのアクセス制御に関連する問題の解決に役立ちます。「なぜそのような制御されたアクセスが必要なのでしょうか?」という疑問があるかもしれません。何が何であるかを理解するのに役立ついくつかの状況を見てみましょう。例1
大量の古いコードを含む大規模なプロジェクトがあり、データベースからレポートをダウンロードするクラスがあると想像してみましょう。このクラスは同期的に動作します。つまり、データベースがリクエストを処理している間、システム全体はアイドル状態になります。レポートは平均して 30 分で作成されます。この機能により、アップロードは 00:30 に開始され、管理者は朝にこのレポートを受け取ります。分析中に、レポートは生成後すぐに、つまり 1 日以内に受け取る必要があることが判明しました。システムはデータベースからの応答を待つため、開始時刻を再スケジュールすることはできません。解決策は、アップロードとレポート生成を別のスレッドで開始することで動作原理を変更することです。このソリューションにより、システムは通常どおり動作し、管理者は最新のレポートを受け取ることができます。ただし、問題があります。現在のコードの関数はシステムの他の部分で使用されているため、書き換えることはできません。この場合、Deputy パターンを使用して中間プロキシ クラスを導入できます。このクラスは、レポートのアップロード要求を受け取り、開始時刻を記録し、別のスレッドを起動します。レポートが生成されると、スレッドは作業を完了し、全員が満足するでしょう。例 2
開発チームはポスター Web サイトを作成します。新しいイベントに関するデータを取得するには、特別なクローズド ライブラリを通じて実装されるサードパーティ サービスを利用します。開発中に問題が発生しました。サードパーティ システムは 1 日に 1 回データを更新し、ユーザーがページを更新するたびにそのシステムへのリクエストが発生します。これにより、大量のリクエストが作成され、サービスが応答を停止します。解決策は、サービス応答をキャッシュし、再起動するたびに保存された結果を訪問者に提供し、必要に応じてこのキャッシュを更新することです。この場合、Deputy パターンを使用することは、完成した機能を変更することなく優れた解決策となります。パターンの仕組み
このパターンを実装するには、プロキシ クラスを作成する必要があります。サービス クラス インターフェイスを実装し、クライアント コードの動作をシミュレートします。したがって、クライアントは実際のオブジェクトの代わりに、そのプロキシと対話します。通常、すべてのリクエストはサービス クラスに渡されますが、その呼び出しの前後に追加のアクションが行われます。簡単に言うと、このプロキシ オブジェクトは、クライアント コードとターゲット オブジェクトの間の層です。非常に遅い古いディスクからのリクエストをキャッシュする例を見てみましょう。これを、動作原理を変更できない古代のアプリケーションでの電車の時刻表にたとえてみましょう。更新されたスケジュールを含むディスクは、毎日決まった時刻に挿入されます。したがって、次のようになります。- インターフェース
TimetableTrains
。 TimetableElectricTrains
このインターフェースを実装するクラス。- クライアント コードがディスク ファイル システムと対話するのは、このクラスを通じてです。
- クライアントクラス
DisplayTimetable
。そのメソッドはprintTimetable()
クラス メソッドを使用しますTimetableElectricTrains
。
printTimetable()
クラスはTimetableElectricTrains
ディスクにアクセスし、データをアンロードしてクライアントに提供します。このシステムはうまく機能しますが、非常に遅いです。したがって、キャッシュ メカニズムを追加することでシステムのパフォーマンスを向上させることが決定されました。これは、プロキシ パターンを使用して実行できます。 この方法では、クラスは、前のクラスではなく、DisplayTimetable
そのクラスと対話していることにさえ気づきませんTimetableElectricTrainsProxy
。新しい実装では、スケジュールを 1 日に 1 回ロードし、リクエストが繰り返されると、すでにロードされているオブジェクトをメモリから返します。
どのようなタスクにプロキシを使用する方が良いですか?
このパターンが確実に役立つ状況をいくつか示します。- キャッシング。
- 遅延実装は、遅延実装とも呼ばれます。必要に応じてオブジェクトをロードできるのに、なぜ一度にオブジェクトをロードするのでしょうか?
- リクエストのログ記録。
- 暫定的なデータとアクセスのチェック。
- 並列処理スレッドを起動します。
- 通話履歴を記録またはカウントします。
長所と短所
- + サービス オブジェクトへのアクセスを必要に応じて制御できます。
- + サービスオブジェクトのライフサイクルを管理するための追加機能。
- + サービス オブジェクトなしで動作します。
- + コードのパフォーマンスとセキュリティが向上します。
- - 追加の処理により性能が低下するリスクがあります。
- ・プログラムのクラス構造が複雑になる。
実際の代替パターン
ディスクから列車の時刻表を読み取るシステムを一緒に実装してみましょう。public interface TimetableTrains {
String[] getTimetable();
String getTrainDepartureTime();
}
メインインターフェイスを実装するクラス:
public class TimetableElectricTrains implements TimetableTrains {
@Override
public String[] getTimetable() {
ArrayList<String> list = new ArrayList<>();
try {
Scanner scanner = new Scanner(new FileReader(new File("/tmp/electric_trains.csv")));
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
list.add(line);
}
} catch (IOException e) {
System.err.println("Error: " + e);
}
return list.toArray(new String[list.size()]);
}
@Override
public String getTrainDepartureTime(String trainId) {
String[] timetable = getTimetable();
for(int i = 0; i<timetable.length; i++) {
if(timetable[i].startsWith(trainId+";")) return timetable[i];
}
return "";
}
}
すべての列車の時刻表を取得しようとするたびに、プログラムはディスクからファイルを読み取ります。しかし、これらはまだ花です。このファイルは、1 つの列車の時刻表を取得する必要があるときにも読み取られます。このようなコードが悪い例にのみ存在するのは良いことです :) クライアント クラス:
public class DisplayTimetable {
private TimetableTrains timetableTrains = new TimetableElectricTrains();
public void printTimetable() {
String[] timetable = timetableTrains.getTimetable();
String[] tmpArr;
System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути");
for(int i = 0; i < timetable.length; i++) {
tmpArr = timetable[i].split(";");
System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
}
}
}
ファイル例:
9B-6854;Лондон;Прага;13:43;21:15;07:32
BA-1404;Париж;Грац;14:25;21:25;07:00
9B-8710;Прага;Вена;04:48;08:49;04:01;
9B-8122;Прага;Грац;04:48;08:49;04:01
テストしてみましょう:
public static void main(String[] args) {
DisplayTimetable displayTimetable = new DisplayTimetable();
displayTimetable.printTimetable();
}
結論:
Поезд Откуда Куда Время отправления Время прибытия Время в пути
9B-6854 Лондон Прага 13:43 21:15 07:32
BA-1404 Париж Грац 14:25 21:25 07:00
9B-8710 Прага Вена 04:48 08:49 04:01
9B-8122 Прага Грац 04:48 08:49 04:01
次に、パターンを実装する手順を見てみましょう。
-
元のオブジェクトの代わりに新しいプロキシを使用できるようにするインターフェイスを定義します。この例では、それは です
TimetableTrains
。 -
プロキシクラスを作成します。サービス オブジェクトへの参照が含まれている必要があります (クラスで作成するか、コンストラクターで渡します)。
プロキシ クラスは次のとおりです。
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { return timetableTrains.getTimetable(); } @Override public String getTrainDepartureTime(String trainId) { return timetableTrains.getTrainDepartureTime(trainId); } public void clearCache() { timetableTrains = null; } }
この段階では、元のオブジェクトへの参照を持つクラスを作成し、すべての呼び出しをそれに渡します。
-
プロキシクラスのロジックを実装します。基本的に、呼び出しは常に元のオブジェクトにリダイレクトされます。
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } return timetableCache; } @Override public String getTrainDepartureTime(String trainId) { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } for(int i = 0; i < timetableCache.length; i++) { if(timetableCache[i].startsWith(trainId+";")) return timetableCache[i]; } return ""; } public void clearCache() { timetableTrains = null; } }
このメソッドは、
getTimetable()
スケジュール配列がメモリにキャッシュされているかどうかを確認します。そうでない場合は、ディスクからデータをロードするリクエストを発行し、結果を保存します。リクエストがすでに実行中の場合は、すぐにメモリからオブジェクトが返されます。シンプルな機能のおかげで、getTrainDepartireTime() メソッドを元のオブジェクトにリダイレクトする必要はありませんでした。その機能を新しいメソッドに複製しただけです。
そんなことはできません。コードを複製したり、同様の操作を実行したりする必要がある場合、それは何か問題が発生したことを意味しており、問題を別の角度から見る必要があります。この単純な例では他に方法はありませんが、実際のプロジェクトでは、コードはより正確に記述される可能性が高くなります。
-
クライアント コード内の元のオブジェクトの作成を置換オブジェクトに置き換えます。
public class DisplayTimetable { // Измененная link private TimetableTrains timetableTrains = new TimetableElectricTrainsProxy(); public void printTimetable() { String[] timetable = timetableTrains.getTimetable(); String[] tmpArr; System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути"); for(int i = 0; i<timetable.length; i++) { tmpArr = timetable[i].split(";"); System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]); } } }
検査
Поезд Откуда Куда Время отправления Время прибытия Время в пути 9B-6854 Лондон Прага 13:43 21:15 07:32 BA-1404 Париж Грац 14:25 21:25 07:00 9B-8710 Прага Вена 04:48 08:49 04:01 9B-8122 Прага Грац 04:48 08:49 04:01
素晴らしい、正しく動作します。
特定の条件に応じて、元のオブジェクトと置換オブジェクトの両方を作成するファクトリを検討することもできます。
ドットの代わりに便利なリンク
-
パターンと「代理」についての優れた記事
GO TO FULL VERSION