🎁

java.utilのインターフェースのラッパークラスを作ろう(Decoratorパターン)

2024/12/14に公開

概要

リストを引数に取りラップされたリストを返すメソッドの作り方と、その時に考えたことを話します。
内容はDecoratorパターンを用いた実装です。

対象

既存のインターフェースの実装に対して、共通する特別な動作を追加したい人(例:リストに要素を追加する時に必ずソートするようにするなど)。

きっかけ

最近Javaのコードを書いている時に、常にソートされているコードが欲しいなぁと思ったが、それらしいクラスはなかった[1]

要件

  • リストに変更を加えた後にもリストがソートされていて欲しい。

却下された案

まずは、考えはしたけど却下された案について書いていきます。

一番最初に考えたコードはこれでした。

public class AlignedList<E> extends AbstractList<E> {
    private List<E> list;
    private Comparator<? super E> comparator;

    public AlignedList(Comparator<? super E> comparator) {
        this.list = new ArrayList<E>();
    }
    :
    :

しかし、この書き方では、AlignedListクラスで扱うリストがArrayListに依存してしまうという制約が生じます。
これは望ましくありません。なぜなら、使われるリストの構造それ自体は整列されているという要件とは全く別の要件で選択されるはずだからです。すなわち、ArrayListでもLinkedListでも、そのほかどんなListの実装にも対応ができて欲しいと私は考えました[2]

そこでラッパークラスの考えへとつながります。

synchronizedListというメソッド

ここで一度脇道にそれて、synchronizedListというメソッドの話をします。

まず初めに、Listの実装はスレッドセーフであることを保証しません。現にArrayListなどはスレッドセーフではありません。

この実装はsynchronizedされません。複数のスレッドが並行してArrayListインスタンスにアクセスし、それらのスレッドの少なくとも1つがリストを構造的に変更する場合は、外部でその同期をとる必要があります。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/ArrayList.html

しかし、ListではsynchronizedListというメソッドを用いることで入力として与えたリストに対応するスレッドセーフなリストを得ることができます。

指定されたリストに連動する同期(スレッドセーフな)リストを返します。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/Collections.html#synchronizedList-java.util.List-

このメソッドで用いられている考えがクラスをラップするというものです。

このような、ある値をラップするクラスをラッパークラスと呼びます[3]

そして、このデザインパターンにはDecoratorパターンという名前がついています。

実際にやった案

前章のsynchronizedListメソッドの例とDecoratorパターンを参考にしてクラスを作ります。
この時、匿名クラスで作るかprivateクラスとして作るかのどちらかを選ぶ必要がありましたが、今回はsynchronizedListメソッドがSynchronizedRandomAccessListを返すことに倣い、privateクラスを返すこととして、そのクラス名はAlignedListとしました。

private static class AlignedList<E> extends AbstractList<E> {
    private final List<E> list;
    private final Comparator<? super E> comparator;

    private AlignedList(List<E> list, Comparator<? super E> comparator) {
        this.list = list;
        this.comparator = comparator;
        list.sort(comparator);
    }
    :
    :

具体的な処理に関してはほとんど委譲するのみであり、独自の振る舞いを追加したい箇所のメソッドのみ適当な形で編集したら良いでしょう。もしスケルトン実装がある場合は、それを継承して実装するのも良いでしょう。

実際に考えられそうな利用例

  • デバッグのために変化があった時に都度ログを出力したい
  • 常にソートされているリストが欲しい
  • リストの変更履歴を保持したい

他にも色々な場面で使えそうな気はします。

ラッパーを実装する時に気を付けること

もしインターフェースの実装をするのであれば、適当な形で必要なメソッドを実装する必要もあります。
今回の例のようにインタフェースのスケルトン実装がある場合にも、気を付けるべき点があります。

今回の例ではsetaddremoveはオプション操作であり、必ずしもオーバーライドする必要はありませんが、実際にラップされるインスタンスのクラスはこれらが実装されているであろう点を考えると適当な形でオーバーライドしておくべきでしょう。

ラッパークラスのメリット、デメリット

最後にラッパークラスのメリット、デメリットについて考えていきましょう。

メリット

振る舞いの追加が容易になる

実際に新しく子クラスなどをつくることなく、メソッドを通すだけで新しい振る舞いを追加することが可能となります。

特定の実装に依存しない

実装の内情はほとんど委譲であり、インターフェースを介して操作をするため、特定の実装に依存することがありません。

デメリット

ラップ前のインスタンスへアクセスすると不具合が発生する

インスタンスに対してラップをした場合、ラップされる元となったインスタンスへの直接の変更は、ラップの影響を受けないため、使用者が注意をする必要があります

脚注
  1. 正確にはSortedList<E>というクラスが存在したが、add​(int index, E element)をオーバーライドしてないので除外した。 ↩︎

  2. 予め決めた要件は願望をかなえるのに不十分でした。 ↩︎

  3. 一般的にはプリミティブ型の値をラップするクラスのみを指す場合もあります。 ↩︎

Discussion