覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

「APIデザインの極意」-優れたAPIを決定づけるもの

「APIデザインの極意」第3章 優れたAPIを決定づけるもの で気になった部分の抜粋

APIデザインの極意 Java/NetBeansアーキテクト探究ノート

APIデザインの極意 Java/NetBeansアーキテクト探究ノート


メソッドとフィールドのシグニチャ

  • システムのコンポーネント間でのコミュニケーションにリフレクションが必要なのであれば、何かが間違っているか、既存のAPIでは不十分であることを示している


APIとしてのテキストメッセージ

  • テキストメッセージを解析するしかない状況に気を付ける
    • 他の方法で情報が利用できなければ、人々はコードが生成したテキスト出力を解析してしまう


プロトコル

  • ネットワークを介して送信されるメッセージの形式を意味するプロトコルもAPIの一つである
    • このAPIの利点は、「あなたの」サーバで起きているすべてのアクセス頻度と通信を計測して分析できること
  • ネットワークプロトコルの問題点は、アプリケーションとの会話に使用される様々なクライアントやプロトコルバージョンが拡散していくこと
  • プロトコルのようなAPIが後方互換性を保ちながら発展する必要がある理由は、個々の参加者の独立したライフサイクルによって問題が拡大するからである
  • お互いに通信しあう2つのプログラムは異なるバージョンであり、少し異なったバージョンのプロトコルを必要とするのはほぼ確実である。それでもプログラムは通信する必要がある


振る舞い

  • APIが提供している抽象的概念がどれだけ優れているかに関係なく、 その背後にある実装はたいていは漏れ出しており、その結果APIの一部にもなっている
  • コンポーネントの振る舞いが変わらない場合にだけ、そのユーザは最終アプリケーションでそのコンポーネントを何も考えることなく新たなバージョンに置き換えることができる


APIの広い定義

  • APIの定義は、クラスやメソッドあるいは関数とシグニチャの単純な集まりをはるかに超えている
  • 大きなシステムコンポーネント無知な状態で組み立てるのに役立つという意味でのAPIは単純なテキストメッセージから複雑で把握するのが難しいコンポーネントの振る舞いなどと範囲が広い


APIの品質検査方法

  • APIの美しさだけが、優れたAPIの唯一の測定方法になることはありません
  • 使いやすく、広く受け入れられ、生産的なAPIを設計するという目的からそれるべきではない
  • 私たちは、個々のAPIが特定の目的を満足させているかを測定する方法を作りだす必要がある


■理解しやすさ

  • APIの作成に最も似た活動は本を書くことです
    • 一人の著者と多数の読者がいる。読者は著者について何かしら知っているが、著者は読者についてはほとんど知らない
    • 読者のスキルと知識を正しく推測することは、理解しやすいAPIにするために細心の注意が必要な芸術的な部分となる
  • ほとんどのAPIのユーザは、 自分が行いたいことと似たようなことを行っている既存アプリケーションを探し、そのアプリケーションをコピーして必要に応じて修正する という方法をとる
  • APIの使用方法の多くの例を示すことで開発者が必要とすることに近い例を見つける可能性が高まる
    • 開発者がAPIを理解する可能性を大幅に高める
    • APIが使用している概念が新しければ新しいほど豊富な例が役立つ


■一貫性

  • APIのユーザがある概念を理解するのに時間を費やさなければならないのであれば、その概念をAPI全体に対し一貫して適用できることが重要


■発見できること

  • どんなに美しいAPIであっても、 想定している利用者がそのAPIを発見できなかったり、使用方法を容易に理解できなければ役に立たない
  • APIのユーザはクラスの集まりには興味はない
    • ユーザは自分の仕事を終わらせることに興味があり、そのためにはAPI利用例を見て行いたいことに近い方法を選択できるようにすることが重要である
  • 提供するAPIの種類に関係なく開始地点としての役割を果たし、人々の問題を解決するために正しい方向に向かわせる単一の場所を提供することは重要
    • 人々はクラスという観点から考えたりはしないので、その開始地点を実際の目的や仕事あるいは少なくとも期待される目標や仕事に基づくように最適な方法で構成することが重要である


■単純な仕事は簡単でなければならない

  • APIの基本的な誤りは、1つのAPIに異なるグループが興味を持つ複数の項目を入れてしまうことである
    • APIの1つの側面にしか興味が無い人々には、全く異なるグループに向けて設計された部分によって注意がそらされてしまい、興味のある部分を発見できなくなってしまう
    • 取るべき手段は、 APIを2つ以上の別の部分に分けてしまうこと
      • 呼び出し側向けとAPIに機能を追加するプロバイダ向けでパッケージ(名前空間)を分けてしまう等
  • たとえばJavaMail APIは膨大な数の概念とクラスが混在しているので、メールの送受信をするだけでもかなりの分量のコードを書く必要がある。
    • このAPIは、プロバイダ向けに最適化されてしまっており、この誤った最適化によりJavaのメールアプリケーションを比較的少なくしてしまっている原因になっている


■投資の保全

  • APIのユーザがそのAPIで本当に満足するためには、新しいバージョンのライブラリのリリース時に、それまで作成したものが動かなくなることが無いことを信用できなければならない
  • ライブラリに対してコーディングすることは時間・学習・労力・お金の投資である
    • API設計者のもっとも重要な責任は、そのライブラリを使用する人々の投資が無駄にならないようにすること
  • ユーザがAPIで良い経験を積めば積むほどAPIの良さを周りに話し、そのAPIを使用することに良い印象を持ち、そのAPIの利用は促進されて広く知られるようになる
    • 結果としてAPIに好意を持って利用している人たちのコミュニティが形成される
    • そのため、参加者の投資を無駄にしないことが重要であり、API契約を互換性のある方法、あるいは、少なくとも予測可能な方法で発展させることに努めることが常に重要



関連エントリ

「APIデザインの極意」-APIを作成する動機

「APIデザインの極意」第2章 APIを作成する動機 で気になった部分の抜粋

APIデザインの極意 Java/NetBeansアーキテクト探究ノート

APIデザインの極意 Java/NetBeansアーキテクト探究ノート


分散開発

  • アプリ全体を組み立てる際には個々のビルディングブロックを糊付けする必要があり、それらのブロックはお互いに会話をする必要がある。
  • たいていは、明確に定義されたAPIを通して話しをする。
  • 各コンポーネントのAPIは、みんなの無知( ※選択的無知)を増加させるための最初の一歩である。
  • APIは機能を提供しているコンポーネントの詳細を理解する必要性を最小限にしてくれる。
  • APIは各コンポーネントの機能と内部実装に関する抽象概念とみなすことができる
  • 無知な利用が成功するためには、 APIがライブラリの本質的な真意をきちんと反映していることが重要

(※選択的無知 ・・・ 何を深く知り、何を深く知る必要がないかを積極的に選択すること)


アプリケーションのモジュール化

  • コンポーネントには名前が必要
  • 名前は一意でシステム内でコンポーネントを特定できて、そして、 説明的でなければならない
  • コンポーネントが名前を持つ必要があると期待することは自然な流れに思える。しかし、この点を注意深く見てみると、 これらの名前はコンピュータよりも人にとって重要であると分かる。
  • コンポーネントがたいていは人が理解できる名前を持っているという事実は、名前が主に対象としているのは人間という証です。
  • モジュール方式のシステム内の個々のコンポーネントは必要とするほかのすべてのコンポーネントに関する情報を保持ししている。
  • モジュール方式のシステムではほかのコンポーネントへの依存は、 依存先コンポーネントの識別名と必要な最低バージョンを示すことで表現することができる。


すべては、コミュニケーション次第

  • アプリケーションは無知の概念に基づいており、アプリケーションの最終組み立てを行う人を限界まで無知に近づけようとする。
  • 結果として理想的なアプリケーションは、モジュール方式のアーキテクチャに基づいている。
  • APIがうまく設計されていなければ、使用する側はそのAPIの作者の意図を正しく理解できずに誤用する。
  • APIが信頼できるものであるためには、 誤った利用を防ぐ必要があり 、また、 発展する準備ができていなけれなばらない。
  • 最初に作成された時に完璧であるAPIはほとんどありません。
  • APIの将来的な改善をおこなうための重要な条件は 分離です。


経験的プログラミング

  • APIは自己文書化されている必要がある。
  • 言い換えると ドキュメント無しで利用可能である 必要があります
  • APIは解決すべき課題へとAPIのユーザを自然に導かなければならない



関連エントリ

Go言語 - enumを定義する

Go言語には、C言語やJavaにおける「enum(列挙型)」のような機能がありません。
Go言語をやり始めて最初にこれを知った時、 え嘘でしょ? と言いたくなりました。

が、実際には 定数定義とiotaを利用することでC言語のおける列挙型に近い振る舞いを実現することが可能です。
例えば、Red, Blue, Yellowを持つColorというenumを作る場合は以下のように定義します。

type Color int

const (
    Red Color = iota
    Blue
    Yellow
)


で、iotaって何ですかって話なんですが。
識別子iotaは、定数宣言文(const)内で使用される、型なしの連続する整数定数を表します。
本定数は予約語constが現れた時に0に初期化されて、各定数定義の後に1ずつインクリメントされます。
ちなみに読み方は「イオタ」。

例えば、iotaを使って定数(val0, val1, val2)を定義すると、各定数の値は0, 1, 2になります。

const (
    val0 = iota // val0 == 0
    val1 = iota // val1 == 1
    val2 = iota // val2 == 2
)

iotaは 0 から開始され、(以下例のように)iotaを省略しても評価されて連続する整数がセットされます

const (
    val0 = iota // val0 == 0
    val1        // val1 == 1
    val2        // val2 == 2
)


ということで、冒頭のColorは、実際には以下コード内のコメントのように、Redに0, Blueに1, Yellowに2がセットされています。
これにより疑似的にenumを表現しています。

type Color int

const (
    Red Color = iota // Red    == 0
    Blue             // Blue   == 1
    Yellow           // Yellow == 2
)


実際にはenumとして作成したtypeに対してfmt.Stringerインターフェースを実装(Stringメソッドを用意)しておくのが良いです。
Stringerインターフェースを実装しておいた方がfmt.Println等で表示した際に分かりやすくなります)

そこまでやるとColorの定義は以下のようになります。

type Color int

const (
    Red Color = iota
    Blue
    Yellow
)

func (c Color) String() string {
    switch c {
    case Red:
        return "Red"
    case Blue:
        return "Blue"
    case Yellow:
        return "Yellow"
    default:
        return "Unknown"
    }
}

Stringメソッドのこの実装はどうにかならないんだろうかと良く思いますが・・・)


というわけでGo言語でもenumを定義することができました。

Go言語 - 空インターフェースと型アサーション

Go言語には、全ての型と互換性を持っているinterface{}型(空インターフェース)というものが存在しています。

たとえば以下のように、interface{}で宣言した変数にはどんな型の値でも代入可能です

var obj interface{}

obj = 123                                                              // int
obj = "str"                                                            // string
obj = []string{"linux", "windows", "android"}                          // slice
obj = make(chan string)                                                // channel
obj = func(val int) string { return fmt.Sprintf("number is %d", val) } // function


また、引数の型をinterface{}にすると、どんな型の値でも受け取ることができる関数を記述できます

func anyExec(any interface{}) {
    ・・・
}

func main() {
    anyExec(12)
    anyExec("hello")
    anyExec([]string{"cat", "dog"})
    anyExec([2]string{"hello", "world"})
}


型アサーション

どんな型の値でも受け取れるinterface{}ですが、interface{}型の引数で受け渡された値は、元の型の情報が欠落しています。
(元の型の値を操作するための関数等を実行できません)

Go言語ではこのような局面で利用するための型アサーションを提供しており、型アサーションにより実体の型が何であるかを動的にチェックすることができます。(以下構文を使用します)

<変数>.(<型>)


基本的に以下のように記述します。

value, ok := <変数>.(<型>)

1番目の変数には型アサーション成功時に実際の値が格納されます。2番目の変数には型アサーションの成功の有無(true/false)が格納されます。


以下はサンプルの関数です。引数に型アサーションを用いて型に応じた処理の分岐をさせています

func printIf(src interface{}) {
    if value, ok := src.(int); ok {
        fmt.Printf("parameter is integer. [value: %d]\n", value)
        return
    }

    if value, ok := src.(string); ok {
        value = strings.ToUpper(value) // 対象がstring型なのでstringを引数に取る関数が実行できる
        fmt.Printf("parameter is string. [value: %s]\n", value)
        return
    }

    if value, ok := src.([]string); ok {
        value = append(value, "unknown") // 対象がsliceなのでAppendができる
        fmt.Printf("parameter is slice string. [value: %s]\n", value)
        return
    }

    fmt.Printf("parameter is unknown type. [valueType: %T]\n", src)
}


■呼び出し側

func main() {
    printIf(12)
    printIf("hello")
    printIf([]string{"cat", "dog"})
    printIf([2]string{"hello", "world"})
}

■実行結果

parameter is integer. [value: 12]
parameter is string. [value: HELLO]
parameter is slice string. [value: [cat dog unknown]]
parameter is unknown type. [valueType: [2]string]


型switch

型アサーションと分岐を組み合わせた処理を手軽に記述するための型switchが提供されています。
型switchを利用すると前述のprintIf関数は以下のように記述できます

func printIf(src interface{}) {
    switch value := src.(type) {
    case int:
        fmt.Printf("parameter is integer. [value: %d]\n", value)
    case string:
        value = strings.ToUpper(value) // 対象がstring型なのでstringを引数に取る関数が実行できる
        fmt.Printf("parameter is string. [value: %s]\n", value)
    case []string:
        value = append(value, "<不明>") // 対象がsliceなのでAppendができる
        fmt.Printf("parameter is slice string. [value: %s]\n", value)
    default:
        fmt.Printf("parameter is unknown type. [valueType: %T]\n", src)
    }
}


■呼び出し側

func main() {
    printIf(120)
    printIf("bye")
    printIf([]string{"apple", "orange", "banana"})
    printIf([2]string{"good", "bye"})
}

■実行結果

parameter is integer. [value: 120]
parameter is string. [value: BYE]
parameter is slice string. [value: [apple orange banana <不明>]]
parameter is unknown type. [valueType: [2]string]



関連エントリ

Go言語 - interfaceを触ってみる

Go言語にはメソッドの型だけを定義したinterfaceというものが存在しています。
interfaceは型の一種であり、任意の型が"どのようなメソッドを実装するべきか"を規定するためのものです。

大雑把にいえばJavaでいうところのinterfaceに似たものです。
interfaceを利用することで、オブジェクト指向言語でいうところのポリモーフィズムと同様の機能を実現できます。


インターフェースの定義は以下のように記述します(メソッドは複数記述することができます)

type <型名> interface {
    <メソッド名>(<引数の型>, ...) (<戻り値の型>, ...)
    ・
    ・
}


■具体的なインターフェスの定義

本エントリ内の各サンプルプログラムで利用するためのインターフェスとして、
describeというものを以下のように定義しました。説明文をstring型で返すdescriptionメソッドを1つだけ持ちます

type describe interface {
    description() string
}


■インターフェスを引数に取る関数

describeインターフェースを利用するprintDescription関数を定義して以降のサンプルで利用します。
printDescription関数は、descriptionメソッドを実行して説明文を取得し、その説明文を標準出力に書きだすだけのものです)

func printDescription(d describe) {
    fmt.Printf("Description: %s\n", d.description())
}


以下の各サンプルで具体的にdescribeインターフェースを実装してみます。


既存の組み込み型にインターフェースを実装させる

正確には既存の組み込み型には追加で新しいインターフェースを実装させることはできませんが、
typeを使って既存の組み込み型の別名の型を定義することで、その新しい型にインターフェースを実装させることができます。
本サンプルではint型の別名の方に実装させます


■インターフェースを実装

type MyInt int

// 組み込み型の別名の型をレシーバにして対象インターフェースのメソッドを定義
func (i MyInt) description() string {
    return fmt.Sprintf("MyInt is actually int. value is %d", i)
}


■実行サンプル

func main() {
    val1 := MyInt(100)
    printDescription(val1)
}

■実行結果

Description: MyInt is actually int. value is 100


構造体にインターフェースを実装させる

構造体もtypeで別名を付けて定義することでインターフェースを実装させることができます


■インターフェースを実装

type Square struct {
    edgeLength int
}

// 構造体をレシーバにして対象インターフェースのメソッドを定義
func (sq *Square) description() string {
    return fmt.Sprintf("The lengths of all four sides are equal. [edgeLength: %d]", sq.edgeLength)
}


■実行サンプル

func main() {
    sq := &Square{edgeLength: 10}
    printDescription(sq)
}

■実行結果

Description: The lengths of all four sides are equal. [edgeLength: 10]


関数にインターフェースを実装させる

関数もtypeで別名を付けて定義することでインターフェースを実装させることができます


■インターフェースを実装

type Product struct {
    id    uint
    name  string
    price uint
    PR    PRStatement
}

type PRStatement func() string

// PRStatementという関数をレシーバにして対象インターフェースのメソッドを定義
func (pr PRStatement) description() string {
    return pr()
}


■実行サンプル

func main() {
    p1 := &Product{id: 1, name: "Golang PC", price: 10000}
    p1.PR = func() string {
        return fmt.Sprintf("この %s は、値段が%d円なのでとてもお買い得です", p1.name, p1.price)
    }
    printDescription(p1.PR)

    p2 := &Product{id: 2, name: "リンゴ", price: 100}
    p2.PR = func() string {
        return fmt.Sprintf("この %s は、とても美味しいです", p2.name)
    }
    printDescription(p2.PR)
}

■実行結果

Description: この Golang PC は、値段が10000円なのでとてもお買い得です
Description: この リンゴ は、とても美味しいです

describeインターフェースの実装内容によって出力結果が変わっています。


まとめ

というわけでinterfaceを利用することで、Goのプログラムに型の柔軟性を与えることができることが分かりました



関連エントリ