新宿(近々代々木に移転)で働く18歳エンジニアのブログ

思ったこととか、技術的なこと書きます。

(4) 柔軟なインターフェースをつくる

インターフェース

クラス間の窓のようなもの。界面や接触面、中間面。クラス内のメソッドと、何をどのように外部に晒すのか。

オブジェクト志向アプリケーションはクラスから成り立つが、「メッセージ」によって定義される。 アプリケーション設計で中心となるものは、ドメインオブジェクトではなくオブジェクト間で交わされるメッセージ。 クラスはソースコードリポジトリに何が入るのかを制御し、「メッセージ」は実際の動きやいきいきとしたアプリケーションを反映する。 このメッセージ交換はパブリック(公開された)インターフェースに沿って行われる。

適切に定義されたインターフェースは、根底にあるクラスの責任をあらわにし、最大限の利益を最小限のコストで提供する安定したメソッドから成る。

https://gyazo.com/f40fa11bd39c9826abed4f01777a649b

クラス内のメソッドは全て同じではなく、より一般的なものもあれば、ほかのものより変わりやすいものもある。 左は、クラスが全てを明らかにしている。どのオブジェクトのどのメソッドであってもその粒度に関わらず、他のオブジェクトから実行できるようになってしまっている。

クラスがすること(クラスの持っているより具体的な単一責任)は、おのずとその目的を果たすものになる。 したがって、パブリックインターフェースは クラスの責任を明確に述べる契約書 ともいえる。

どのクラスもパブリックな部分(=安定した部分)とプライベートな部分(=変化しうる部分)に分けられる。

パブリックインターフェース

クラスのパブリックインターフェースを作り上げるメソッドによって、クラスが外の世界へ表す顔(フェース)が構成される。

  • クラスの主要な責任を明らかにする
  • 外部から実行されることが想定される
  • 気まぐれに変更されない
  • 他者がそこに依存しても安全
  • テストで完全に文書化されている

パブリックインターフェースに含まれるメソッドは次のようにあるべき

  • 明示的にパブリックインターフェースだと特定できる
  • 「どのように」より「何を」になっている
  • 名前は考えられる限り変わり得ないもの
  • オプション引数としてハッシュをとる

プライベートインターフェース

  • 実装の詳細に関わる
  • 他のオブジェクトから送られてくることは想定されていない
  • どんな理由でも変更されうる
  • 他者がそこに依存するのは危険
  • テストでは言及さえされないこともある

新たにコードを追加する場合、普通は既存の設計を拡張する →最初にコードを書くということは今後の拡張されうる既存の設計を決定するということ

適切に定義されたインターフェースが存在することは、それが完璧であることよりも重要

インターフェースこそがアプリケーションを定義し、その将来を決定づけるもの

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

https://gyazo.com/eda286a1c0c733de61ae3eaa29547d4e

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

https://gyazo.com/a3b34b04e4961ac6b5695ed08af0c7be

Tripから余計な責任を取り除いて、それをcustomerに移しただけ

Moeが知っていること

  • 旅行の一覧が欲しい
  • suitable_tripsメッセージを実装するオブジェクトがある。
  • 適切な旅行を見つけることには適切な自転車を見つけることも含まれる
  • suitable_bicycleメッセージを実装する他のオブジェクトがある。

→ 問題点は、moe自身が何を望むかだけではなく、他のオブジェクトが共同作業をして、「どのように」望むものを準備するのかまで知っているということ(customerクラスが旅行の適切さを評価するためのアプリケーションルールの所有者になっている)

4.5

https://gyazo.com/6ecf2ef2e94a1cac97285e73b1604c8e

手続的なコーディングスタイルはオブジェクト志向の目的を打ち砕くので、上記のような構成はよくない

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

https://gyazo.com/5f4f4cf7f95beaf47a336ec9bd5edfd9

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

https://gyazo.com/b0982728acda29648d42d44809b48eaf

  • 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

https://gyazo.com/a3b34b04e4961ac6b5695ed08af0c7be

custmerがsuitable_tripsメッセージを送るのは、全く妥当(これはまさにcustomerが望むこと)

問題は送り手ではなく、受け手にある。このメソッドを実装する責任を持つオブジェクトをまだ特定できていない。 → TripFinderクラスが必要。

https://gyazo.com/f0bef85cd548970efddcfdf893f33cd9

適切な旅行(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つ目のオブジェクトにメッセージを送る際に異なる型の二つ目のオブジェクトを介することを禁じること。(ただし、中間のオブジェクトが全て同じ型を持つ場合はその限りではない)

一言

インターフェースの例として、レストランの厨房とフロアの例はわかりやすかった。(書籍参照)

参考リンク