目次


Javaの理論と実践

初期化の原子性を使用可能にする

Self-returnイディオムで、より使いやすいAPI設計を

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Javaの理論と実践

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Javaの理論と実践

このシリーズの続きに乞うご期待。

私は1985年の夏、APLでメインフレーム・ベースのビジネス・アプリケーションを開発するという仕事に携わったことがあります。APLを覚えていない人のために言うと、APLというのは非常に簡略化された言語であり、怪しげな記号だらけの特別なキーボードが必要なのですが、配列に保存されたデータを操作するために、非常に強力な機構を備えています。例えば、Conwayの「Life」セルラー・オートマトン・ゲーム(cellular automata game)は、APLコードを使うと30文字から40文字で実装することができ、ある数字以下の素数全てを見つけるプログラムであれば、20文字で書くことができます。(ただし知らない人から見ると、こうしたAPLプログラムは、ほとんど暗号の羅列のように見えます。)APL好きな人はよく、APLを使えばどんなプログラムも1行で書けると冗談を言うものです(ただし、そうしたプログラムを読むのは楽ではありません)。

力比べや、わざわざ混乱しやすくしたプログラミングのコンテストは別として、ある一つのオブジェクトに対して任意のオペレーション・シーケンスを単一表現で実行できるということは、ソフトウェア・エンジニアリングの観点から見て、どのような面で重要なのでしょうか。Java™言語では、複雑なオブジェクト(フィールド初期化子やメソッド・パラメーターなど)を単一表現でインスタンス化、初期化できると、コードが読みやすくなる場合が幾つかあります。さらに、単一表現でインスタンス化、初期化できないと、非常に不便な場合さえあるのです(コンストラクター引数を使って新しいオブジェクトを初期化し、そのオブジェクトを、super()またはthis()コンストラクターに渡すような場合)。

可変性: イエスの場合、ノーの場合、そして時々の場合

一部のオブジェクトは不変(immutable、つまり一旦作られると状態が変化しない)ですが、可変な(mutableな)ものもあります。一部の不変オブジェクトでは、不変性はクラスの実装(例えばStringクラスなど)によって保証されますが、その他の不変オブジェクトでは、単純に習慣や仕様、ドキュメンテーションなどによって不変性が確保されます。(不変オブジェクトの美徳、つまり単純さや安全性、スレッド・セーフなどについては、このコラムや他の場所でも強調されているので、ここではあえて繰り返しません。)不変性は、それが強制されたものであるにせよ、与えられたオブジェクトに対する単なる習慣であるにせよ、その振る舞いは同じです。つまりそのオブジェクトが初期化されると、その状態が再度修正されることはありません。

実体によっては、不変オブジェクト、可変オブジェクトのどちらでも妥当です。例えばStringクラスは不変ですが、それはストリング・クラスを実装する妥当な方法の一つにすぎません。(C++のSTLは可変ストリング・クラスを実装しますが、これもStringを実装する上での、もう一つの妥当な方法と言えます。)他のオブジェクトは、可変オブジェクトでしかあり得ません。例えば、カウンターが不変であるというのは無意味です。厳密に定義すると、可変オブジェクトというのは、作った後でも外から見える形で状態を変更できる任意のオブジェクト、ということです。しかし、状態が変化するか否かという面だけで可変性を定義してしまうと、オブジェクトのライフサイクルにおける重要な区分を見逃してしまうことになります。

可変性のライフサイクル

オブジェクトによっては、そのライフサイクルの期間中ずっと変化し続けるものがあります。例えばカウンターやその他、状態を保持するオブジェクト等です。その他のオブジェクトは、セッターに対する一連のコールによって、あるいはその他の可変メソッドによって必要な状態に初期化され、その後はガーベジ・コレクターに集められるまで変化することはありません。厳密に言うと、これらのオブジェクトは、その状態がコンストラクターの中で完全に設定されず、変化しうるため、可変オブジェクトです。しかし、それを使うプログラムの側から見ると、不変オブジェクトのようなものです。例えば、コンフィギュレーション用のプロパティー・ファイルを保持するためにアプリケーションが使うPropertiesオブジェクトを考えてみてください。アプリケーションの初めの方では、Propertiesオブジェクトはインスタンス化され、ファイルからの値が入っていますが、その後では再度修正されることはありません。このPropertiesオブジェクトのライフサイクルには2つのフェーズがあります。つまり、(可変オブジェクトとして扱われる)初期化フェーズと、(不変オブジェクトとして扱われる)使用フェーズです。このように使われるPropertiesオブジェクトは通常、最初のフェーズが完了するまで、アプリケーションの他の部分に公開されることはありません。ですから、アプリケーションの他の部分から見ると、Propertiesオブジェクトは不変オブジェクトのようなものです。

このようにフェーズが変化する振る舞いは、ごく一般的なものです。SimpleDateFormatなど一部のクラスはほとんどの場合、このように2つのフェーズで使われます。つまり、いったんフォーマット化オプションが設定されると、フォーマッターは何度も使われるのですが、フォーマッターが「完全に初期化」された後は、設定が変更されることはほとんどありません。他のクラス、PropertiesHashMap等は、このように2つのフェーズで使われることもありますが、完全に可変オブジェクトとして扱われる場合も頻繁にあります。この2フェーズ・ライフサイクルを持つオブジェクトに対して確立された名前は無いので、私がIOI(immutable-once-initialized、いったん初期化されると不変)という名前を付けることにしましょう。IOIオブジェクトは一般的に、構築-修正-修正-修正-公開-使用-使用-使用、というようなライフサイクルをたどります。

Self-returnイディオム

API設計者は、どういう場合にオブジェクトがIOI的に使われるかを予見し、使われる場合には初期化が容易になるように「self-returnイディオム」を使うことによって、プログラマーの負担を軽減することができます。self-returnイディオムでは、mutatorメソッド(setXyz()appendFoo())がアクションを実行後に、this参照を返します。StringBufferクラスを見ると、self-returnイディオムが分かります。つまり、内部バッファーの状態を更新した後、全てのappend()メソッドがStringBuffer自体への参照を返すのです。これをリスト1に示します。

リスト1. StringBuffer.append()でのself-returnイディオム
public StringBuffer append(String str) {
    // append str to the internal buffer
    return this;
}

Self-returnイディオムの利点は、複数のコールを別々のステートメントで書く代わりに、次のように一つに連結できることです。

stringBuffer.append("a=").append(a)
    .append("; b=").append(b);

このコードは、別々に4つのステートメントを書くよりも読みやすく、また簡潔です。多くの可変メソッド(セッターやadd()append()など)は、いずれにせよ値は返さないため、self-returnイディオムを使うことによってAPI設計に悪い副作用が現れることはほとんどありません。しかも、コールする側は、ずっと楽になるのです。StringBufferの例に従うようなクラスが無いのは残念です。もしStringBufferと同じようにできれば、一部のクラスはずっと使いやすくなるのですが・・・。

静的初期化

皆さんは、『既知の値を幾つか使ってSetを静的に初期化したいが、プログラムにSetを修正させたくない』と何度も思ったことがありませんか。既存のCollectionsクラスを使うと、静的初期化子ブロックが必要になり、また作られた場所と異なる場所でコレクションを初期化する必要があります。例えば、文書の中で検索しようとしている、ある正規表現パターンでSetを事前初期化したい、としましょう。既存のAPIを使うと、リスト2のようなことをする必要があります。

リスト2. Setを静的に初期化する
private static Set<Pattern> patternSet = new HashSet<Pattern>();
static {
    s.add(Pattern.compile("\b(roast beef)\b"));
    s.add(Pattern.compile("\b(on rye)\b"));
    s.add(Pattern.compile("\b(with mustard)\b"));
}

確かに、静的初期化子を書き、オブジェクトの宣言の近くに置くことは難しいことではありませんが、少しばかり面倒なものです。それに、初期化と宣言が離れれば離れるほど、意図していた不変性が、将来の修正によって崩される可能性が高くなります。しかも、プログラムの観点からpatternSetを不変にしようとすると(小さなコーディング間違いを防げるので、これは良い習慣です)、静的初期化子ブロック内で一時的Setをインスタンス化してCollections.unmodifiableSet()でラップし、ラップされたセットに中身を入れてpatternSettに戻さなければならなくなります。

この場合、Collectionsクラスがself-returnイディオムを使っていたら良かったと言えるでしょう。そうすれば、Setの作成と初期化を全て一つの場所でできたからです。しかし、私達が望むことをしてくれるアダプターを作ることはできるのです。リスト3は、Setの初期化プロセスを単純化するアダプター・クラスを示しています。

リスト3.  self-returningのappend()メソッドを追加するSetアダプター・クラス
public class SetAdapter<T> implements Set<T> {
    private final Set<T> s; public SelfReturnSetAdapter(Set<T> s) { this.s = s; }
    public Set<T> append(T t) { s.add(t); return this; }
    public Set<T> unmodifiableSet() { return Collections.unmodifiableSet(s); }
    // delegate other Set methods to s
}

こうすれば、SetAdapterを使うことによって、パターン・セットをもっと容易に初期化でき、しかも変数の初期化とパターン・セットの初期化を分離せずに済むのです。さらに追加のボーナスとして、パターン・セットを修正不可ラッパーでラップするように最後のコールで行うことによって、パターン・セットを容易に「閉じる」ことができ、しかも相変わらず、一時変数を導入しなくてもpatternSet変数を最終とすることができるのです。透明性による唯一の欠点は、add()メソッドをオーバーライドして値を返すことができないため、私達の可変メソッドには、例えばappend()のような別の名前をつける必要があります。リスト4は、静的初期化子ブロックを使う代わりにSetAdapterを使って、インラインでpatternSetが初期化されることを示しています。

リスト4. SetAdapterを使って、インラインでpatternSetが初期化される
private final static Set<Pattern> patternSet = new SetAdapter(new HashSet<Pattern>())
          .append(Pattern.compile("\b(roast beef)\b"))
          .append(Pattern.compile("\b(on rye)\b"))
          .append(Pattern.compile("\b(with mustard)\b"))
          .unmodifiableSet();

DOM文書をインスタンス化する

DOM APIの設計者がこの概念を理解していれば、XML文書表現の構築はずっと簡単であったはずです(もっとも、DOM APIを批判するのは簡単すぎるのですが)。一例として、ある記事と、それに埋め込まれたリンクを表す、次のようなXML文書を構築したい、という場合を考えてみてください。

<article title="Flossing Penguins - A Dentist's Journey to the Pole"
       author="Jeremy Stringfellow, DMD"
       url="http://www.penguinfloss.com/travel/stringfellow.html">
   <link anchor="Glide Floss" url="http://www.crest.com/glide/index.jsp" />
   <link anchor="Antarctica Facts" url="http://www.cia.gov/cia/publications/factbook/geos/ay.html" />
</article>

DOMでこの文書を構築しようとすると多くの一時変数を伴い、面倒なことになります。文書と記事要素、2つのリンク要素を作り、それらに属性を追加し、その親に要素を付け加えなければなりません。残念ながら、これらの操作はリスト5に示すように、それぞれ別のステートメントでなければなりません。

リスト5. DOM要素をインスタンス化する
Document document = documentFactory.newDocument();
Element articleElement = document.createElement("article");
articleElement.setAttribute("title", article.getTitle());
articleElement.setAttribute("author", article.getAuthor());
articleElement.setAttribute("url", article.getURL());
        
Element linkElement = document.createElement("link");
linkElement.setAttribute("anchor", link.getAnchor());
linkElement.setAttribute("url", link.getURL());
articleElement.appendChild(linkElement);
        
linkElement = document.createElement("link");
linkElement.setAttribute("anchor", anotherLink.getAnchor());
linkElement.setAttribute("url", anotherLink.getURL());
articleElement.appendChild(linkElement);
        
document.appendChild(articleElement);

今度は、このDOMクラスがsetAttribute()appendChild()に対するself-returnイディオムをサポートしていると考えてみてください。そうすると、それぞれの要素は単一の表現で完全に作ることができ、幾つかある一時変数は必要がなくなります。しかもボーナスとして、結果としてできる文書の構造とコードの構造を同じにすることさえできるのです。これをリスト6に示します。

リスト6. 架空のself-returningなDOM APIでDOM要素をインスタンス化する
document.appendChild(
    document.createElement("article")
        .setAttribute("title", article.getTitle())
        .setAttribute("author", article.getAuthor())
        .setAttribute("url", article.getURL())
        .appendChild(
            document.createElement("link")
                .setAttribute("anchor", link.getAnchor())
                .setAttribute("url", link.getURL()))
        .appendChild(
            document.createElement("link")
                .setAttribute("anchor", anotherLink.getAnchor())
                .setAttribute("url", anotherLink.getURL())));

これで大幅にコードが少なくなったわけではありませんが、このAPIがこのように動作すれば、DOM要素全体を単一のステートメントでインスタンス化し、初期化することができます。それによって、DOM要素を返すメソッドを作ったり、可変初期化子の中でDOM要素を初期化したりすることが少し容易に(そして、よりクリアに)になります。また、self-returnイディオムを使わないと、ヘルパー機能を使わずに完全なDOM要素をsuper()this()コンストラクターに渡せないことに注意してください。DOM APIでは単一ステートメントの中で要素を作ることができず、super()this()コンストラクターは、コンストラクター中での最初の要素でなければならないためです。

無精をつなぎ合わせる

self-returnイディオムを使うと、時にはコードの読みやすさが改善され、論理実体を単一表現で完全に初期化できるようになります。つまり、フィールドを初期化する際や、引数をsuper()コンストラクターやthis()コンストラクターに渡す際に、一時変数やヘルパー機能を無くすことができるのです。しかしself-returnイディオムには他にも、無精を決め込むことに由来する利点があります。与えられたAPIを使うために必要な作業の量を減らすことによって、そのAPIが適切かつ効果的に使われる可能性を高めることができるのです。API設計者はこの点をあまり重視しない傾向があるため、開発者は多くの場合、「正しく行う」ことと「まずまず十分に行う」ことのいずれかを選択するように迫られてしまいます。開発者が面倒がってそのAPIを避けてしまわないように、API設計者はAPIを非常に使いやすいものにし、開発者が正しく行うように仕向けるべきなのです。(DOM APIの設計者は、この教訓を理解していません。)APIを設計する際には、IOI的にオブジェクトが作れるユースケースを探し、そうしたオブジェクトを容易に作るための適切な方法を提供すべきです。そして可能であれば、初期化子やスーパークラス・コンストラクター引数の中で使えるように、単一表現で作る方法をユーザーに提供すべきでしょう。同様に、セッターに期待される役割を考えてみてください。そのセッターは可変オブジェクトの状態を更新するための、真のアクセサー(accessors)なのでしょうか。それとも、SimpleDateFormatのように、拡張構築プロセスの一部として使われる可能性が高いのでしょうか。もし後者であれば、セッターがthis参照を返すようにしても、何らコストはかかりません。

無精に関連して言うと、便利なtoString()実装を(デバッグのために強制される前に)クラスに与えることが、どのくらい頻繁にあるでしょうか。確かに、良質なtoString()を書くことは難しいことではありませんが、フィールドがクラスに追加されても無精を決め込んで、こうしたメソッドを書かなかったり、更新しなかったりすることが間々あるものです。本音を言えば、こうしたメソッドは長々としたストリング連結を伴い、書いたり修正したりするのが面倒なのです。

リスト7は、toString()実装を書くための助けとなる、単純なユーティリティー・クラスを示しています。これはStringBufferの上に作られた単純クラスであり、これを使うと、進むにつれてself-returnイディオムを使った単一表現で状態変数を追加しながら、toString()の値を完全構築できるのです。

リスト7.  ToStringクラス
public class ToString {
    private StringBuffer sb = new StringBuffer();
    public ToString(String title) { sb.append(title).append(" "); }
    public ToString(Object o) {     this(o.getClass().getName()); }
    public ToString add(String name, String value) {
        sb.append(name).append("=\"").append(value).append("\" ");
        return this;
    }
    public ToString add(String name, Object value) {
        return add(name, value == null? "null" : value.toString());
    }
    public ToString add(String name, int value) {
        sb.append(name).append("=").append(value).append(" ");
        return this;
    }
    // name-value versions for other primitive types  public ToString addGroup(String name, String value) {
        sb.append(name).append("={").append(value).append("} ");
        return this;
    }
    public ToString add(String name, String[] value) {
        sb.append(name).append("=[");
        for (int i = 0; i < value.length; i++) sb.append("\"").append(value[i]).append("\" ");
        sb.append("] ");
        return this;
    }
    public String toString() {
        return sb.toString();
    }
}

ここでも、結果としてできるtoString()コードは手動で実装したものと大きく異なるわけではなく、少し読みやすく、(特にEclipseのようなIDEでの)編集がしやすくなっただけです。この利点はtoString()を書くための時間が何秒間か節約できたことではなく、無精を決め込んでtoString()メソッドを全く作らないという可能性が少なくなることです。リスト8は、手動による方法と、ToStringを使った方法の両方での、典型的なtoString()メソッドを示しています。(ToStringクラスは、ログ・メッセージとして書くための説明的構造化ストリングを生成するために、toString()メソッドと独立に使うこともできます。)

リスト8. 手動での方法と、ToStringを使った方法でのtoString()
// by hand
public String toString() {
    return "Address " + "streetAddress=" + streetAddress + " "
        + "city=" + city + " " + "state=" + state + " "
        + "zipCode=" + zipCode + " ";
}

// with ToString
public String toString() {
    return new ToString("Address")
        .add("StreetAddress", streetAddress)
        .add("city", city).add("state", state)
        .add("zipCode", zipCode)
        .toString();
}

まとめ

self-returnイディオムは格別奥深いものではなく、革命的な手法でもありませんが、APIの使いやすさを少しずつ改善するものです。オブジェクトが、『いったん初期化後は不変』という形で使われる場合には、そのオブジェクトを単一のステートメントや表現で宣言、初期化できれば非常に便利です。初期化の原子性を許さないクラスの制限を、初期化子ブロックやヘルパー機能を使って回避することは可能ですが、コードが読みにくくなり、しかも不必要に読みにくくなる場合が多いのです。APIを書く際には、オブジェクトの可変性に関するライフサイクルをよく考え、コールする側がself-returnイディオムによって楽にならないかどうかを考えてみてください。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218559
ArticleTitle=Javaの理論と実践: 初期化の原子性を使用可能にする
publish-date=04272005