Skip to content

Instantly share code, notes, and snippets.

@e10dokup
Last active May 26, 2016 01:10
Show Gist options
  • Save e10dokup/0851d80c5f5cbca5347d2939ea7ae8f5 to your computer and use it in GitHub Desktop.
Save e10dokup/0851d80c5f5cbca5347d2939ea7ae8f5 to your computer and use it in GitHub Desktop.
輪講第8章

第8章 クライアント用とプロバイダ用のAPIを分離

はじめに

Q.APIって2つ以上あるの?
A.はい

ex. X Multimedia System (XMMS) 用のAPIを想定

  • オーディオファイルの再生
  • 次の曲へスキップ
  • 前の曲へ戻る
  • プレイリストを提供する
    • 曲の追加
    • 曲の削除
    • 並べ替え

本来はユーザのために用意されているが,APIを通じて他のプログラムからのアクセスが可能 -> クライアントAPI

ちなみにサードパーティの出力プラグインを登録することが可能

  • ディスクに書き込む
  • ネットワークを介して放送する

これらの機能ではプレイヤ自身が他とコミュニケーションを開始する -> これはクライアントの動きではない. プロバイダAPIっていいたいんだけど SPI:Service Provider Interface とする

8.1 CとJavaでAPI/SPIを表現する

まずCで書いてから,同じものをJavaで書くことにする

なんでC?

クライアントAPIの表現に適している.ヘッダーのファイルに関数を宣言するだけでおk

APIを書いてみる

void xmms_play();
void xmms_pause();
void xmms_add_to_list(char *);

これをJavaで書くと…

public class XMMS {
    public void play() { doPlay(); }
    public void pause() { doPause(); }
    public void addToPlayList(String file) { doAddToPlayList(file); }
}

って書くととりあえずCと同じインタフェースになる.

  • static宣言する
  • インスタンスメソッドのまま
    • わざわざ doPlay() とか書かずに,直接書いてしまう
  • abstractにする
  • finalにする といった選択肢はあるが,今回は特に追加の修飾子を付けず,クライアント側は事前に定義されたXMMSオブジェクトのインスタンスを不思議な力によって得ているとする

気づき

CとJavaでクライアントAPIを処理する方法は似ている

SPIを書いてみる

過去に再生した曲のリストを取得するSPI(プラグイン)がほしい -> 再生を行う関数から始めないといけない

void my_ playback_prints(char* text) {
  printf("%s\n", text);
}

という関数がほしいけど,プレイヤ自身が他の関数へのポインタを受け付ける登録関数を持っている必要がある

// char*を引数に持つ関数ポインタを引数とする登録関数
void xmms_register_playback(void (*f)(char*));

実際は次のように実行する

xmms_register_playback(my_playback_prints);

この流れではJavaだとどうなるか

// 先ほどのXMMSクラスの中に実装されているイメージ?
interface Playback {
    public void playback(byte[] data);
}

class MyPlaybackPrints implements XMMS.Playback {
    public void playback(byte[] data) {
        System.out.prinln(new String(data));
    }
}

// 実行コード
xmms.registerPlayback(new MyPlaybackPrints());

気づき

  • Cだと忌まわしきポインタが出現する
  • Javaでは private,final,staticではないと宣言されたメソッドによりコールバックを提供することで,簡単にSPIを作成できる

8.2 APIの発展は,SPIの発展とは異なる

APIの場合 -> クライアントにメソッドを提供している

  • 追加には何の問題もない.新しくメソッドを追加したところで,バイナリ互換性は崩壊することはない
  • クライアントには多くの可能性や選択肢を持たせることが可能になる.当然使用しないという選択肢もある
    • 拡張は双方に得をもたらす
  • 削除では問題あり.これまでそこを使っていたユーザが苦しむ

追加は受け入れられるが,削除は受け入れられない

SPIの場合 -> 他の人実装しなければならないインタフェースが拡張の対象になる

  • 実装しなければならないメソッドが増えるので,それを行っていない既存実装が動作しなくなる
  • 削除(呼び出しの停止)は何の問題はない.インタフェースからメソッドを取り除いても実装したメソッドへのコールが止まるだけ.

削除は受け入れられるが,追加は受け入れられない

大事なことは定義を始めるときに「どの部分がクライアントが呼び出すAPIで,どの部分が提供する機能の拡張を行うSPIか」把握すること.単一クラス内にAPIとSPIを混在させるとお互いがからみ合って発展のしようがなくなる. APIとSPIは常に分離せよ

8.3 Java 1.4とJava 1.5の間でのWriterの発展

Writerクラスってなんだっけ

  • 文字ストリームに書き込むためのabstractクラス

  • 1.4世代 -> java.lang.Appendable を実装する必要があった.

    • 特に Appendable append(CharSequence csq)

1.4 -> 1.5にするにあたって,新しいabstractメソッドを追加することはできない

  • Writerはそもそもabstractクラス.こいつを継承したクラスがすでにあるので,やってしまうとコンパイルできなくなる
    • BufferedWriterとかね

abstractを使わずに例外をスローすることはできるか?

public Writer append(CharSequence csq) throws IOException {
    throw new UnsupportedOperationException();
}

これで,とりあえず達成はできるが…

try {
    bufferedWriter.append(what);
} catch (UnsupportedOperationException ex) {
    bufferedWriter.write(what.toString());
}
  • try-catchと言った防御的コードが必要
    • 1行で済んでいたはずなのに...

例外スローは良い選択とはいえない.まだこっちみたいに append() のデフォルトを実装するほうがかなりマシ

if(csq == null) {
    write("null");
} else {
    write(csq.toString());
}
return this;

こうすると,クライアント側で変更が要らなくなるので,クライアントからしたら優れた解法になる.ちなみにJDKが使用したのはこれ.このようにするとクライアントに与えられる選択肢は

  • メソッドのデフォルト実装に頼る
  • サブクラスでオーバーライドされた実装に頼る

ただし,問題点が1つだけ

  • シーケンスから文字を処理できる高度に最適化されたライターでも,求めている最適な性能を得ることができるかどうかは別.
  • 出力ストリームの性能を上げたい -> BufferedOutputStream
  • ライターでの操作の性能を上げたい -> BufferedWriter
  • BufferedWriter で新しい append() は機能しない
  • BufferedWriter に新しいメソッドを実装しながら,新しい方へ移譲するか,互換性を選ぶ必要がある
  • 互換性を選ぶということは append() をオーバーライドしないということ.これだけでBufferedWriterを使っている人に影響をあたえることはなくなる.

CountingWriterとCDSequenceの例

  • append() がオーバーライドされているので,最適化がされたように見えるが, BufferedWriterのコンストラクタの引数にこれを置いたことで,最適化の意味をなくしている.

じゃあ今度は性能を失うことなく移譲できるようにしてみる

if(shouldBufferAsTheSequenceIsNotTooBig(csq)) {
    write(csq.toString());
} else {
    flush();
    out.append(csq)
}
return this;

こうすることで,性能を失うことなく移譲できるような仕組みを持たせることができるが, BufferedWriter のサブクラスでオーバーライドされるかも知れない.

CryptoWriterの例

このクラスは1.4時代に作成されたものとすると,全ての必要なメソッドをオーバーライドして,どの write() が実行されても正しく暗号化を行うことができるが, append() が追加された今では,それに対して何も対処ができていないので.ユーザがそれを使おうとするとテキストは全く暗号化されない.実装的に CryptoWriter は全ての書き込みを包み込むことを意図していただろうが,1.5時代の BufferedWriter ではそれができなくなってしまっている.

リフレクションを使う

膨大なシーケンスを処理すると言ったある種の目的に対して,直接実際のWriterに移譲する.とすると, BufferedWriter.writer(String) 呼ぶ必要があるので,後方互換性が失われる.これをリフレクションを使ってクラスのサブクラス化がされているか, write() がオーバーライドされているか調べることで,いつ何をすべきか知ることができる.

boolean isOverriden = false;
try {
    isOverriden = (getClass().getMethod("write"), String.class).getDeclaringClass() != Writer.class)
            || (getClass().getMethod("write"), Integer.TYPE).getDeclaringClass() != BufferedWriter.class)
            || (getClass().getMethod("write"), String.class, Integer.TYPE).getDeclaringClass() != BufferedWriter.class);
} catch(Exception ex) {
    throw new IOException(ex);
}

if(isOverriden() || shouldBufferAsTheSequenceIsNotTooBig(csq)) {
    write(csq.toString());
} else {
    flush();
    out.append(csq)
}
return this;
  • 全ての write() の振る舞いが期待されているか,つまり,メソッドがオーバーライドされているか調べる
  • オーバーライドがされていなければ,CharSequenceの処理を実際のWriterに渡して処理を移譲する
  • オーバーライドがされていれば,互換性確保のために自分自身の write() を用いる.
  • 以上が発生すると遅くなるけど,CryptoWriter等のコードが機能し続けることができる

見栄えが良くない?

一応動くけど…,リフレクションまで使ってここまでしないといけないのか?

  • 後方互換性が多くの場合には制約であること
  • 発展が時々必要であること
  • 特に最初のバージョンをリリースした時にうまく設計できていない

最初のバージョンでAPIが発展できるようになっていなかったために支払わなければいけない代償Writer はクライアントAPI,SPIが混在させているので,発展に対して異なる制約を課してしまうので,追加,削除どちらかを行うとどちらかの都合が悪くなってしまう.

この混在を防ぐ方法はあるか?

簡単. サブクラス化させない移譲させない こと

サブクラス化させない

行う必要があるのは「他人が実装するサブクラス可能なAPIからクライアントAPIを分離する」こと.

Writerを例にとって

public final class Writer {
    private final Impl impl;

    private Writer(Impl impl) {
        this.impl = impl;
    }

    public final void write(int c) throws IOException {
        char[] arr = {(char) c};
        impl.write(arr, 0, 1);
    }

    // 以下,同様の `Impl` を用いた実装

    // 以下,APIの実装を構築するためのファクトリメソッドを持つサービスプロバイダ部分

    public static Writer create(Imple impl) {
        return new Writer(impl)
    }

    public static Writer create(final java.io.Writer w) {
        return new Writer(new Impl() {
            public void write(String str, int off, int len) throws IOException{
                w.write(str, off, len);
            }

            // 以下,同様のImplクラスの中身に関する実装
        });
    }

    public static interface Impl {
        public void close() throws IOException;
        public void flush() throws IOException;
        public void write(String s, int off, int len) throws IOException;
        public void write(char[] a, int off, int len) throws IOException;
    }
}

こうすることで,互換性を保ちながらのメソッドの追加が容易になる.finalクラスへのメソッドの追加も,新しいインタフェースの導入もバイナリ互換.

CharSequenceをサポートする

public final class Writer implements Appendable {
    private final Impl impl;
    private final ImplSeq seq;

    private Writer(Impl impl, ImplSeq seq) {
        this.impl = impl;
        this.seq = seq;
    }

    public final void write(int c) throws IOException {
        if(impl != null) {
            char[] arr = {(char) c};
            impl.write(arr, 0, 1);
        } else {
            seq.write(new CharSequence(c));
        }
    }

    // 以下,同様の `Impl` , `ImplSeq` を用いた実装

    // 新しいappendに関わる実装

    public final Writer append(CharSequence csq) throws IOException {
        if(impl != null) {
            String s = csq == null ? "null" : csq.toString();
            impl.write(s, 0, s.length());
        } else {
            seq.write(csq);
        }
    }

    public final Writer append(CharSequence csq, int start, int end) throws IOException {
        return append(csq.subSequence(start, end));
    }

    public final Writer append(char c) throws IOException {
        write(c); 
        return this; 
    }

    // 以下,サービスプロバイダ部分
    // 実装者用のあらたまImpleSeqインタフェースとそれを変換する新たなファクトリメソッドが増えてる

    public static Writer create(Impl impl) {
        return new Writer(impl, null);
    }

    public static Writer create(ImpleSeq seq) {
        return new Writer(null, seq);
    }

    public static Writer create(final java.io.Writer w) {
        return new Writer(null, new ImplSeq() {
            public void write(CharSequence csq) throws IOException{
                w.append(csq);
            }

            // 以下,同様のImplSeqクラスの中身に関する実装
            // Implに比較して,メソッド数を減らすことができている
        });
    }

    public static Writer createBuffered(final Writer out) {
        return create(new SimpleBuffer(out);
    }

    public static interface Impl {
        public void close() throws IOException;
        public void flush() throws IOException;
        public void write(String str, int off, int len) throws IOException;
        public void write(char[] arr, int off, int len) throws IOException;
    }

    public static interface ImplSeq {
        public void close() throws IOException;
        public void flush() throws IOException;
        public void write(CharSequence seq) throws IOException;
    }
}

BufferedWriter のサブクラスが作成可能な場合, append() のデフォルト実装は実行時振る舞いを複雑にしていたが,これによりこの問題はほぼ解消される.

Writer.Impl を実装して, Writer のインスタンスを生成するために適切なファクトリメソッドを使用することができるし, Writer.ImplSeq を実装し,新たなファクトリメソッドを使用して,新たなメソッド append() を提供することができる.

このようにすると複雑さが増しているかもしれないが.新たなAPIが「ファクトリメソッドを使用し」,「出来る限り隠蔽し」,そしてクライアント用インタフェースとプロバイダ用インタフェースを明確に定義できるので,これらのりようは複雑ではない,更に,発展に関する問題の多くをうまく解決できるようになる.最初のバージョンを書くのは苦労するが,APIとSPIを分離することで,将来の発展を非常に簡単にし,移譲とサブクラス化が一緒にうまく機能しないことで起きる問題を防ぐことに繋がる

8.4 適切にAPIを分離する

APIには局所性が現れる

関連するものはお互いに並べて定義されていることが期待される.同じクラス,同じパッケージに定義されるべき.

  • Stringの内容を操作するメソッドは,殆どがStringクラスのメンバーとして宣言されている
  • java.io 以下に InputStreamOutputStream が基本的なストリームとして定義され,お互いに関連している.更にストリームの提供を補うための機能やデコレータを持つサブクラスや入出力のストリームを結びつけるクラスがこのパッケージ内で提供されている

APIからユーザが期待する局所性

  • 関連するメソッドを同じクラスに入れる
  • 関連しないメソッドを,他に良い場所が思いつかないからという理由だけで,属すべきでない場所にいれない
  • 関連するクラスを単一のパッケージに入れる
  • 特別な状況で役立つ追加のクラスを他の場所に移動させる

APIは,大抵は様々な人々に対する機能の集合 である. java.io はすべての人々に対するものだが,ZIPユーティリティはZIPに関心のある人々だけに向けるべきものであり,暗号化ユーティリティは暗号化に関心のある人々だけのもの.その興味に応じてZIPユーティリティが必要な java.util.zip を見るようにして,必要に応じて java.io を見る.パッケージ選択で対象の事柄から注意をそらされる必要は最低限に留める.簡単にパッケージ間を調べられるようなAPI構造にすべき.

NetBeansモジュールにおける分割

APIを4つの分類に定義することでユーザグループへの対象分けを行っている

  • コアAPI
    • これを使用しないと出来ない機能的操作をライブラリで行いたいユーザのグループが対象
  • サポートAPI
    • APIのりようを容易にするユーティリティメソッドの集合.使用する必要はないが,楽をするために提供されている.ライブラリの処理にこれらのインタフェースは必要なく.ただのヘルパーであることを伝えるために分離.
  • コアSPI
    • ライブラリへプラグインしたい別のユーザのグループに対するインタフェースの集合.プラグインサポートが無いなら不要
  • サポートSPI
    • プラグインのインタフェースの実装は困難なので,ヘルパーインタフェースを用意するのは善.何を実装する必要があるのか,何が単なるヘルパーのユーティリティサポートであるか明確に述べるのは良いことなので分離.

APIがその使用の局所性に従うことで,APIユーザの人生が改善される.API設計の際には,対象としているユーザグループについて考え,ユーザの必要性に最も適した方法でAPIを構成すること.単一APIで構成されるライブラリは殆ど無い.

大抵は,APIが対象としている複数の目的と複数のグループが存在するので,ユーザの必要性に合わせてAPIを構成すること.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment