1. 序章

最近、 Creation Design Patterns と、JVMおよびその他のコアライブラリ内でそれらを見つける場所を調べました。 次に、動作設計パターンを見ていきます。 これらは、オブジェクトが互いにどのように相互作用するか、またはオブジェクトとどのように相互作用するかに焦点を当てています。

2. 責任の連鎖

Chain of Responsibility パターンを使用すると、オブジェクトで共通のインターフェイスを実装し、必要に応じて各実装で次のインターフェイスに委任できます。 これにより、実装のチェーンを構築できます。各実装は、チェーン内の次の要素の呼び出しの前後にいくつかのアクションを実行します

interface ChainOfResponsibility {
    void perform();
}
class LoggingChain {
    private ChainOfResponsibility delegate;

    public void perform() {
        System.out.println("Starting chain");
        delegate.perform();
        System.out.println("Ending chain");
    }
}

ここでは、デリゲート呼び出しの前後に実装が出力される例を見ることができます。

デリゲートに電話する必要はありません。 そうするべきではなく、代わりにチェーンを早期に終了することを決定できます。 たとえば、いくつかの入力パラメータがある場合、それらを検証し、それらが無効である場合は早期に終了することができます。

2.1. JVMでの例

サーブレットフィルタは、このように機能するJEEエコシステムの例です。 単一のインスタンスがサーブレットの要求と応答を受け取り、FilterChainインスタンスはフィルターのチェーン全体を表します。 次に、各自が作業を実行してから、チェーンを終了するか、chain.doFilter()を呼び出して、次のフィルターに制御を渡す必要があります

public class AuthenticatingFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if (!"MyAuthToken".equals(httpRequest.getHeader("X-Auth-Token")) {
             return;
        }
        chain.doFilter(request, response);
    }
}

3. 指示

Command パターンを使用すると、実行時に正しくトリガーできるように、いくつかの具体的な動作(またはコマンド)を共通のインターフェイスの背後にカプセル化できます。

通常、コマンドインターフェイス、コマンドインスタンスを受信するReceiverインスタンス、および正しいコマンドインスタンスの呼び出しを担当するInvokerがあります。 次に、コマンドインターフェイスのさまざまなインスタンスを定義して、レシーバーでさまざまなアクションを実行できます

interface DoorCommand {
    perform(Door door);
}
class OpenDoorCommand implements DoorCommand {
    public void perform(Door door) {
        door.setState("open");
    }
}

ここでは、 Door をレシーバーとして受け取り、ドアを「開く」ようにするコマンド実装があります。 呼び出し側は、特定のドアを開きたいときにこのコマンドを呼び出すことができ、コマンドはこれを行う方法をカプセル化します。

将来的には、 OpenDoorCommand を変更して、ドアが最初にロックされていないことを確認する必要があるかもしれません。 この変更は完全にコマンド内にあり、レシーバークラスとインボーカークラスに変更を加える必要はありません。

3.1. JVMでの例

このパターンの非常に一般的な例は、Swing内のActionクラスです。

Action saveAction = new SaveAction();
button = new JButton(saveAction)

ここで、 SaveAction はコマンドであり、このクラスを使用するSwing JButton コンポーネントは呼び出し元であり、Action実装はActionEvent[で呼び出されます。 X188X]を受信機として。

4. イテレータ

Iteratorパターンを使用すると、コレクション内の要素間で作業し、それぞれを順番に操作できます。 これを使用して、要素がどこから来ているかに関係なく、いくつかの要素に対して任意のイテレータを使用する関数を記述します。 ソースは、順序付きリスト、順序なしセット、または無限ストリームである可能性があります。

void printAll<T>(Iterator<T> iter) {
    while (iter.hasNext()) {
        System.out.println(iter.next());
    }
}

4.1. JVMでの例

すべてのJVM標準コレクションは、iterator()メソッドを公開することによってIteratorパターンを実装しますそれはイテレータコレクション内の要素の上。 ストリームも同じメソッドを実装しますが、この場合、無限のストリームである可能性があるため、イテレータが終了しない可能性があります。

5. Memento

Mementoパターンを使用すると、状態を変更して以前の状態に戻すことができるオブジェクトを作成できます。基本的に、オブジェクトの状態を「元に戻す」機能です。

これは、セッターが呼び出されるたびに前の状態を保存することで、比較的簡単に実装できます。

class Undoable {
    private String value;
    private String previous;

    public void setValue(String newValue) {
        this.previous = this.value;
        this.value = newValue;
    }

    public void restoreState() {
        if (this.previous != null) {
            this.value = this.previous;
            this.previous = null;
        }
    }
}

これにより、オブジェクトに加えられた最後の変更を元に戻すことができます。

これは多くの場合、オブジェクトの状態全体をMementoと呼ばれる単一のオブジェクトにラップすることによって実装されます。 これにより、すべてのフィールドを個別に保存する代わりに、状態全体を1回のアクションで保存および復元できます。

5.1. JVMでの例

JavaServer Facesは、実装者が状態を保存および復元できるようにするStateHolderと呼ばれるインターフェースを提供します。 これを実装するいくつかの標準コンポーネントがあります。たとえば、 HtmlInputFile HtmlInputText HtmlSelectManyCheckbox などの個々のコンポーネントと、などの複合コンポーネントで構成されます。 X235X]HtmlForm。

6. 観察者

Observer パターンを使用すると、オブジェクトは、変更が発生したことを他のユーザーに示すことができます。 通常、サブジェクト(オブジェクトを発行するイベント)と一連のオブザーバー(これらのイベントを受信するオブジェクト)があります。 オブザーバーは、変更について通知を受けたいという件名に登録します。 これが発生すると、サブジェクトで発生した変更により、オブザーバーに通知されます

class Observable {
    private String state;
    private Set<Consumer<String>> listeners = new HashSet<>;

    public void addListener(Consumer<String> listener) {
        this.listeners.add(listener);
    }

    public void setState(String newState) {
        this.state = state;
        for (Consumer<String> listener : listeners) {
            listener.accept(newState);
        }
    }
}

これは一連のイベントリスナーを受け取り、状態が新しい状態値で変化するたびにそれぞれを呼び出します。

6.1. JVMでの例

Javaには、これを正確に実行できる標準のクラスのペアがあります– java.beans.PropertyChangeSupportおよびjava.beans.PropertyChangeListener

PropertyChangeSupport は、オブザーバーを追加および削除したり、状態の変化をすべて通知したりできるクラスとして機能します。 PropertyChangeListener は、発生した変更を受信するためにコードが実装できるインターフェイスです。

PropertyChangeSupport observable = new PropertyChangeSupport();

// Add some observers to be notified when the value changes
observable.addPropertyChangeListener(evt -> System.out.println("Value changed: " + evt));

// Indicate that the value has changed and notify observers of the new value
observable.firePropertyChange("field", "old value", "new value");

java.util.Observerjava.util.Observableという、より適切と思われるクラスの別のペアがあることに注意してください。 ただし、これらは柔軟性がなく信頼性が低いため、Java9では非推奨になっています。

7. ストラテジー

Strategy パターンを使用すると、一般的なコードを記述し、それに特定の戦略をプラグインして、正確なケースに必要な特定の動作を提供できます。

これは通常、戦略を表すインターフェイスを持つことによって実装されます。 クライアントコードは、正確なケースの必要に応じて、このインターフェイスを実装する具象クラスを記述できます。 たとえば、エンドユーザーに通知し、プラグイン可能な戦略として通知メカニズムを実装する必要があるシステムがあるとします。

interface NotificationStrategy {
    void notify(User user, Message message);
}
class EmailNotificationStrategy implements NotificationStrategy {
    ....
}
class SMSNotificationStrategy implements NotificationStrategy {
    ....
}

次に、実行時に、このメッセージをこのユーザーに送信するために実際に使用するこれらの戦略のどれを正確に決定できます。 また、システムの他の部分への影響を最小限に抑えて使用する新しい戦略を作成することもできます。

7.1. JVMでの例

標準のJavaライブラリは、このパターンを広範囲に使用します。多くの場合、最初は明白ではないように思われるかもしれません。 たとえば、Java8で導入されたStreams API は、このパターンを広範囲に使用します。 map() filter()、およびその他のメソッドに提供されるラムダはすべて、汎用メソッドに提供されるプラグ可能な戦略です。

ただし、例はさらに遡ります。 Java1.2で導入されたComparatorインターフェースは、必要に応じてコレクション内の要素をソートするために提供できる戦略です。 コンパレータのさまざまなインスタンスを提供して、必要に応じてさまざまな方法で同じリストを並べ替えることができます。

// Sort by name
Collections.sort(users, new UsersNameComparator());

// Sort by ID
Collections.sort(users, new UsersIdComparator());

8. テンプレートメソッド

テンプレートメソッドパターンは、いくつかの異なるメソッドを連携させて調整する場合に使用されます。 テンプレートメソッドと1つ以上の抽象メソッドのセットを使用して基本クラスを定義します–実装されていないか、デフォルトの動作で実装されています。 次に、テンプレートメソッドはこれらの抽象メソッドを固定パターンで呼び出します。次に、コードはこのクラスのサブクラスを実装し、必要に応じてこれらの抽象メソッドを実装します。

class Component {
    public void render() {
        doRender();
        addEventListeners();
        syncData();
    }

    protected abstract void doRender();

    protected void addEventListeners() {}

    protected void syncData() {}
}

ここには、任意のUIコンポーネントがいくつかあります。 サブクラスは、 doRender()メソッドを実装して、コンポーネントを実際にレンダリングします。 オプションで、 addEventListeners()および syncData()メソッドを実装することもできます。 UIフレームワークがこのコンポーネントをレンダリングすると、3つすべてが正しい順序で呼び出されることが保証されます。

8.1. JVMでの例

Javaコレクションで使用されるAbstractList、AbstractSet、およびAbstractMapには、このパターンの多くの例があります。たとえば、 indexOf()メソッドと lastIndexOf()メソッドはどちらも機能します listIterator()メソッドに関しては、デフォルトの実装がありますが、一部のサブクラスでオーバーライドされます。 同様に、 add(T)メソッドと addAll(int、T)メソッドはどちらも、 add(int、T)メソッドでは機能しません。デフォルトの実装があり、サブクラスで実装する必要があります。

Java IOは、 InputStream OutputStream Reader、、およびWriter内でもこのパターンを利用します。 たとえば、 InputStream クラスには、 read(byte []、int、int)に関して機能するいくつかのメソッドがあり、実装するにはサブクラスが必要です。

9. ビジター

ビジターパターンを使用すると、instanceofチェックに頼ることなく、コードでさまざまなサブクラスをタイプセーフな方法で処理できます。サポートする必要のある具象サブクラスごとに1つのメソッドを持つビジターインターフェイスがあります。 基本クラスには、 accept(Visitor)メソッドがあります。 サブクラスはそれぞれ、この訪問者に対して適切なメソッドを呼び出し、それ自体を渡します。 これにより、これらの各メソッドで具体的な動作を実装できます。各メソッドは、具体的なタイプで機能することを認識しています。

interface UserVisitor<T> {
    T visitStandardUser(StandardUser user);
    T visitAdminUser(AdminUser user);
    T visitSuperuser(Superuser user);
}
class StandardUser {
    public <T> T accept(UserVisitor<T> visitor) {
        return visitor.visitStandardUser(this);
    }
}

ここに、3つの異なるビジターメソッドを備えたUserVisitorインターフェースがあります。 この例のStandardUserは適切なメソッドを呼び出し、AdminUserSuperuserでも同じことが行われます。 次に、必要に応じてこれらを操作する訪問者を作成できます。

class AuthenticatingVisitor {
    public Boolean visitStandardUser(StandardUser user) {
        return false;
    }
    public Boolean visitAdminUser(AdminUser user) {
        return user.hasPermission("write");
    }
    public Boolean visitSuperuser(Superuser user) {
        return true;
    }
}

StandardUser には権限がなく、 Superuser には常に権限があり、 AdminUser には権限がある場合がありますが、これはユーザー自体で検索する必要があります。

9.1. JVMでの例

Java NIO2フレームワークは、 Files.walkFileTree()でこのパターンを使用します。 これには、ファイルツリーのウォークのさまざまな側面を処理するメソッドを持つFileVisitorの実装が必要です。 コードはこれを使用して、ファイルの検索、一致するファイルの印刷、ディレクトリ内の多くのファイルの処理、またはディレクトリ内で機能する必要のあるその他の多くのことを実行できます

Files.walkFileTree(startingDir, new SimpleFileVisitor() {
    public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
        System.out.println("Found file: " + file);
    }

    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println("Found directory: " + dir);
    }
});

10. 結論

この記事では、オブジェクトの動作に使用されるさまざまなデザインパターンについて説明しました。 また、コアJVM内で使用されているこれらのパターンの例も確認したので、多くのアプリケーションがすでに恩恵を受けている方法で使用されていることがわかります。