6/1 から 8/24 まで、フィードフォース社内で『オブジェクト指向設計実
践ガイド』の読書会を主催しました。
社内で開催した『オブジェクト指向設計実践ガイド』の読書会終わり!今回読むのは2回目。画像の通り、だいぶがっつり読むことが出来たし、業務や個人のコードも良くなって満足。 pic.twitter.com/XUin6wBWpA
— マスタカ (@masutaka) August 24, 2017
↑ これはブログ記事に使うための先行 Tweet でした。やっと使えたw
都度メモを取って会社の Slack channel に書いていたのですが、公開の
タイミングを逃して後悔していました。そんな折、kano-e さんが会社の
新生開発者ブログに記事をポスト。
空気を読まずに私もポストします。個人ブログに。本当に五月雨式のメモ
で自分向けです。
第1章 オブジェクト指向設計
まだ意識が高まっておらず、感想を書いていなかった。プロローグ的な章。
第2章 単一責任のクラスを設計する
これらが当たり前に出来るスキルが必要だと思った。
- 健康的な書き方をほかのプログラマーに促進するコードを書く
- atter_reader 等を介して、複雑か簡単かどうかさえも見せないように
隠蔽する - リファクタリングでメソッドへの切り出しをすることで、クラスの責務
を明確にする - 決断を先延ばしにする
第3章 依存関係を管理する
- DI 初登場!!
- 第3章は P19 の『オブジェクト指向設計とは、「依存関係を管理する
こと」です。』を具体的に説明した章 - とにかく依存関係を減らすことが重要
- DI
- メソッドでキーワード引数を使うことで、パラメータの順序依存をなくす
- ファクトリパターンを使った外部インターフェイスのラッピング
- あまりに複雑だと、ラッピングしすぎないほうが良いことも
- 自身より変更しないものに依存しなさい
- Ruby にはインターフェイスがないので、エンジニアのスキルがな
いと振る舞いに気づかないことも
- 本質的に抽象はより安定
- Jpeg, Png, Gif などより、Image といった抽象度を高めたオブジェ
クトのほうが変更されづらい
- Jpeg, Png, Gif などより、Image といった抽象度を高めたオブジェ
第4章 柔軟なインターフェイスをつくる
◆ P102 の図は少し煙に巻かれた気がした
- “#prepare_trip” に self を渡す発想はなかった
- でも、self には “.bicycles” という振る舞いが必要だから、bicycles
をそのまま渡せば良いのでは? - CarMechanic class とか登場したら、 “.cars” という振る舞いを持つ
オブジェクトを渡さないといけないし - この例では P104 にもある「手放しの信頼」を醸し出すことが目的だっ
たのかな
◆ P111 デメテルの法則
- デメテルの法則はドットの数だけではなく、中間オブジェクトの型も考
慮に入れる- 前回 [2016-09-22-1] 読んだ時に見逃してた
- もっとも、そんなに単純な話でもなくて「パブリックインターフェイス
が欠けている可能性」を考慮に入れた視点が必要
◆ 身も蓋もないし、よく聞く話だけど、完璧なアプリケーションなど作れ
ない。それを追求し続けることが一番重要
- ここで言う「完璧なアプリケーション」はこの本の場合、「依存関係が
完璧に管理されたアプリケーション」なんだと思う
第5章 ダックタイピングでコストを削減する
P128
具象的なコードは、理解はかんたんですが、拡張にはコストが伴います。
抽象的なコードは、最初のわかりにくさは増すかもしれませんが、一度理
解してしまえば、はるかに変更しやすいのです。
(中略)
オブジェクトクラスについての不明瞭さを大目に見るという能力は、自身
を持った設計者であることを証明します。
ここが一番自分に刺さった。そう、具象的なコードは理解がかんたんなの
で、初心者はこっちが分かりやすいと惑わされてしまうのです。
今関わっているサービスがそんなコード。たしかに読めば何をしているか
分かる。でも具象的すぎて自分のスタックが溢れる。あと、変更が大変。
変更が大変。少しの変更にものすごく時間がかかる。
P133
賢くダックを選ぶ
“kind_of?” や “responds_to?” を使うことは必ずしも悪ではない。例に
出ているように、active_record ではそのようなコードがある。
しかし、 “kind_of?” などで確認されているクラスの安定性が違う。これ
は P80 の「自身より変更されないものに依存しなさい」と繋がる。
ダックタイピングは必ずしも初めから必要ない。必要な時期が来たら、ダッ
クを感じ取って、適切にリファクタリングされれば良いと思う。もちろん、
影響が少ない小さなモジュールなどは、初めから意識しても構わない。
第6章 継承によって振る舞いを獲得する
◆継承が効果を発揮すること
P153
① モデル化しているオブジェクトが一般ー特殊の関係をしっかり持っていること
② 正しいコーディングテクニックを使っていること
①は P159 の「すべてを下げてその中のいくつかを引き上げる」戦略によっ
て、解決できます。
②は例えば P172 の post_initialize を初めとしたフックメッセージを使
うことです。このテクニックにより、継承を使ってもサブクラスを疎結合
に出来ます。サブクラスは親のアルゴリズムを知らなくて良くなりますし、
いつ誰が当該処理をするかも知る必要がなくなります。継承で super を
使うのは最後の手段にすることが重要だと思います。super を使うことは、
事実上親のアルゴリズムを知ってしまっているという宣言になります。
◆雑感
何も考えずに継承を使うと、依存度の高いプログラムが出来上がる。そも
そも継承の例で安易に super 使いすぎ。洗脳っぽくなっているかも。
第7章 モジュールでロールの振る舞いを共有する
この章でモジュールが登場したことで、継承とどちらを使うか選択肢が増えた。
その中で P201 の「契約を守る」が重要らしい。
P202 契約を守ると、おのずとリスコフの置換原則に従っていることになります。
調べてみると「契約プログラミング」というものがあるらしい。
リスコフの置換原則
- 事前条件を派生型で強めることはできない。つまり、上位の型よりも強
い事前条件を持つ派生型を作ることはできない。- 事後条件を派生型で弱めることはできない。つまり、上位の型よりも弱
い事後条件を持つ派生型を作ることはできない。さらに、この原則によれば、派生型のメソッドが発生する例外は、上位の
型のメソッドが発生する例外の派生型か、上位のメソッドの例外と同じも
のでなければならない。共変性と反変性も参照。
このような契約を結ぶことで、制約は生まれるが、変更に強いコードを手
に入れることが出来るのだと思う。
でもこれ、結構きつくない?
とは言え、モジュールや継承を使ってコードを最適化するのは、具象クラ
スが 3 つ現れたときが目安(第6章 最終ページ P178)。
始めは「StringUtilsに文字列が空かどうかを聞く(P187)」実装になる
かもしれない。でもその文字列がまだ一種類だけだったら、まだ最適化は
早いと思う。
第8章 コンポジションでオブジェクトを組み合わせる
この章はだいぶ難しかった。継承や委譲など、もろもろまとめにかかって
いる。
一言で書くと「P175 の継承を使った実装をコンポジットされた実装にリ
ファクタリングしたら(P225)、めっちゃシンプルになった!」になるが、
周辺の説明が難しい。
実際に直面した時、うっかり継承を使ってしまいそう。
ただ、継承とそれについては、P233 に簡潔にまとめられている。簡潔す
ぎるけど!
- 継承とは、特殊化です
- 継承が最も適しているのは、過去のコードの大部分を使いつつ、新たな
コードの追加が比較的少量のときに、既存のクラスに機能を追加する場合
です- 振る舞いが、それを構成するパーツの総和を上回るなら、コンポジショ
ンを使いましょう
継承はリスコフの置換原則に当てはめられるように、親と子が置換可能で
なければいけない。しかし実際はそうでないケースにも継承が使われてい
る。そもそも継承はコストが高いけど、代わりの手段としてコンポジット
があるよ!ということ。
個人的には広義の委譲がコンポジットで、狭義の委譲が Ruby の
“Forwardable”、ActiveSupport の “delegate” かなと思いました。
あまり賛同は得られなかったので、頭が回ってなかったかもしれない。
P226 で配列の配列をデータとして扱っているやつ、以前書いた gem で似
たデータ構造を考えた時は、YAML にしたら良い感じで使えました。敢え
て “gist_id:” とか “filename:” とか付けてないのがポイント。
https://github.com/masutaka/gist_updater/blob/v0.4.3/gist_updater.yml.example
この gem 自体は、まだ私の修行が足りなくて、依存度を低く出来ていな
い実装あり。
今回は単純なデータ構造だったけど、階層の深い Hash を OpenStruct に
するには、こんなテクニックがありました。
$ irb -r ostruct -r json
irb(main):001:0> h = {hoge: {fuga: 2}}
=> {:hoge=>{:fuga=>2}}
irb(main):002:0> d = JSON.parse(h.to_json, object_class: OpenStruct)
=> #<OpenStruct hoge=#<OpenStruct fuga=2>>
irb(main):003:0> d.hoge
=> #<OpenStruct fuga=2>
irb(main):004:0> d.hoge.fuga
=> 2
OpenStruct は存在しないメンバにアクセスすると、nil が返っちゃうね。
例外にしてくれない。
そういう意味では Hash に対する優位性は、メッセージを介したやり取り
にできるだけか。
第9章 費用対効果の高いテストを設計する
とうとう最終章。ボリュームは最大だったが、注意深く読むと「ダックタ
イプをどうテストするか?」の解決に向かい続けており、読みづらさはな
かった。
ネタバレ(?)すると、P263 で放り投げられた問題はその後 P270 → P276
と進むことで解決される。
DI(依存性を注入)する実装を書いた時、テストで本物のオブジェクトを
注入するとコストが高くなる。これを回避するため、RSpec では double
を使うが、この double オブジェクト自体にもインターフェイスのテスト
が必要と、この本には書いてある。
本では MiniTest だが、RSpec で書くとこんな感じ。
- Wheel class 自身のテスト
- Wheel class を使う Gear class のテスト
- “shared_examples ‘a diameterizable interface’”
驚くべきは自分で作った double に対してもインターフェイスのテストを
していること。でもこうすることで、依存性を最小限にしつつ、インター
フェイスを変えた場合に必要な箇所のテストが落ちてくれる。
普通はこんなことしないので、この前提がないチームでこんな PR が作ら
れると、だいぶ議論になりそうでツライみたいな話はしました。
他、私が投げかけた疑問は P269 のコード。こんな private method のテ
スト(@observer が changed を呼んでいるか?)みたいなことは必要な
のか?
答えはやっぱり必要で、テスト対象が副作用を持つ/持たないで判断が大
きく分かれる。副作用がなければ当然必要なく、I/O のみテストすれば良
い。DB の更新など、副作用がある場合はこのようなテストが必要になる
こともある。
本では副作用のないメッセージを「クエリ」、副作用があるメッセージを
「コマンド」と命名している。
近況
[2016-09-22-1]
と今回の読書会を経て、自分の実装がガラッと変わった
と感じています。設計に小うるさくなったのは認める。
割とあっさり class を作って責務を分けますし、(この本の話からは外
れますが)場合によっては module_function で module を関数の集合体
にすることもあります。
ただ、最近学習している golang は OOP ではないので、習得した知識が
そのまま使えず、やるせなさを感じることも。
頑張ります。
いつかまた読み返すと思います。