62 | Chain of Responsibility パターン (パート 1): アルゴリズムを柔軟に拡張できる機密情報フィルタリング フレームワークを実装するには?
前のレッスンでは、テンプレート モードと戦略モードを学習しましたが、今日は責任連鎖モードを学習します。これらの 3 つのモードには同じ機能があります: 再利用と拡張は、実際のプロジェクト開発、特にフレームワーク開発でより一般的に使用されます。これらを使用して、フレームワークの拡張ポイントを提供し、フレームワークのユーザーが変更せずに使用できるようにします。フレームワーク ソースコードの場合、フレームワークの機能は拡張ポイントに基づいてカスタマイズされます。
今日は主に責任連鎖モデルの原理と実装について説明します。さらに、一連の責任モデルを使用して、アルゴリズムを柔軟に拡張できる機密性の高い単語フィルタリング フレームワークを実装します。次のレッスンでは、実際の戦闘に近づきます. Servlet Filter と Spring Interceptor を分析することで、責任連鎖パターンを使用して、フレームワークで一般的に使用されるフィルターとインターセプターを実装する方法を確認します.
早速、今日から本格的に勉強を始めましょう!
責任連鎖パターンの原則と実装
責任連鎖モデルの英訳は、Chain Of Responsibility Design Pattern です。GoF の「デザイン パターン」では、次のように定義されています。
リクエストを処理する機会を複数のオブジェクトに与えることで、リクエストの送信者と受信者を結び付けないようにします。受信オブジェクトをチェーンし、オブジェクトが処理するまでチェーンに沿ってリクエストを渡します。
中国語に翻訳すると、リクエストの送信と受信を分離して、複数の受信オブジェクトがリクエストを処理する機会を持つことができます。これらの受信オブジェクトをチェーンにつなぎ、チェーン内の受信オブジェクトの 1 つが処理できるようになるまで、チェーンに沿ってリクエストを渡します。
これはかなり抽象的ですので、さらにわかりやすい言葉で説明します。
Chain of Responsibility パターンでは、複数のプロセッサ (つまり、定義で言及した「受信オブジェクト」) が同じ要求を順番に処理します。要求は、最初に A プロセッサによって処理され、次に B プロセッサに渡され、B プロセッサが処理された後に C プロセッサに渡されるというようにチェーンを形成します。チェーン内の各プロセッサは、独自の処理責任を負うため、責任チェーン モードと呼ばれます。
一連の責任モデルについて、コードの実装を見てみましょう。コードの実装と組み合わせると、その定義を理解しやすくなります。責任連鎖モードを実装するには多くの方法がありますが、ここでは一般的に使用される 2 つの方法を紹介します。
最初の実装を以下に示します。その中で、Handler はすべてのプロセッサ クラスの抽象親クラスであり、handle() は抽象メソッドです。各特定のハンドラ クラス (HandlerA、HandlerB) の handle() 関数のコード構造は似ています. リクエストを処理できる場合は、それを渡し続けません; 処理できない場合は、次のプロセッサ (つまり、successor.handle() の呼び出し)。HandlerChain はプロセッサ チェーンであり、データ構造の観点からは、チェーンの先頭と末尾を記録する連結リストです。その中で、レコード チェーン テールは、プロセッサの追加の便宜のためです。
public abstract class Handler {
protected Handler successor = null;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public abstract void handle();
}
public class HandlerA extends Handler {
@Override
public void handle() {
boolean handled = false;
//...
if (!handled && successor != null) {
successor.handle();
}
}
}
public class HandlerB extends Handler {
@Override
public void handle() {
boolean handled = false;
//...
if (!handled && successor != null) {
successor.handle();
}
}
}
public class HandlerChain {
private Handler head = null;
private Handler tail = null;
public void addHandler(Handler handler) {
handler.setSuccessor(null);
if (head == null) {
head = handler;
tail = handler;
return;
}
tail.setSuccessor(handler);
tail = handler;
}
public void handle() {
if (head != null) {
head.handle();
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
実際、上記のコードの実装は十分にエレガントではありません。プロセッサ クラスの handle() 関数には、独自のビジネス ロジックが含まれているだけでなく、次のプロセッサへの呼び出し、つまりコード内の successor.handle() も含まれています。このコード構造に慣れていないプログラマは、新しいプロセッサ クラスを追加するときに、handle() 関数で successor.handle() を呼び出すのを忘れる可能性が高く、コードにバグが発生します。
この問題に対応して、コードをリファクタリングし、テンプレート パターンを使用して、successor.handle() を呼び出すロジックを特定のハンドラー クラスから分離し、抽象親クラスに配置しました。このように、特定のプロセッサ クラスは独自のビジネス ロジックを実装するだけで済みます。リファクタリング後のコードは次のようになります。
public abstract class Handler {
protected Handler successor = null;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public final void handle() {
boolean handled = doHandle();
if (successor != null && !handled) {
successor.handle();
}
}
protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
// HandlerChain和Application代码不变
2 番目の実装を見てみましょう。コードは次のとおりです。この実装はより単純です。HandlerChain クラスは、リンクされたリストの代わりに配列を使用してすべてのプロセッサを保存し、HandlerChain の handle() 関数で各プロセッサの handle() 関数を順番に呼び出す必要があります。
public interface IHandler {
boolean handle();
}
public class HandlerA implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerChain {
private List<IHandler> handlers = new ArrayList<>();
public void addHandler(IHandler handler) {
this.handlers.add(handler);
}
public void handle() {
for (IHandler handler : handlers) {
boolean handled = handler.handle();
if (handled) {
break;
}
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
GoF によって与えられた定義では、プロセッサ チェーン内のプロセッサが要求を処理できる場合、要求を渡し続けることはありません。実際、責任連鎖モードには別のバリエーションがあります。つまり、要求はすべてのプロセッサによって処理され、途中で終了することはありません。このバリアントにも 2 つの実装があります。リンクされたリストを使用してプロセッサを格納し、配列を使用してプロセッサを格納します。これらは上記の 2 つの実装に似ており、わずかに変更するだけで済みます。
以下に示すように、ここでは実装方法の 1 つだけを示します。別の方法として、上記の実装に従って自分で変更することもできます。
public abstract class Handler {
protected Handler successor = null;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public final void handle() {
doHandle();
if (successor != null) {
successor.handle();
}
}
protected abstract void doHandle();
}
public class HandlerA extends Handler {
@Override
protected void doHandle() {
//...
}
}
public class HandlerB extends Handler {
@Override
protected void doHandle() {
//...
}
}
public class HandlerChain {
private Handler head = null;
private Handler tail = null;
public void addHandler(Handler handler) {
handler.setSuccessor(null);
if (head == null) {
head = handler;
tail = handler;
return;
}
tail.setSuccessor(handler);
tail = handler;
}
public void handle() {
if (head != null) {
head.handle();
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
責任連鎖パターンの適用シナリオの例
Chain of Responsibility パターンの原則と実装が完了したので、実際の例を通して、Chain of Responsibility パターンのアプリケーション シナリオについて学びましょう。
UGC (ユーザー生成コンテンツ、ユーザー生成コンテンツ) をサポートするアプリケーション (フォーラムなど) の場合、ユーザー生成コンテンツ (フォーラムで公開された投稿など) には、センシティブな言葉 (ポルノ、広告、反動的など) が含まれている場合があります。 )。このアプリケーション シナリオでは、一連の責任パターンを使用して、これらの機密用語をフィルター処理できます。
センシティブな言葉を含むコンテンツについては、直接公開を禁止する方法と、センシティブな言葉をモザイク処理(例えば、センシティブな言葉を***に置き換える)してから公開する方法の2つの方法があります。最初の処理方法は GoF によって与えられた責任連鎖モデルの定義に準拠し、2 番目の処理方法は責任連鎖モデルの変形です。
以下に示すように、ここでは最初の実装のコード例のみを示します。コード実装のスケルトンのみを示します。特定のセンシティブ ワード フィルタリング アルゴリズムは示しません。他のコラムを参照してください。 「データ構造とアルゴリズムの美しさ」のマルチモード文字列マッチングは、それ自体で実装されています。
public interface SensitiveWordFilter {
boolean doFilter(Content content);
}
public class SexyWordFilter implements SensitiveWordFilter {
@Override
public boolean doFilter(Content content) {
boolean legal = true;
//...
return legal;
}
}
// PoliticalWordFilter、AdsWordFilter类代码结构与SexyWordFilter类似
public class SensitiveWordFilterChain {
private List<SensitiveWordFilter> filters = new ArrayList<>();
public void addFilter(SensitiveWordFilter filter) {
this.filters.add(filter);
}
// return true if content doesn't contain sensitive words.
public boolean filter(Content content) {
for (SensitiveWordFilter filter : filters) {
if (!filter.doFilter(content)) {
return false;
}
}
return true;
}
}
public class ApplicationDemo {
public static void main(String[] args) {
SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain();
filterChain.addFilter(new AdsWordFilter());
filterChain.addFilter(new SexyWordFilter());
filterChain.addFilter(new PoliticalWordFilter());
boolean legal = filterChain.filter(new Content());
if (!legal) {
// 不发表
} else {
// 发表
}
}
}
上記の実装を読んだ後、次のようなセンシティブ ワード フィルタリング機能も実装できます。コードはより単純ですが、なぜ責任連鎖モードを使用する必要があるのでしょうか。これはオーバーエンジニアリングですか?
public class SensitiveWordFilter {
// return true if content doesn't contain sensitive words.
public boolean filter(Content content) {
if (!filterSexyWord(content)) {
return false;
}
if (!filterAdsWord(content)) {
return false;
}
if (!filterPoliticalWord(content)) {
return false;
}
return true;
}
private boolean filterSexyWord(Content content) {
//....
}
private boolean filterAdsWord(Content content) {
//...
}
private boolean filterPoliticalWord(Content content) {
//...
}
}
アプリケーションの設計パターンは、主にコードの複雑さに対処し、オープンとクローズの原則を満たし、コードのスケーラビリティを向上させるものであると、これまで何度も述べてきました。ここで一連の責任パターンを適用することも例外ではありません。実際、戦略、同様の質問についても話しました。たとえば、なぜ戦略パターンを使用するのですか? そのときの理由は、現在責任連鎖モデルを適用する理由とほぼ同じであり、当時の説明と合わせて見ることができます。
まず、Chain of Responsibility パターンがコードの複雑さをどのように処理するかを見てみましょう。
コード ロジックの大きなブロックを関数に分割し、大きなクラスを小さなクラスに分割することは、コードの複雑さに対処する一般的な方法です。一連の責任モデルを適用して、機密性の高い各単語フィルター関数を分割し、独立したクラスに設計して、SensitiveWordFilter クラスをさらに簡素化し、SensitiveWordFilter クラスのコードが多すぎたり複雑すぎたりしないようにします。
次に、一連の責任モデルによってコードがオープンとクローズの原則を満たし、コードのスケーラビリティを向上させる方法を見てみましょう。
たとえば、新しいフィルタリング アルゴリズムを拡張する場合は、特殊なシンボルもフィルタリングする必要があります. 非責任チェーン モードのコード実装に従って、SensitiveWordFilter のコードを変更する必要があります。閉鎖。ただし、そのような変更は比較的集中しており、許容されます。責任連鎖モードの実装はより洗練されています. 新しい Filter クラスを追加し、それを addFilter() 関数を介して FilterChain に追加するだけで済みます. 他のコードを変更する必要はまったくありません.
ただし、Chain of Responsibility パターンを使用して実装したとしても、新しいフィルタリング アルゴリズムを追加する場合は、クライアント コード (ApplicationDemo) を変更する必要があり、開閉の原則に完全には準拠していないと言うかもしれません。 .
実際、これを改良すると、上記のコードをフレームワーク コードとクライアント コードの 2 つのカテゴリに分けることができます。その中で、ApplicationDemo はクライアント コード、つまりフレームワークを使用するコードに属します。ApplicationDemo 以外のコードは、機密単語フィルタリング フレームワーク コードに属します。
センシティブ ワード フィルタリング フレームワークは、当社が開発および保守しているものではなく、当社が導入したサード パーティのフレームワークであると仮定すると、新しいフィルタリング アルゴリズムを拡張したいと考えており、フレームワークのソース コードを直接変更することは不可能です。このとき、冒頭で述べたことを達成するために責任連鎖モデルを使用できます.フレームワークのソースコードを変更することなく、責任連鎖モデルによって提供される拡張ポイントに基づいて新しい機能を拡張できます. つまり、フレームワークのコード スコープ内でオープン/クローズの原則を実装します。
さらに、責任チェーン モードを使用すると、責任チェーンを使用しない実装方法と比較して別の利点があります。つまり、フィルタリング アルゴリズムの構成がより柔軟になり、特定のフィルタリング アルゴリズムの使用のみを選択できます。
キーレビュー
では、本日の内容は以上です。集中する必要があることをまとめて一緒に確認しましょう。
Chain of Responsibility パターンでは、複数のプロセッサが同じ要求を順番に処理します。要求は、最初に A プロセッサによって処理され、次に B プロセッサに渡され、B プロセッサが処理された後に C プロセッサに渡されるというようにチェーンを形成します。チェーン内の各プロセッサは、独自の処理責任を負うため、責任チェーン モードと呼ばれます。
GoF の定義では、プロセッサが要求を処理できるようになると、後続のプロセッサに要求を渡し続けることはありません。もちろん、実際の開発では、このモードのバリエーションもあります。つまり、リクエストは途中で終了せず、すべてのプロセッサで処理されます。
Chain of Responsibility パターンには 2 つの一般的な実装があります。1 つは連結リストを使用してプロセッサを格納する方法で、もう 1 つは配列を使用してプロセッサを格納する方法で、後者の実装の方が簡単です。
クラスディスカッション
今日は、責任連鎖モデルの使用について話しました。フレームワーク コードをオープンとクローズの原則に適合させることができます。新しいハンドラーを追加するには、クライアント コードを変更するだけです。コードを変更せずに、クライアント コードも開閉の原則を満たしたい場合は、どうすればよいでしょうか。
メッセージを残して、あなたの考えを私と共有してください。何かを得た場合は、この記事を友達と共有してください。