(4) 柔軟なインターフェースをつくる
インターフェース
クラス間の窓のようなもの。界面や接触面、中間面。クラス内のメソッドと、何をどのように外部に晒すのか。
オブジェクト志向アプリケーションはクラスから成り立つが、「メッセージ」によって定義される。 アプリケーション設計で中心となるものは、ドメインオブジェクトではなくオブジェクト間で交わされるメッセージ。 クラスはソースコードリポジトリに何が入るのかを制御し、「メッセージ」は実際の動きやいきいきとしたアプリケーションを反映する。 このメッセージ交換はパブリック(公開された)インターフェースに沿って行われる。
適切に定義されたインターフェースは、根底にあるクラスの責任をあらわにし、最大限の利益を最小限のコストで提供する安定したメソッドから成る。
クラス内のメソッドは全て同じではなく、より一般的なものもあれば、ほかのものより変わりやすいものもある。 左は、クラスが全てを明らかにしている。どのオブジェクトのどのメソッドであってもその粒度に関わらず、他のオブジェクトから実行できるようになってしまっている。
クラスがすること(クラスの持っているより具体的な単一責任)は、おのずとその目的を果たすものになる。 したがって、パブリックインターフェースは クラスの責任を明確に述べる契約書 ともいえる。
どのクラスもパブリックな部分(=安定した部分)とプライベートな部分(=変化しうる部分)に分けられる。
パブリックインターフェース
クラスのパブリックインターフェースを作り上げるメソッドによって、クラスが外の世界へ表す顔(フェース)が構成される。
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
パブリックインターフェースに含まれるメソッドは次のようにあるべき
- 明示的にパブリックインターフェースだと特定できる
- 「どのように」より「何を」になっている
- 名前は考えられる限り変わり得ないもの
- オプション引数としてハッシュをとる
プライベートインターフェース
- 実装の詳細に関わる
- 他のオブジェクトから送られてくることは想定されていない
- どんな理由でも変更されうる
- 他者がそこに依存するのは危険
- テストでは言及さえされないこともある
新たにコードを追加する場合、普通は既存の設計を拡張する →最初にコードを書くということは今後の拡張されうる既存の設計を決定するということ
適切に定義されたインターフェースが存在することは、それが完璧であることよりも重要
インターフェースこそがアプリケーションを定義し、その将来を決定づけるもの
rubyのpublic, protected, private
- どのメソッドが安定で、どのメソッドが不安定かを示すため
- アプリケーションのほかのところにどれだけメソッドが見えるかを制御するため
Rubyだとこれらをpublicに再定義できるので、これらは絶対的な制限ではない。 = キーワードはアクセスを拒否するわけではなく、大変にするだけ。
プライベートインターフェースにはなるべく依存しないようにする。もしどうしても依存しなければならない場合は依存を隔離する。
パブリックインターフェースを構築する際は、そのパブリックインターフェースが他者に要求するコンテキストが最少限になるようにする。メッセージの送り手がクラスがどのようにその振る舞いを実装していることを知ることなく求めているものを得られるように作る。
private
もっとも不安定な部類のメソッドを示し、設定される可視性はもっとも限定的。受け手が省略された形式で呼び出されなければならない。(受け手が明示されている状態では呼び出されるべきではない)
- Tripクラスがfun_factorというプライベートメソッドを持つ。Trip内からself.fun_factor, 他のオブジェクトからa_trip.fun_factorを送ることもしてはいけないが、Tripのインスタンスとそのサブクラスの内部から、自分(デフォルトで送られる省略された受け手)が受けるfun_factorを送ることはできる。
protected
受け手がself、もしくはselfと同じクラスかサブクラスのインスタンスである場合に受け手を明示できる
- Tripクラスのfun_factorはprotected selfがa_tripと同じ種類のもの(クラスか、サブクラス)であるクラスの場合、常にself.fun_factorを送るようにもできる。a_trip.fun_factorもOK。
以下の記事めっちゃわかりやすい http://rubyblog.pro/2016/11/public-protected-private-in-ruby
リファクタリング具体例
ドメインオブジェクト
データと振る舞いの両方を兼ね備えた名詞。大きくて目に見える現実世界のものを表し、最終的にデータベースに表されるもの。 Customer, Trip, Route, Bike, Mechanic
シーケンス図
シーケンス図はオブジェクト間で交わされるメッセージを明記する。シーケンス図はパブリックインターフェースをあらわにし、実験し、そして最終的には定義するための手段となる。
オブジェクトが存在するから、メッセージを送るのではない。メッセージを送るためにオブジェクトは存在する。
4.3
Moeが知っていること
- 旅行の一覧が欲しい
- suitable_tripsメッセージを実装するオブジェクトがある。
Moeはsuitable_tripsメッセージをtripに送り、そのレスポンスを得ている。CustomerであるMoeがsuitable_tripsメッセージをTripクラスに送り、Tripクラスはそのメッセージを処理するためにアクティブにされ、処理が終わったら、レスポンスを返す。
Customorがsuitable_tripsメッセージを送るのはOK。でもTripが受け取るのは良くない。
ある旅行に対して、自転車が利用可能かどうかをTripクラスが見つけ出すべきではない。(この受け手は、このメッセージに応える責任を負うべきなのだろうか?)それをすべきなのはBicycleクラスなのではないか。
- Tripはsuitable_tripsに責任を持つ
- Bycycleはsuiable_bicycleに責任を持つ
4.4
Tripから余計な責任を取り除いて、それをcustomerに移しただけ
Moeが知っていること
- 旅行の一覧が欲しい
- suitable_tripsメッセージを実装するオブジェクトがある。
- 適切な旅行を見つけることには適切な自転車を見つけることも含まれる
- suitable_bicycleメッセージを実装する他のオブジェクトがある。
→ 問題点は、moe自身が何を望むかだけではなく、他のオブジェクトが共同作業をして、「どのように」望むものを準備するのかまで知っているということ(customerクラスが旅行の適切さを評価するためのアプリケーションルールの所有者になっている)
4.5
手続的なコーディングスタイルはオブジェクト志向の目的を打ち砕くので、上記のような構成はよくない
TripはMechanicに「私は何を望んでいるか知っているし、あなたがそれをどのようにやるかも知っているよ」と伝えている。
- Tripのパブリックインターフェースはbicyclesメソッドを含む
- Mechanicのパブリックインターフェースは、clean_bicycle, pump_tires, lube_chain, check_brakes メソッドを含む
- Tripは、clean_bicycle, pump_tires, lube_chain, check_brakes メソッドに応答できるオブジェクトを持ち続けることを想定する
→ TripはMechanicが行うことについて、詳細をいくつも知っていて、新たな手順を加えた場合はTripも変わらなければならない。
受け手にどのように振る舞うか(どのように)を伝えるメッセージになっている。
送り手の望みを頼むメッセージ(何を)と受け手にどのように振る舞うか(どのように)を伝えるメッセージの違いは大きい。
4.6
TripはMechanicに「私は自分が何を望んでいるかを知っていて、あなたが何をするかも知っているよ」と伝えている。
- Tripのパブリックインターフェースはbicyclesメソッドを含む
- Mechanicのパブリックインターフェースはprepare_bicycleメソッドを含む
- Tripはprepare_bicycleに応答できるオブジェクトを持ち続けることを想定する
TripとMechanic間の会話が「どのように」から「なにを」に変わってパブリックインターフェースのサイズが小さくなった。受け手にどのように振る舞うか(どのように)を伝えるメッセージから、送り手の望みを頼むメッセージ(何を)になった。(多くのメソッドをそとに晒していたのに、今はprepare_bicycleのみ) = ほかのところから依存されるメソッドがわずかしかない。
Tripは単一の責任を持つけれど、コンテキストを求める。
Tripはprepare_bicycleに応答できるオブジェクトを持ち続けることを想定しているので(旅行の準備にはいつでも自転車の準備が求められていて、Tripは常にprepare_bicycleメッセージを自身のMechanicへおくらなければならないので)、prepare_bicycleメッセージに応答できるMechanicのようなオブジェクトを用意しない限り、Tripを再利用するのは不可能。
オブジェクトがそのコンテキストから完全に独立することを模索する
→ 相手がだれかも何をするかも知らずに他のオブジェクトと共同作業できるオブジェクトはまだ予期していなかった方法で再利用できる
共同作業するためのテクニック→ 依存オブジェクトの注入
4.7
- prepare_trip(self) TripはMechanicに何を望むかを伝え、selfを引数として渡す
- bicycles Mechanicは準備が必要なBicycleの集まり(bicycles)を得るため、Tripにコールバックする
TripはMechanicに「私は自分が何を望んでいるかを知っているし、あなたがあなたの担当部分をやってくれると信じているよ」と伝えている。
- Tripが何を望んでいるか → 準備されること
- 旅行が準備されなけらばならないという知識 → Tripの責任の領域
- 自転車が必要だという事実 → Mechanicの領域
自転車を準備する必要があるというのは、Tripが「どのように」準備されるかであり、Tripが「何を」望むかの答えではない。
- Tripのパブリックインターフェースはbicyclesを含む
- Mechanicのパブリックインターフェースを含む、ひょっとするとprepare_bicycleを含みうる
- Tripはprepare_tripに応答できるオブジェクトを持ち続けることを想定する
- Mechanicはprepare_tripとともに渡されてきた引数がbicyclesに応答することを想定する
4.8
custmerがsuitable_tripsメッセージを送るのは、全く妥当(これはまさにcustomerが望むこと)
問題は送り手ではなく、受け手にある。このメソッドを実装する責任を持つオブジェクトをまだ特定できていない。 → TripFinderクラスが必要。
適切な旅行(suitable trips)を見つける責任をTripFinderが負っている
TripFinderは何がどうなれば適切な旅行になるかの知識を全て持っている(安定したパブリックインターフェースを提供し、また、変化しやすい乱雑な内部実装の詳細は隠している。)
この振る舞いはCustomerから抽出されているので、他のどんなオブジェクトからも隔離されたまま使用できる(ほかの旅行会社が適切な旅行を見つけるためにwebサービスを経由して、TripFinderを使う)
デメテルの法則(LoD: Low of Demeter)
- オブジェクトを疎結合にするためのコーディング規約の集まり
- デメテルは、3つ目のオブジェクトにメッセージを送る際に異なる型の二つ目のオブジェクトを介することを禁じる。(直接の隣人にのみ話しかけよう、ドットは一つしか使わないようにしよう)
class Trip def depart hogehoge customer.bicycle.wheel.tire # 遠くの属性、tireをとりにいっている customer.bicycle.wheel.rotate # 遠くの振る舞い、rotateを実行している hash.keys.sort.join(', ') fugafuga end end
これらのメソッドチェーンにはTRUEになっていないものがある
- wheelがtire, rotateを変更するとdepartも変わらなければならないかもしれない。(reasonableではない)
- tire, rotateの変更はdepart内の何かを壊す可能性がある。(transparentではない)
- Tripを再利用するためには、wheelとtireを持つbicycleを持つcustomerにアクセスできるようにする必要がある(usableではない)
- 似たような問題を抱えるコードが再生産されやすい(examplaryではない)
特定の状況に限っては中間のオブジェクトを介して、遠くの属性を取得することが一番コストが低い方法かもしれない →変更のコストとその頻度、それと違反を修正するコストを比べ、バランスをとる
hash.keys # return Enumerable hash.keys.sort # return Enumerable hash.keys.sort.join # return String
中間のオブジェクトは全て同じ型を持つので、デメテルの法則の違反はない。
移譲(他のオブジェクトにメッセージを渡すこと)を使い、列車事故を避けるデメテルの法則に形だけ従い、その主旨に反するコードになってしまう場合がある
https://ruby-doc.org/stdlib-2.4.0/libdoc/forwardable/rdoc/Forwardable.html
customer.ride # customerのrideメソッドは、Tripから実装の詳細を隠す
デメテルの法則違反は、パブリックインターフェースが欠けているオブジェクトがあるのではないかというヒントになる。
まとめ
- アプリケーション設計で中心となるものは、ドメインオブジェクトではなくオブジェクト間で交わされるメッセージ。そのメッセージ交換はパブリック(公開された)インターフェースに沿って行われる。インターフェースとは、クラス内のメソッドと、何をどのように外部に晒すのかを示す、クラス間の窓のようなもの。
- 適切に定義されたインターフェースは、根底にあるクラスの責任をあらわにし、最大限の利益を最小限のコストで提供する安定したメソッドから成る
- パブリックインターフェースが依存しても安全なように設計されるのに対して、プライベートインターフェースは、他者がそこに依存するのは危険(具体的には、rubyではpublic, protected, private を使う)
- インターフェースこそがアプリケーションを定義し、その将来を決定づけるもの
- デメテルの法則違反は、パブリックインターフェースが欠けているオブジェクトがあるのではないかというヒントになる。
- デメテルの法則は、3つ目のオブジェクトにメッセージを送る際に異なる型の二つ目のオブジェクトを介することを禁じること。(ただし、中間のオブジェクトが全て同じ型を持つ場合はその限りではない)
一言
インターフェースの例として、レストランの厨房とフロアの例はわかりやすかった。(書籍参照)
参考リンク
(3) 依存関係を管理する
オブジェクトは単一の責任を持っている→複雑な問題を解決するためにはオブジェクト間で共同作業をする必要がある
一方のオブジェクトに変更を加えたとき、他方のオブジェクトも変更しなければならない=依存関係がある
class Gear def initialize(hoge, fuga, rim, tire) hogehoge end def gear_inches ratio * Wheel.new(rim, tire).diameter # 「他のクラスの名前」「引数」と「その順番」「self以外のどこかに送ろうとするメッセージの名前」の四つの依存がある。 end fugafuga end
オブジェクト間の結合(CBO: Coupling Between Objects)
GearがWheelを知れば知るほど両者間の結合はより強固になる。結合が強固になればなるほど、その二つはあたかも一つのエンティティのように振る舞うようになる。
オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある。
1.他のクラスの名前
2.self以外のどこかに送ろうとするメッセージの名前
3.メッセージが要求する引数、引数の順番
一定の依存関係が二つのクラス間に築かれるのは避けられない。でも上記は不必要な依存。
1.他のクラスの名前
依存オブジェクトの注入(dependency injection)
wheelの名前に変更があったときに、Gearのgear_inchesメソッドも変更する必要がある
- GearがWheelに協力を求める必要があるとき、結局何かが、どこかでWheelクラスのインスタンスを作らなければならないから
- 名前の変更はエディターでできる
上記の理由からこの依存は無害に思える。
しかし、この依存は、明示的に「wheelインスタンスのギアインチしか計算する意思はない」と宣言していることになる。(たとえ直径を持ち合わせ、ギアを扱うオブジェクトであったとしてもwheel以外との共同作業の拒絶を意味する)
オブジェクトのクラスが重要なのではない。送ろうとしている「メッセージ」こそが重要。
Gearがアクセスする必要があるのは、diameterに応答できるオブジェクト(diameterを知っているオブジェクト)、つまりダックタイプのオブジェクト(gearはそのオブジェクトのクラスも気にせず、知るべきでもない)
class Gear def initialize(hoge, fuga, wheel) hogehoge @wheel = wheel end def gear_inches ratio * wheel.diameter end fugafuga end # gearはdiameterを知るduckを要求する Gear.new(54, 11, Wheel.new(38, 1.4)).gear_inches # Gear.new(54, 11, Bar.new(38, 1.4)).gear_inches 再利用ができる
インスタンス(Wheelインスタンス)の作成をクラス(Gear)の外に移動することで、二つのクラス間の結合が切り離される。=依存オブジェクトの注入
クラス名を知っておく責任や、そのクラスに送るメソッドの名前を知っておく責任がどこか他のクラスに属するものなのではないかとうたがえる能力がないと依存オブジェクトの注入は使えない。
依存を隔離する
- インスタンス変数の作成を分離する(dependenby injectionができない場合)
class Gear def initialize(hoge, fuga, rim, tire) hogehoge @wheel = Wheel.new(rim, tire) end fugafuga end
class Gear attr_reader :hoge, :fuga, :rim, :tire def initialize(hoge, fuga, rim, tire) hogehoge end def wheel @wheel ||= Wheel.new(rim, tire) end end
外部のクラス名に対する依存をどのように管理するか 依存するものを常に気にとめ、それらを注入することを習慣化せていけば疎結合なコードになる なんの工夫もなくその場その場でクラスを参照していくようにしない。
2.self以外のどこかに送ろうとするメッセージの名前
脆い外部メッセージ(self以外に送られるメッセージ)を隔離する
ratio, wheelはselfに送る、wheelに送っていくdiameterは外部メッセージ
gear_inchesは、wheelがdiameterに応答(diametarはwheelにメッセージを送る)することに依存している gear_inchesがwheelがdiameterを持つことを知っている
# 恐ろしい計算 foo = some_intermediate_result * wheel.diameter # 恐ろしい計算 end
gear_inchesは、selfに送るメッセージに依存するようになった。
# 恐ろしい計算 foo = some_intermediate_result * diameter # 恐ろしい計算 end def diameter wheel.diameter end
このテクニックが必要になるのは、メッセージへの参照がクラス内に埋め込まれていて、さらにそのメッセージが変わる可能性が高い時。
3.メッセージが要求する引数、引数の順番
引数が必要なメッセージをおくるとき、送り手側としては、それら引数についての知識をもたざるを得ない。
引数の順番への依存を取り除く
class Gear def initialize(hoge, fuga, wheel) @hoge = hoge @fuga = fuga @wheel = wheel end end Gear.new(25, 1.4, Wheel.new(23, 1.4)).gear_inches
class Gear def initialize(args) @hoge = args[:hoge] @fuga = args[:fuga] @wheel = args[:wheel] end end Gear.new(hoge: 25, fuga: 1.4, wheel: Wheel.new(23, 1.4)).gear_inches
メリット
- 引数の順番に対する依存性がすべて取り除かれる
- キー名が引数に関する明示的なドキュメントとなっている
デメリット
- 冗長性の増加(この冗長性には価値がある、冗長性は現時点での必要性と未来の不確実性が交差するところに存在)
- ハッシュのキー名への依存(この新しい依存は元の依存より安定していて、変更が強制されるリスクが低くなっている)
使用するかどうかはそのときによって変わる
外部から使用されることを目的としたフレームワークにおいて、かなり不安定で常用な引数をもつメソッド → 全部をハッシュ化して使った方が全体のコストが削減される可能性は高い
自分でしか使わず2つの数字を分けるだけのメソッドを書いている場合 → 単純に引数を渡して、順番への依存を拠有する方がシンプルだし、コストも安上がり
メソッドが、安定性の高い引数を数個とそれよりは安定性のないオブショナルな引数をいくつも受け取るという場合。 → 二つ組み合わせる。link_to の引数みたいな感じ
def initialize(args) @chainring = args[:chainring] || 40 @cog = args[:cog] || 18 @wheel = args[:wheel] end
# hashの[]メソッドは存在しないキーに対してはnilを返すということを頼りにしている @bool = args[:boolean_thing] || true # いずれにせよ、@boolにtrueが設定されるようになってしまっている。
したがって真偽値を引数にとったり、引数のfalseとnilの区別が必要なのであれば、デフォルト値の設定にはfetchメソッドを使う方がよい。
fetchメソッドが期待するのは、フェッチしようとしているキーがフェッチ先のハッシュにあること(なかったら、第二引数の値を返す)
Hash#fetch Returns a value from the hash for the given key. If the key can't be found, there are several options: With no other arguments, it will raise an KeyError exception; if default is given, then that will be returned; if the optional code block is specified, then that will be run and its result returned. h = { "a" => 100, "b" => 200 } h.fetch("a") #=> 100 h.fetch("z", "go fish") #=> "go fish" h.fetch("z") { |el| "go fish, #{el}"} #=> "go fish, z" The following example shows that an exception is raised if the key is not found and a default value is not supplied. h = { "a" => 100, "b" => 200 } h.fetch("z")
def initialize(args) @chainring = args.fetch(:chainring, 40) @cog = args.fetch(:cog, 18) @wheel = args[:wheel] end
デフォルト値が単純な数字や文字列以上のものである場合 initializeからデフォルト値を完全に除去し、独立したラッパーメソッド内に隔離
def initialize(args) args = defaults.merge(args) @chainring = args[:chairing] @cog = args[:cog] @wheel = args[:wheel] end def defaults {chainring: 40, cog: 18} end
依存せざるを得ないメソッドが固定順の引数を要求し、しかもそれが外部のものの場合、そのメソッド自体を変更することはできない場合。(フレームワークの一部、Gearの新しいインスタンスを自身のコード内で作成しなければならない) → 固定順の引数をオプションハッシュに置き換える
module SomeFramework class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end hogehoge end end module GearWrapper def self.gear(args) SomeFramework::Gear.new(args[:chainring], args[:cog], args[:wheel]) end end GearWrapper.gear( chainring: 52, cog: 11, wheel: Wheel.new(26, 1.5)).gear_inches
GearWrapperの責任は、SomeFramework::Gearのインスタンスを作成すること 目的が他のクラスのインスタンスの作成であるオブジェクト=ファクトリー
依存方向の管理
依存方向の選択は、「自身より変更されないものに依存するようにする」
- あるクラスは他のクラスより要件が変わりやすい
- 具象クラスは抽象クラスよりも変わる可能性が高い
- 抽象化されたものは、それらが共通し、安定した性質を表し、抽出元となった具象クラスより変わりにくい
- 動的型付け言語(ruby)ではインターフェースを定義するために明示的に抽象を宣言する必要はない。
- 静的片付け言語では意図的に、抽象インターフェースを定義する必要がある。
- 多くのところから依存されたクラスを変更すると広範囲に影響が及ぶ
その他の依存関係
破壊的な類の依存
いくつものメッセージをチェーンのようにつないで遠くのオブジェクトに存在する振る舞いを実行しようとする場合 → 第4章 柔軟なインターフェースを作る
大別できる依存関係
コードに対するテストの依存関係、テストが設計を駆動する テストはコードを参照するがゆえにコードに依存する → 第9章 費用対効果の高いテストを設計する
まとめ
オブジェクトが次のものを知っているとき、オブジェクトには依存関係がある。
1.他のクラスの名前
- 依存オブジェクトの注入(dependency injection)
- インスタンス変数の作成の分離
2.self以外のどこかに送ろうとするメッセージの名前
- 外部メッセージ(self以外に送られるメッセージ)を隔離
3.メッセージが要求する引数、引数の順番
- 初期化の引数にハッシュを使う
- 明示的にデフォルト値を設定する
- デフォルト値が単純な数字や文字列以上のものである場合、initializeからデフォルト値を完全に除去し、独立したラッパーメソッド内に隔離
- そのメソッド自体を変更することはできない場合、固定順の引数をオプションハッシュに置き換える
依存方向の選択は、「自身より変更されないものに依存するようにする」
参考リンク
(2) 単一責任のクラスを設計する
TRUE 変更が簡単なコードに伴う性質
- Transparent(見通しが良い) 変更するコードにおいても、そのコードに依存する別の場所のコードにおいても、変更がもたらす影響が明白
- Reasonable(合理的) どんな変更があってもかかるコストは変更がもたらす利益にふさわしい
- Usable(利用性が高い) 新しい環境、予期していなかった環境でも再利用できる
- Exemplary(模範的) コードに変更を加える人が上記の品質を自然と保つようなコードになっている
TRUEなコードを書くためには、それぞれのクラスが明確に定義された単一の責任を持つように徹底する必要がある
クラスが単一責任かどうか見極める方法
あたかもそれに知覚があるかのように問いただすこと。
クラスのメソッドを質問に置き換えた時に意味をなす質問になっているか。 例: Gearさん、あなたの比(ratio)を教えてくれませんか? → OK Gearさん、あなたのgear_inchesを教えてくれませんか? → NG gear_inchesは自転車の情報なので、gearに聞くのは筋違いではないか。 Gearさん、あなたのタイヤのサイズを教えてくれませんか? → NG タイヤのサイズは自転車の問題なのではないか。 Gearさん、あなたのタイヤ(tire)はなんですか?→ OK タイヤがgearの属性になっているから
一文でクラスを説明してみること
考えつく限り短い説明に「それと」が含まれていれば、おそらくクラスは二つ以上の責任を負っている。「または」が含まれていれば、二つ以上責任があるわけではなく、互いにあまり関連もしない責任を負っている。
凝縮度
クラス内の全てがそのクラスの中心的な目的に関連していれば、このクラスは凝縮度が高い、もしくは単一責任であると言われる。単一責任の原則(RDD)はRDD(Responsibility Driven Design)という概念に由来。「クラスはその目的を果たすための複数の責任を持つ」
設計を決定するときを見極める方法
今日何もしないことの将来的なコストはどれだけだろうと考える。 とても小さなアプリケーション、未来は不確か、初期段階での知識量はプロジェクト全体でいえばもっとも少ない。最も費用対効果の高い行動は「より多くの情報を持つこと」。
何もしないことによる将来的なコストがいまと変わらないときは、決定は延期する。決定は必要なときにのみその時点で持っている情報を使ってする。
現時点での要件と未来の可能性の相互間のトレードオフをよく理解し、コストが最小になるように決断を下す。
変更を歓迎するコードを書く
振る舞いはメソッド内に捉えられていて、メッセージを送ることによって実行できる。 →単一責任のクラスをつくれば、どんな些細な振る舞いもそれぞれがただ一つに存在 = DRY
オブジェクトは、振る舞いに加えデータを持ち、インスタンス変数に保持される。
インスタンス変数の隠蔽
データへのアクセスは、インスタンス変数を直接参照するのか・隠蔽するのかの二つのうちどちらかで行われるが、インスタンス変数は常にアクセサメソッドで包み、直接参照しないようにする。
Module#attr_reader
Creates instance variables and corresponding methods that return the value of each instance variable. Equivalent to calling “attr:name” on each name in turn. String arguments are converted to symbols.
# attr_reader :hoge def hoge @hoge end
cogメソッドはコード内で唯一cogが何を意味するのかをわかっている箇所。 コグは、データ(どこからでも参照される)から振る舞い(一箇所で定義される)へと変わる。
デメリット
- 可視性、publicなcogメソッドを定義すると、クラス外のアプリケーション内の他のオブジェクトにも公開される。またprivateなcogメソッドの場合は、その振る舞いをアプリケーション全体に公開せずに済む。
- データも普通のオブジェクトも一見区別ができなくなる。
データ構造の隠蔽
インスタンス変数に結びついていることがよくないのであれば、複雑なデータ構造への依存はさらによくない。データ構造に関する複雑な情報は隔離されるべき。
obscuring_references.rb dataメソッドは単に配列を返すだけ。dataメッセージの送り手それぞれが何のデータが配列のどのインデックスにあるかを完全に知っていなければならない。 またdiametersメソッドが知っているのは、直径を計算する方法だけではなく、リムとタイヤのがどこにあるかも知っている。→配列の構造に依存しているため、配列にデータを持つとすぐにあちこちで配列の構造に参照するようになる
revealing_references.rb diametersメソッドは配列の内部構造について何も知らない。 渡されてくる配列の構造に関する知識はすべてwheelifyメソッド内に隔離。wheelifyメソッドは、arrayの配列をStructの配列に変換。
Struct [明示的にクラスを書くことなく、いくつもの属性を1ヶ所に束ねるための便利な方法。](http://ruby-doc.org/core-2.4.1/Struct.html)
メソッドから余計な責任を抽出する
メソッドもクラスのように単一の責任を持つべき
def diameters wheels.collect {|wheel| wheel.rim + (wheel.tire * 2) } end
wheelの繰り返し処理と、wheelの直径の計算、二つ責任を持っている。
def diameters wheels.collect {|wheel| diameter(wheel)} end def diameter(wheel) wheel.rim + (wheel.tire * 2) end
それぞれの要素に対し実行される処理から、繰り返しを分離することはよくある
def gear_inches ratio * (rim + (tire * 2)) end
車輪の直径の計算とgear_inchesの計算の二つ責任を持っている。
def gear_inches ratio * diameter end def diameter rim + (tire * 2) end
最終的な設計がわかっていない段階でもするべき
単一責任のメソッドがもたらす恩恵
- 隠蔽されていた性質を明らかにする
- クラスをリファクタリングし、全てのメソッドが単一の責任を持つようにするとクラスが明確になる
- コメントをする必要がない
- そのメソッド内のコードにコメントが必要なくらいなら、そのコードを別のメソッドに抽出する
- 再利用を促進する
- 他のクラスの移動が簡単
クラス内の余計な責任を隔離する
Gearクラスにはいくつか車輪(wheel)のような振る舞いがある ではこのアプリケーションにWheelクラスは必要なのだろうか?
いろいろな場合が選択肢にある。
でも目的は、設計に手を加える数を可能な限り最小にしつつ、Gearを単一責任に保つこと
変更可能なコードを書いているので、どうしてもしなければならないときまで決断を先延ばす。
WheelをGearに組み込むことは長期的な設計目標でないことは明らか Gearをきれいにしながらもwheelに関する決定は遅らせている。
その後、自転車の車輪の円周もほしいと言われる。=WheelクラスをGearから独立させて使いたいという明確なニーズ Wheelの振る舞いをGearクラス内に隔離しておいたおかげでこの変更は難しくない。Wheel Structを独立したWheelクラスに変えて円周を計算するcircumstancesメソッドを追加するだけ
まとめ
ひとつのことに専念するクラス(単一責任のクラス)はその一つのことをアプリケーションの他の部位から隔離することで、悪影響を及ぼすことのない変更と重複のない再利用が可能になる
単一責任のクラスにするためにはそれぞれのメソッドも単一責任である必要がある
単一責任のクラスかどうか見極める方法
- [ ] あたかもそれに知覚があるかのように問いただすこと。
- [ ] 一文でクラスを説明してみること
設計を決定するときは、現時点での要件と未来の可能性の相互間のトレードオフをよく理解し、コストが最小になるように決断を下す。
また変更を歓迎するコードを書くためには以下が必要
- [ ] インスタンス変数の隠蔽
- [ ] データ構造の隠蔽
メソッドからも余計な責任を抽出する(それぞれの要素に対し実行される処理から、繰り返しを分離する)
クラス内の余計な責任を隔離する
決定は必要になったときにのみ、その時点で持っている情報を使ってする。
参考リンク
(1) オブジェクト指向設計 from オブジェクト指向設計実践ガイド
システムを、あらかじめ決められた手続きの集まりではなく、オブジェクト間で受け渡されるメッセージの連続としてモデル化
部品が相互に作用しあい、全体の振る舞いが生まれる 部品がオブジェクト、相互作用はオブジェクト間で受け渡されるメッセージ メッセージの送り手は受け手を知っている必要がある→依存関係を作る
オブジェクト指向設計とは、依存関係を管理すること
オブジェクト設計とは、変更が簡単になるようにどんなコード構成にするか
5つの原則 SOLID
- Single responsibility 単一責任
- Open-closed オープン・クローズド
- Liskow Substitution リスコフの置き換え
- Interface Segregation インターフェース分離
- Dependency Inversion 依存性逆転
DRY
Don’t Repeat Yourself
LoD
Law of Demeter デメテルの法則
BUFD(Big Up Front Design)
提案されたアプリケーションのすべての機能の、想定される未来の内部動作をすべて特定し、完全に文書化すること
参考リンク
学校の課題でhtml/css/js縛りでなんか作るっていうのがあったので、shell scriptを書いてみた
要件
- 紙芝居、ページは画像と文字列、ボタン(次のページへ)で構成されており、たまにある入力の値の条件分岐によってページの遷移先を変える
- その入力値はページ間で保持する必要がある(縛りがあるのでDBとかは使えない。JSだけというか)
実装したこと
- ほとんどのHTMLが同じ構成なので、コマンドで引数でもたせて40枚近い HTMLを作るshell scriptを書いた。(なお一発コマンドでできるわけではなくて、内容に合わせて枚数文コマンドを打つ必要がある)
https://github.com/tenshotanaka/Shakure-Cinderella/blob/master/html-build.sh
- userの選択をcookieに保存し、その値によってページの遷移先を変えるようにした。なおライブラリは以下を使った。
https://github.com/js-cookie/js-cookie/releases/tag/v2.1.4
所感、感想
急いで書いたので、動くだけのクソコードになってしまった。jsとか絶対抽象化できるし、es6じゃないし。エンジニアとしてこのままにしてしまうのは恥ずかしいので一応issueにまとめた。でもこの機会でshell scriptに触れることができたのでよかった。やっぱりコード書くの楽しい。linux実践みたいな本読む時、復習したい。
参考リンク
www.shellscript.sh GitHub - tenshotanaka/Shakure-Cinderella: 情報基礎最終課題