Hyrum's Law (The Law of Implicit Interfaces) について


ハイラムの法則 とは

Software Engineering at Googleでも紹介されたソフトウェア工学についての考察で、以下のように要約されます。

With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.

意訳すれば以下でしょうか。

APIに多くの利用者がいれば、API仕様なんてものは無いに等しい

観測できる全ての振る舞いは、どこかの誰かによって依存されることになるのだ

www.hyrumslaw.com


ハイラムの法則 の意味するところ

Googleのソフトウェアエンジニア である Hyrum Wright は、その仕事の中で、ごく単純なライブラリの変更が、遠く離れたシステムの障害に繋がるという経験をしました。 このような「暗黙的な依存関係」の存在が、教訓として「ハイラムの法則」と名付けられました。


APIは、利用者と内部実装を分離するインターフェースとなり、利用者に抽象を提供します。抽象により利用者は詳細に立ち入ることなく、問題の解決を簡素化することができます。この抽象化は、ソフトウェアの複雑さを管理するために必要不可欠な要素となります。

API は「期待される動作」(仕様) を規定し、その実装によって期待される動作が実現されます。 API が公開されて利用が拡大すると、APIのユーザー(コンシューマ)は、そのインターフェイスを通じて意図的に公開された実装の詳細や、APIを利用する上で知り得た実装の詳細に依存し始めます。

APIの設計/実装者は、ユーザに対して、API仕様に対してのみ依存することを望みます。 しかし実際には、仕様では規定しない内部実装の詳細が、暗黙的なインターフェースとしてコンシューマによって依存されてしまいます。

インターフェイスに十分な数のコンシューマがいれば、意図的であろうとなかろうと、コンシューマは集合的に実装のあらゆる側面に依存することになり、暗黙のインターフェースは最終的に実装と完全に一致していきます。この時点でインターフェースは消滅し、実装がインターフェースとなり、それに対するいかなる変更もコンシューマの期待に反することになります。


ハイラムの法則 による教訓

  • APIの開発者は、利用者に対して、実装の詳細を可能な限り観測できないようにすべきである
  • APIの内部実装を変更する際、利用者はAPI仕様以上の情報を使ってシステムを構築していると考えなければならない
  • APIの利用者は、API仕様にのみ依存すべきである


ハイラムの法則のよくある例に、ハッシュテーブルがあります。 ハッシュテーブルに格納されたエントリをイテレートする場合、その順序性は一般的に保証されません。しかしAPI仕様上は順序性は保証されないものの、特定のプラットフォームの実装上で順序が決定(ハッシュ値順であったり格納順であったり)できる場合があり、このような特定の実装に依存してシステムが構築されてしまう場合があります(直接ハッシュテーブルを扱うのではなく、いくつものAPIを経由して取得した結果が順序付けされていれば、それは順序性が担保されていると捉えてしまうのは自然なことでしょう)。

Go の map では、このような実装依存を防ぐために、イテレート順が常にランダムとなるように実装されているのです。


具体例

Java Stream のよくある例を見てみましょう。

List<String> names = people.stream()
    .map(Person::getName)
    .collect(Collectors.toList());

ここで Collectors.toList() の API仕様は以下のようになっています。

入力要素を新しいListに蓄積するCollectorを返します。 返されるListの型、可変性、シリアライズ性、またはスレッド安全性は一切保証されません。返されるListをより細かく制御する必要がある場合は、toCollection(Supplier)を使用してください。

API仕様では先のように規定されていますが、実際の実装では、以下のように ArrayList のインスタンスが使われています。

public static <T> Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
        (left, right) -> { left.addAll(right); return left; },
        CH_ID);
}

API仕様上では、可変性については規定されていませんが、実装上は add で要素を追加することも remove で要素を削除することもできます。そしてこのような要素の操作を行う利用者側のコードは世界中に溢れているでしょう。

「可変性は一切保証されない」ため、Collections.unmodifiableList() でイミュータブルなリストを返すように変更することはAPI仕様上では合法です。しかし暗黙的な依存関係として広く漏れだしてしまっている今や、そのような変更は行うことはできないでしょう。