今日は、主に責任連鎖モデルの原理と実現について説明します。さらに、責任の連鎖モデルを使用して、アルゴリズムを柔軟に拡張できる機密性の高い単語フィルタリングフレームワークを実装します。次のレッスンでは、ServletFilterとSpringInterceptorを分析することにより、実際の戦闘に近づきます。責任の連鎖モデルを使用して、フレームワークで一般的に使用されるフィルターとインターセプターを実装する方法を説明します。
責任連鎖モデルの原理と実現
責任の連鎖モデルの英語訳は、責任の連鎖設計パターンです。GoFの「デザインパターン」では、次のように定義されています。
複数のオブジェクトにリクエストを処理する機会を与えることにより、リクエストの送信者をその受信者に結合することを避けてください。受信オブジェクトをチェーンし、オブジェクトがそれを処理するまでチェーンに沿ってリクエストを渡します。
中国語に翻訳すると、リクエストの送信と受信が分離され、複数の受信者がリクエストを処理できるようになります。これらの受信オブジェクトをチェーンにストリングし、チェーン上の特定の受信オブジェクトが処理できるようになるまで、このチェーンに沿って要求を渡します。
これはより抽象的なので、より理解しやすい言葉でさらに解釈します。
責任連鎖モデルでは、複数のプロセッサ(つまり、現在の定義の「受信オブジェクト」)が同じ要求を順番に処理します。リクエストは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();
}
}
責任連鎖モデルのアプリケーションシナリオの例
責任の連鎖パターンの原理と実装が完了したので、実際の例を使用して、責任の連鎖パターンのアプリケーションシナリオを学習しましょう。
UGC(ユーザー生成コンテンツ)をサポートするアプリケーション(フォーラムなど)の場合、ユーザー生成コンテンツ(フォーラムで公開された投稿など)には、機密性の高い単語(ポルノ、広告、反論など)が含まれる場合があります。単語)。このアプリケーションシナリオでは、責任連鎖モデルを使用して、これらの機密性の高い単語をフィルタリングできます。
デリケートな単語を含むコンテンツの場合、2つの方法で対処できます。1つは公開を直接禁止する方法、もう1つは公開する前に機密性の高い単語をモザイク化する方法(たとえば、***を使用して機密性の高い単語を置き換える)です。最初の処理方法は、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) {
//...
}
}
これまで何度も言ってきたように、アプリケーションの設計パターンは、主にコードの複雑さに対処し、オープンとクローズの原則を満たし、コードのスケーラビリティを向上させることです。責任の連鎖モデルも例外ではありません。実際、戦略モードについて説明するときに、同様の質問についても話しました。たとえば、なぜ戦略モードを使用するのですか?その際の理由は、責任連鎖モデルを適用した理由とほぼ同じであり、当時の説明と併せてご覧いただけます。
まず、責任の連鎖モデルがコードの複雑さにどのように対応するかを見てみましょう。
コードロジックの大きなブロックを関数に分割し、大きなカテゴリを小さなカテゴリに分割することは、コードの複雑さに対処するための一般的な方法です。責任連鎖モデルを適用して、引き続き各機密単語フィルタリング関数を分離し、独立したクラスに設計します。これにより、SensitiveWordFilterクラスがさらに簡素化され、SensitiveWordFilterクラスのコードが複雑になりすぎないようになります。
次に、責任の連鎖モデルによって、コードがオープンとクローズの原則を満たし、コードのスケーラビリティを向上させる方法を見てみましょう。
たとえば、新しいフィルタリングアルゴリズムを拡張する場合は、特別なシンボルもフィルタリングする必要があります。無責任チェーンモードのコード実装に従って、SensitiveWordFilterのコードを変更する必要があります。これは、オープンとクローズの原則に違反します。ただし、そのような修正はかなり集中しており、受け入れられます。責任連鎖モデルの実装はより洗練されています。新しいFilterクラスを追加し、addFilter()関数を介してFilterChainに追加するだけです。他のコードを変更する必要はまったくありません。
ただし、責任連鎖モデルを使用して実装した場合でも、新しいフィルタリングアルゴリズムを追加する場合は、クライアントコード(ApplicationDemo)を変更する必要があります。これは、オープンとクローズの原則に完全には準拠していません。
実際、詳細には、上記のコードをフレームワークコードとクライアントコードの2つのカテゴリに分類できます。その中で、ApplicationDemoはクライアントコード、つまりフレームワークを使用するコードに属しています。ApplicationDemo以外のコードは、機密性の高い単語フィルタリングフレームワークコードに属しています。
機密性の高い単語フィルタリングフレームワークが私たちによって開発および保守されているのではなく、私たちが導入したサードパーティのフレームワークであると仮定すると、新しいフィルタリングアルゴリズムを拡張する必要があり、フレームワークのソースコードを直接変更することはできません。現時点では、責任連鎖モデルを使用して、新しい機能を拡張する責任連鎖モデルによって提供される拡張ポイントに基づいて、フレームワークのソースコードを変更することなく、最初に述べたことを実現できます。言い換えれば、フレームワークのコードスコープ内にオープンとクローズの原則を実装しました。
さらに、責任の連鎖モデルの使用には、責任の連鎖の実装に比べて別の利点があります。つまり、構成フィルターアルゴリズムがより柔軟であり、いくつかのフィルターアルゴリズムの使用のみを選択できます。