覚えたら書く

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

複雑なプログラムに現れる傾向や特徴

「ソースコードを汚くするには」に記載したようなコードを書いていると、結果的に複雑なプログラムになってしまいます。
ここでは複雑なプログラムに現れやすい傾向や特徴などについて記載しました

基本的にJavaをターゲットにして本エントリを記載しています


クラス定義

  • import文に現れるパッケージが多種多様になっている
    • 多様なクラスとの依存関係ができて複雑な状態になっている
  • import文が異様に長い(importしているクラスが多い)
    • import文が長いということは依存しているクラスが多いということ。結果的に複雑になっている
  • クラスが大きい
    • 責務が多すぎるためにクラスが巨大化している
  • クラス名が長い
    • 複数の責務を負わされたクラスはその名称が長くなる場合がある
  • 定義されたpublicメソッドが多い
  • インスタンス変数が多い
  • クラス名とクラス内に定義されたメソッドが意味的に紐づかない
    • もともとの責務の範囲を超えたメソッドが定義されてしまった可能性がある
  • abstractクラスなのにでかい(重い実装を持っている)
  • メソッド名が長い
    • メソッド名に端的な名前を付けられないのは、他のメソッドとの区別をするために説明的な内容を記述しなければならないという状況を意味する場合がある。クラスの整理ができていないことに起因する。
  • コンストラクタが多い
    • 色んなタイプのインスタンスを生成するために多様なコンストラクタを定義しなければならなくなっている
  • staticメソッドだけを持つクラスが多い
    • staticメソッドだけを持つstaticなクラスに可変性を入れ込むのはかなり厳しい。そのため機能追加時や変更時などに柔軟な対応がしにくくなる


パッケージ

  • パッケージ間の相互依存が発生している
    • パッケージごとの役割の整理がうまくできていない場合にこのようなことが起きる
    • 一方向の依存に比べて、相互依存は異常に複雑な状態を生み出す


nullの扱い

  • メソッドの戻り値などに対する頻繁なnullチェックが行われている


文字列の扱い

  • 各処理の中で文字列をtrimしている
    • 頻繁なnullチェックなどと同じく渡された値が自分の欲している形になっていない可能性があるので、余計な処理をしなければならなくなっている
    • 基本的にどこか一カ所でやるべき


例外の扱い

  • 自分のLayerと無関係な例外をスローしている
  • 下位メソッドからの例外をjava.lang.Exceptionでcatchしている
    • 下位メソッドが複数の例外をスローしてきたり、実装に寄った例外をスローしてきたりした場合などに、例外の扱いが非常に煩わしくなり、java.lang.Exceptionで例外をまとめてcatchしてしまう状況をつくってしまう。


継承

  • 継承元の親クラスをどんどん遡っていかないとソースコードの内容を理解できない
    • 継承階層が深くなりすぎている。継承は使いどころを間違えると諸刃の剣となる


命名

  • メソッドが機能追加によってそのコード量が増加した場合でもメソッド名が変わっていない
    • 例えば機能追加によりコード量が当初よりも2倍,3倍に増えた場合に、普通に考えれば同じ名前(意味)で保ち続けられるはずはない。適切にメソッド名を変えるか、メソッドを上手く分割する等の対応が必要なはず。


if文

  • ifの条件文が何を意味しているのか一見して理解できない
    • "何の判断をしているのか?"ではなく"どうやって判断するのか?"という条件判定の実現方式になっているとソースコードを読む側は理解できない


ユニットテスト

  • ユニットテスト時にテスト対象クラスのインスタンス化ができない
  • ユニットテスト時に、実行対象のメソッドに渡すパラメータがインスタンス化できない
  • ユニットテストしようとした際に、テスト対象のクラスと直接関係のない外部環境(DBやファイルやネットワーク環境など)のセットアップが必要になる



関連エントリ

「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]



関連エントリ