Open12

[memo]達人プログラマー(第2版)

ぽんみーぽんみー

学びの多い書籍なのでスクラップ作成。
考察も多少メモしていきたい。

ぽんみーぽんみー

8. よい設計の本質

ETC原則:Easier To Change

すぐれた設計は、悪い設計と比較して変更が容易に行える。
よくあるコード設計原則は、ETC原則を特殊化したもの。

  • なぜ結合度を下げるか
      懸念を隔離し、変更しやすくする。つまりETC原則l。
  • なぜ責務を単一化すべきか
      要求の変更とモジュールが対応づけられる。つまりはETC原則。
  • 名前の付け方がなぜ重要か
      優れた名前はコードの可読性を向上させ、変更時はその名を手がかりにする。つまりETC原則。
ぽんみーぽんみー

28. 分離

分離されたコードは変更しやすい。

TDA : Tell, Don't Ask

「照会せずに依頼する」。
オブジェクトから値を取得し、その値を操作し、その結果をオブジェクトに戻してはいけない。
該当オブジェクトに、すべての仕事を任せること。

Bad Code

Code

public void applyDiscount(customer,order_id, discount) {
  totals = customer
           .orders
           .find(order_id)
           .getTotals()
  totals.grandTotal = totals.grandTotal - discount;
  totals.discount = discount;
}

何がBadか

  • 以下を知っていないと操作できないこと。
    • 顧客オブジェクトが、ordersにより注文履歴オブジェクトを公開している
    • 注文履歴オブジェクトが、注文IDを受け取り特定の注文オブジェクトを返すfindメソッドを有している
    • 注文オブジェクトが、総額を管理するオブジェクト(totals)を返すgetTotalsメソッドを有している
    • totalsが、getter/setterとともに割引額のsetterを有している

課題

  • 40%を越える割引は受け付けない処理を加える事になった場合は?
    applyDiscount関数に実装する?しかし、どこかでtotalsオブジェクトのフィールドに変更を加えてるかも...
  • 「責務」という観点で見ると、totals=「総額を管理する責務がある」はずだが、現状そうなっていない

Good Code

Code

まずは、総額を管理するオブジェクトに割引額の処理を委譲する。

public void applyDiscount(customer, order_id, discount) {
  customer
  .orders
  .find(order_id)
  .getTotals()
  .applyDiscount(discount);
}


次に、「顧客オブジェクトから必要な注文を直接取り出す」ように変更する。
(注文履歴オブジェクトを取得して、検索すべきではないとの事)

public void applyDiscount(customer, order_id, discount) {
  customer
  .findOrder(order_id)
  .getTotals()
  .applyDiscount(discount);
}


注文オブジェクトとその総額にも同じことを適用。

public void applyDiscount(customer, order_id, discount) {
  customer
  .findOrder(order_id)
  .applyDiscount(discount);
}

この辺りでSTOP。
TDAは絶対的な法則ではなく、問題を認識しやすくするための単なるパターン。
メソッド呼び出しを連鎖させるとこうなりがちなので意識していく。

memo

  • BadCodeでは、カプセル化のメリットが失われている事が問題だと理解した
  • 世の中にある、例えばPythonのORM等はBadCodeのようにメソッド連鎖してるのでは...? 🤔
  • 以下の項目も関連して興味あり
    • 連鎖とパイプライン
    • サブクラス化が危険な理由
ぽんみーぽんみー

31. インヘリタンス(相続)税

継承は結合度を大きく高めてしまう。
以下いずれかを使用する事で、今後いっさい継承を使わずに済むと紹介されていた。

  1. interfaceとプロトコル
    継承を使わずにポリモーフィズムを実現できる
  2. 委譲(コンポジション)
    参照先のオブジェクトが「全く見えなくなる」 = ”カプセル化”にもつながる。
    ただし、ステップ数は増加する。
  3. mixinとtrait
    • mixin
      幅広いクラスで共通して使われる機能だけを、継承を使って組み込む。 ←(追記)言語によりけり。C++なら実質継承だが、Golangならコンポジション扱い。制約が異なる。
      例)
      mixin CommonFinders {
        def find(id) { ... }
        def findAll() { ... }
      end
      
      class AccountRecord extends BasicRecord with CommonFinders
      class OrderRecord extends BasicRecord with CommonFinders
      
      また、バリデーションを適用させるパターンもある。
      • ユーザが入力したパスワードと、ハッシュ化したパスワードの検証
      • ユーザの情報を更新んするために入力したデータの検証
      • 永続化する前に実施するデータの整合性検証

memo

余談だが、書籍C++ Coding Standardsには、以下のように紹介されていた。

58. 継承よりも合成を使おう

C++で表現できる文法の中で、継承はfriendに次いで強力である。「その継承は、本当に自分の設計に有益なのか?」を考え、それが確実でない場合は、継承よりも合成を採用しよう。
熟練の開発者間で使われ過ぎの傾向にある。ソフトウェア設計の基本原則は、「結合を最小限に抑えること」だ。
合成>継承である点を挙げる↓

  • 呼び出し側コードに影響を与えず、柔軟性に富む
    深く考慮されずpublic継承から始まってしまっている場合、依存度が高まる。こうなると、親クラスに対する変更が容易ではなくなる。
  • コンパイル時の独立性が高く、コンパイル時間が短い
  • 予期しない振る舞いを抑制する
  • 複雑性と脆弱性がより小さい
    ...

また、例外として、以下は利用しても良いとのこと。

  • 代入互換性をモデル化する場合(public継承を活用すべき)
  • 仮想関数を利用する場合
  • protectedメンバーへのアクセスが必要である場合
  • メンバ変数の前に生成し、メンバ変数の後に破壊する必要がある場合

例外事項を見る限りC++寄りではあるが、
「達人プログラマー」の方が、継承に対して厳しく書かれていた印象。

その他

ぽんみーぽんみー

32. 設定

アプリケーションが本番稼働に入った後も、ある程度挙動を変えられるようにするため
アプリの外側で挙動を指示できるように値を保持しておくこと。

例)

  • 外部サービスの認証情報(DBやサードパーティのAPIなど)
  • ログの出力先
  • アプリが使用するポート番号、IPアドレス、マシン・クラスター名など
  • 税率などの外部で保持すべきパラメータ情報
  • ライセンスキー
    など。

保持する方法

  1. 静的ファイルで保持
    yamlやjsonが最も一般的。
    グローバル定数として参照できるため。 ※本書では非推奨。
  2. CaaS(Configuration as a Service)
    「サービスAPIの背後に保存する」。
    アプリケーションを停止し再起動をかけるような事なく、設定データを動的に変えられる。
    本書で推奨している。

memo

推奨するやり方(2)について。
CaaSという単語は知らなかったが、具体的には以下を指すと思われる。

  • AWS上で設定値を保持
    AWS Parameter Storeを使用。名前空間を使用して、パラメータを階層で保持できる。
    例) /prod/myapp/db-url, /prod/myapp/api-key
    さらにAWS KMS(Key Management Service)を使って暗号化し、セキュリティを高める。
  • API上で取得
    boto3経由で取得:
    import boto3
    
    ssm = boto3.clinet('ssm')
    
    def get parameter(name):
        response = ssm.get_parameter(Name=name, WithDecryption=True)
        return response['Parameter']['Value']
    
    db_url = get_parameter('/prod/myapp/db-url')
    api_key = get_parameter('/prod/myapp/api-key')
    
  • 設定値の更新時
    AWS Management ConsoleまたはCLI、AWS SDKを使用。
  • キャッシュ
    Parameter Storeへ過剰にAPIコールさせないように、キャッシュメカニズムを構築。
    起動時に設定値を取得し、メモリに保存しておく。
  • リフレッシュ
    パラメータ値の更新を動的にチェックし、それに応じてキャッシュを更新する。
ぽんみーぽんみー

11. 可逆性

要求やユーザの心情、ハードウェアは、ソフトウェアの開発速度よりも早く変化していく。
”コードの”柔軟性だけでなく、アーキテクチャやデプロイ、ベンダー統合などという切り口からも柔軟性を維持していくべきだ。
細かくコンポーネント分割し、変更に強いソフトウェアを。

ぽんみーぽんみー

26. リソースのバランス方法

メモリやトランザクション、スレッド、ネットワーク接続、ファイル、タイマーなどのリソースは、割り当てたら必ず解放しよう。「始めた事は終わらせる」。

アンチパターン

ファイルを開き、顧客情報を読み取り、フィールドを更新して結果をファイルに書き戻すRubyの例。
このコードは一見ロジックが分離しており綺麗に見えるが問題が潜んでいる。何だろうか。

def read_customer
    @customer_file = File.open(@name + ".rec", "r+")
    @balance = BigDecimal(@customer_file.gets)
end

def write_customer
    @customer_file.rewind
    @customer_file.puts @balance.to_s
    @customer_file.close
end

def update_customer(transaction_amount)
    read_customer
    @balance = @balance.add(transaction_amount, 2)
    write_customer
end

それは、read/writeがcustomer_fileというインスタンス変数を共有する事で密結合になってしまっている。

保守プログラマー目線で考えてみる

仕様変更が発生し、「残高の更新は、更新後の値が負でない場合にのみ行う」と告げられた保守プログラマーを想定してみる。
以下のように修正したとしよう。

def update_customer(transaction_amount)
    read_customer
    if (transaction_amount >= 0.00)
        @balance = @balance.add(transaction_amount, 2)
        write_customer
    end
end

これをデプロイするとバグが発生する。
「オープンしているファイルの数が多すぎる」というエラーメッセージと共にシステムが停止する。
close処理をwrite_customerでしか行っていないので、ファイルがクローズされない。

更にNGな解決案

上記に対する、更にマズい解決策を考えるとこうなる。

def update_customer(transaction_amount)
    read_customer
    if (transaction_amount >= 0.00)
        @balance = @balance.add(transaction_amount, 2)
        write_customer
    else
        @customer_file.close    # Bad idea!
    end
end

これにより問題は修正される。
ただ、結果としてread/write/updateそれぞれ3処理がcustomer_fileを通じて結合してしまい、ファイルのOpen/Close管理が更に面倒になっていきます。
仕様が増えれば増えるほど、コードは汚くなっていきます。

Goodな解決策(その1)

以下のようにリファクタリングする。

def read_customer(file)
    @balance = BigDecimal(file.gets)
end

def write_customer(file)
    file.rewind
    file.puts @balance.to_s
end

def update_customer(transaction_amount)
    file = File.open(@name + ".rec", "r+")        ⭐️
    read_customer(file)
    @balance = @balance.add(transaction_amount, 2)
    write_customer(file)
    file.close       ⭐️
end

ファイル参照を内部で保持するのではなく、パラメータとして引き渡すように変更した事で、ロジック間の結合度を大きく下げている。

Goodな解決策(その2)

別解。

def update_customer(transaction_amount)
    File.open(@name + ".rec", "r+") do |file|
        read_customer(file)
        @balance = @balance.add(transaction_amount, 2)
        write_customer(file)
    end
end

先ほどと似ているが、ファイルのスコープに達した時点でクローズされる。
pythonやエクセルのVBAでも見かけるwith open構文だ。
また、こちらもパラメータとして引き渡すように変更した事で、ロジック間の結合度を大きく下げている。

リソースの割り当て・解放順序

  • リソースは、割り当てた順序と逆の順序で解放するべき
    でないと、一方のリソースがもう一方へのリソースへの参照を保持している場合に「みなし児リソース」が発生してしまう。
  • 異なるコードで同じリソースの組を割り当てる場合、常に同じ順序にすること
    以下のような、デッドロックの可能性を削減できるため。
    • プロセスAがリソース1を割り当て、次にリソース2を要求
    • プロセスBがリソース2を割り当て、次にリソース1を要求
      ⭐️デッドロック発生

余談

メモリに格納する場合の話[1]
スタック領域に入れるならLIFO(後入れ先出し)だが、ヒープ領域なら任意の順序となる。

  • スタックに割り当てられるオブジェクトはLIFOで適用される
    インスタンスなど。継承階層の逆順。

例外処理におけるリソース解放

例外をサポートする言語では、リソースの解放が難しくなる傾向にあるとの事。
これは言語の機能的限界に依存しており、たいていは以下の選択肢になる。

  1. 変数のスコープを使用する
     RustやC++におけるスタック変数など。
  2. try-catchブロックの、finallyを使用する

1で例に挙げた言語機能では、変数のメモリーは、returnやブロックからの脱出、例外によるスコープ外に抜け出す際に実行される。また、デストラクタ呼び出しを契機にする事でも可能。

注意すべきは2である。

try-catch-finallyでのアンチパターン例

以下は落とし穴が潜んでいるコードだが、どこが問題だろうか?

begin
    thing = allocate_resource()
    process(thing)
finally
    deallocate(thing)
end

「リソースの割り当て自体に失敗」する事で例外が発生した場合、割り当てられてもいないthingを解放しようとしてしまう。

改善策

thing = allocate_resource()
begin
    process(thing)
finally
    deallocate(thing)
end

(と書いてたけど、これってリソース割り当て失敗時に例外キャッチできないという事......??)

リソース割り当てのコツ

「メモリ割り当てに対するセマンティック不変性[2]を確立すること」と紹介されていた。
まずは集約データ構造に対して、”誰が責任を持つのか”という点を決定していく。
そしてトップレベルの構造を解放時、どういった事が起こるかを考える。

  • トップレベルの構造は、自らが保持している部分構造の解放についての責任をもつ
    なので、トップレベルの構造は順次、再帰的にに保持している部分構造のデータを解放していく。
  • トップレベルの構造のみを解放する
    トップレベル構造が保持しているデータ(なおかつ、どこからも参照されていない)は「みなし児」としt扱われる。
  • トップレベルの構造は、部分構造が保持されている限り、自らを解放しないこと

主要な構造単位で、割り当てと解放を行う機能モジュールを記述するのも良いと思われる。
(さらにそれらに対してシリアライズ・デシリアライズ・デバッグ出力やトラバース用のフックを設けても良い)

さいごに(⭐️地味に重要)

本書では自分自身を含めて誰も信用しないという教えを実践している。
今回で言うと、「リソースが適切に解放されているかどうか」をチェックするコードを作り込んでおくのも良いと言える。
多くのアプリケーションでは、リソース型ごとにラッパーを作成しておき、そのラッパーに割り当て・解放を追跡させれば同じ事が実現できる。
そして特定のコードの時点で、リソースが適切な状態にあるかをラッパー経由でチェックする。

例えば、サービスのリクエストを待ち続けるようなプログラムであれば以下。

  • 次のリクエストの到着を待つメイン処理ループの先頭`

ここで、前回のループ実行時と比較したリソース使用量を確認すべきであるとの事。

また、低水準ではあるものの、稼働中のプログラム内のメモリリークの有無をチェックし、有益な情報を与えてくれるツールも存在する。それを活用するのもよし。


memo

  • リソースの解放漏れ防止
    本書では共有インスタンスの結合度を下げる事が書かれていた。
    個人的には、C++でのスマートポインタ的なラッパークラスを実装をするのも悪くないと感じたが、
    ファイルのOpen/Closeのような場合だと呼び出すタイミング、スコープをよく考慮する必要があるので△
    (ムダに複数回open/closeされる恐れがある)
  • リソースの割り当て・解放順序
     (これもC++経験での話になるが)C++等での親子クラスのコンストラクタ・デストラクタがコールされる順序と同様。のはず。
  • 余談
    Javaでは、使用後のオブジェクトにはNULLを設定する事がある。
    (オブジェクトの参照カウントを減らし、0になるとガベージコレクションが作用し自動で削除される機能が備わっている)
    これにより、不要なメモリ使用量の増加を防ぐので、長期間で見ると大きな差が生まれる。
    (C、C++における、”参照メモリの解放後にはポインターにNULLを設定する”のと行為は近い)
脚注
  1. メモリにはスタック領域とヒープ領域がある。関数内で定義された変数や引数で受け取ったパラメータはその関数の実行中のみ使用されるので、LIFOのデータ構造とする事で効率よくメモリを使用できる。一方でヒープ領域は、動的にメモリ確保・解放する際に用いられる。 ↩︎

  2. セマンティック=意味付けされた、なので「意味が変わらない事」? ↩︎

ぽんみーぽんみー

23. 契約による設計(DbC)

契約プログラミングについて。

Bertrand MeyerはEiffelという言語で「契約による設計(DbC)」というコンセプトを提唱した。
ざっくり言うと、「要求された以上のことも、以下のことも行わない」。

以下の3つを設ける。

  • 事前条件
    この機能を呼び出す前に満たすべき条件、つまりこの機能からの要求。
    事前条件に違反している場合、この機能を利用できないようにしておく。
    適切なデータを渡すのは、呼び出し側の責任である。
  • 事後条件
    この機能が終了した後、この機能が保証する内容。
    つまり、この機能が正常に終了する事を示す。
    永久ループはNG。
  • クラス不変表明
    メソッドが呼び出される前後で、常に保持されるべき状態などを指す。
    安全性、整合性を保つために必要で、これがないと予期しない方法で動作する可能性があるため、データの不整合やバグを引き起こす。
    (特に並列処理で重要になってくる)

義務と利益

クライアントとサプライヤーで例える。

クライアント

  • 義務
    事前条件を満たした状態で、メソッドを呼び出すこと。
  • 利益
    メソッド終了時に、事後条件が満たした状態が得られること。

サプライヤ

  • 義務
    メソッド終了時に事後条件を満たすこと。
  • 利益
    事前条件を満たした状態で、メソッドが呼び出されること。

事後条件について

事後条件を満たしていないケース。

Bad

def faulty_withdraw(balance, amount):
    """
    A faulty withdrawal function that violates the postcondition.
    """
    if balance < amount:
        raise ValueError("Insufficient funds")
    return balance  # Oops! Forgot to subtract the amount

残高が負の値でないかをチェックしているものの、預金から引き出した後の差額を返していない。

Good

事後条件を満たしているケース。

def withdraw(balance, amount):
"""
Withdraw money from an account.
Precondition: balance >= amount
Postcondition: new balance = old balance - amount
"""
if balance < amount:
    raise ValueError("Insufficient funds")
return balance - amount

不変条件について

ソートクラスを例として記載。

class SortedList:
    def __init__(self):
        self._items = []

    def add(self, item):
        # 事前条件: なし
        # 事後条件: 要素が追加されること
        self._items.append(item)
        # 不変条件を維持するためのソート
        self._items.sort()

    def get_items(self):
        return self._items.copy()

# 不変条件を破壊する操作
bad_sorted_list = SortedList()
bad_sorted_list.add(3)
bad_sorted_list.add(1)
bad_sorted_list.add(2)

# 直接内部リストを操作して不変条件を破壊
bad_sorted_list._items[1] = 4

print(bad_sorted_list.get_items())  # [1, 4, 3] - ソートされていない!

このクラスは、「リストが常にソートされた状態であること」が不変条件として挙げられるが、

# 直接内部リストを操作して不変条件を破壊
bad_sorted_list._items[1] = 4

このコードにより事前条件・事後条件をチェックせず直接呼び出し、不変条件を破壊していると言える。
private属性やイミュータブルなデータ構造を作るべきである。

(⭐️重要)呼び出す側・呼び出される側が結ぶ契約

呼び出し側によって、呼び出される機能の事前条件がすべて満足された場合、当該機能は処理完了時点ですべての事後条件と不変表明を満足させるものとする。

いずれか契約条項が満たされなかった場合、例外のスローやプログラムの終了などといった結果が引き起こされるものとする。

memo

  • フロント・バックエンド(API)間でも適用できる法則と感じた
    Wikiではクラス間で例えられているが。
ぽんみーぽんみー

25. 表明を用いたプログラミング

要件定義や設計、コーディングなど全てに潜んでいるものとして「そんな事は起こり得ない...」という考えを、誰もが密かに抱いてしまう。

  • もし起こり得ないというのであれば、表明(アサーション)[1]を用いてそれを保証すること

assertを用いて実装しよう。
パラメータや結果がnullであるはずがなければ、それをチェックする。

assert result != null && result.size() > 0 : "Empty result from XYZ";

表明は、有効化したままにしておくこと

よくある誤解として、「本番環境にデプロイされたらもう必要ないのでは」というものがある。
数人にしか利用されない開発環境よりも、本番環境の方があり得ない事象が起こりがちなので、そのままにしておくべきとの事。
もしパフォーマンス上の懸念点があるのなら、”実際に影響のある部分”のみOFFにするのは良い。
例)ソート処理の結果を確認、データの再読み込みなど。

演習問題(面白かったので記載)

これらの「起こり得ない事象」のうち、実際に起こりそうなものは?

  • 1ヶ月は28日よりも少ない
  • カレントディレクトリにアクセスできないと言うエラーがシステムコールから返ってくる
  • C++において、a = 2; b = 3; にもかかわらず ( a + b )が5にならない
  • 三角形の内角の和が180度でない
  • 1分が60秒でない
  • ( a + 1 ) < a

memo

  • テストコードで「あり得ない」ケースをチェックするという訳ではないのか...。
  • ありがたみを知るために、契約による設計・プログラミングと一緒に理解を深め直そう。。
脚注
  1. 表明 ↩︎

ぽんみーぽんみー

24. 死んだプログラムは嘘をつかない

  • 例外的事象が発生すると、まずコードを読み始める人がたくさんいる。
     →⭐️エラーメッセージを読む事が当たり前だけど大事
  • 例外を補足後、メッセージを出力し、再スローする人がいる
    • ロジックが埋もれて見にくい
    • コードの結合度が上がるので要注意!
      新たな例外を追加すると、それにあわせて修正が必要。

できるだけ早期にクラッシュさせる

早期に問題を検出する事で、早めにクラッシュ(停止)にもっていける事が”メリット”と記されていた。
ErlangやElixirなどの言語は、そのルールに従っている。

Erlangを生み出したJoe Armstrongは、「防衛的プログラミングは時間の無駄だ。クラッシュさせろ!」述べている。

  • プログラムに問題発生時、その問題を”スーパーバイザ”が引き受ける
  • スーパーバイザ自体が問題を起こした時、スーパーバイザがそのイベントを管理する
    →「スーパーバイザツリー」という。
    可用性の高いフォールトトレラントシステムではこれらの言語の普及が進んでいる!

「障害によって中途半端に動き続けているプログラムよりも、停止したプログラムの方がダメージは少ないはずですから。」

memo

防衛的プログラミングに従っていたけど思想の違いか。。人命にかかわるようなシステムでなければ今回のような感じでいいか。

ぽんみーぽんみー

20. デバッグ

  • バグが起きた時、犯人探しするのではなく、問題を修復することに徹する
  • とにかく「パニクるな」
  • コード修正する前に再現確認すべし
  • バグに遭遇したら、ただ修正するだけでなく、発生した原因も探る

学び

  • スタックトレースを見る場合。
     ⭐️アルゴリズムである二分探索法[^1]を生かして見てみるべし
    →真ん中辺りをみて、それよりも前にエラーが発生しているのか、そうでないかを判別。
  • データセットを使った際に発生するバグも同様のアプローチが考えられる。

memo

  • 二分探索で見ていくという発想が驚きだった。面白い。

[^1] : 本書では、問題解決時に挙げられる分割統治法も例として紹介されていたが、二分探索の例えだけでしっくりきたので割愛。