21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ZealsAdvent Calendar 2019

Day 19

オブジェクト指向設計を実践するためのまとめ

Last updated at Posted at 2019-12-18

Zeals Advent Calendar 2019 の19日目の記事です。

サーバサイドエンジニアの小寺です。私がまだ初学者に近かった頃、「もっと早く知っておけばよかった!」と心の底から思ったオブジェクト指向設計についての知識を書き残しておこうと思います。

今回は、実務で求められる「保守性の高い読みやすいコード」を実践するために
オブジェクト指向設計実践ガイドを学習し、理解できたところを自分なりに要点をまとめ・メモし、備忘録としたものです。
(※4章までの内容が理解できれば十分実践していけると思いますので4章までしかまとめてないです)

参考書籍

オブジェクト指向設計実践ガイド
本書のサンプルコードを引用しています。

対象

  • オブジェクト指向についてふんわり理解している人
  • 責務がどうのこうのといったくだりをふんわり理解してる人
  • オブジェクト指向設計を理解してステップアップしたい初学者

2章 単一責任を意識する

  • なぜ単一責任が必要なのか
    • 再利用したい
    • 再利用できると未来の予期せぬ変更に対応できる
    • 悪いコードを増殖させたくない

単一責任の見極め方

  • 役割を聞く
# 「オブジェクト指向設計実践ガイド」 p41
class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire = tire
  end
  :
  def gear_inches
    ratio * (rim + (tire * 2))
  end
end
gear = Gear.new(52, 11, 26, 1.5)
gear.ratio
# ギアに対してギア比を聞くのは良い
gear.gear_inches
# ギアに対してgear_inchesを聞くのはしっくりこないような気がする
gear.tire
# ギアに対してタイヤのサイズを聞くのはもっとおかしい

インスタンス変数の隠蔽

# 「オブジェクト指向設計実践ガイド」 p46
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end
  
  def ratio
    @chainring / @cog.to_f  # <-- 破滅への道
  end
end

アクセサメソッド(attr_reader)で隠す

# 「オブジェクト指向設計実践ガイド」 p46
class Gear
  attr_reader :chainring, :cog # <--
  def initialize
  :
  def ratio
    chainring / cog.to_f  # <--
  end
end

実装的には以下と同じ

def cog
  @cog
end

実際に得ている恩恵としては未来に複雑な実装が来た時に変更が簡単

def cog
  if 条件
    @cog * 何かの計算
  else
    (@cog * 何かの計算) + 複雑な計算
  end
end

データ構造の隠蔽

# 「オブジェクト指向設計実践ガイド」 p48
class ObscuringReferences
  attr_reader :data
  def initialize(data)
    @data = data
  end
  
  def diameters
    # 0はリム, 1はタイヤ  <-- 何のデータがどこにあるか知っている状態
    # 配列の構造が変わった時などに怖い
    data.collect {|cell|
      cell[0] + (cell[1] * 2)
    }
  end
end

# 以下のデータが必要になる
@data = [[622, 20],[622, 23],...]

Structクラスを使って構造を包み隠す

# 「オブジェクト指向設計実践ガイド」 p50
class ObscuringReferences
  attr_reader :wheels
  def initialize(data)
    @wheels = wheelify(data)
  end
  
  def diameters
    # wheelが rim と tire を持ってることだけ知っていればいい
    wheels.collect {|wheel|
      wheel.rim + (wheel.tire * 2)
    }
  end
  # これで誰でも wheel に rim/tire を送れる
  
  Wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect {|cell|
      cell[0] + (cell[1] * 2)
    }
  end
end

メソッドから余計な責任を抽出する

メソッドもクラスと同じく単一の責任を持つべき(再利用が簡単になるので)
先ほどの diameters を見てみる

# 「オブジェクト指向設計実践ガイド」 p52
class ObscuringReferences
  :
  def diameters
    # wheelsを繰り返し処理するのと、それぞれの直径を計算する
    # といった2つの責任を持っている
    wheels.collect {|wheel|
      wheel.rim + (wheel.tire * 2)
    }
  end
  :
end

上記のメソッドを簡単に変更できるようにしていく

# 「オブジェクト指向設計実践ガイド」 p52
class ObscuringReferences
  :
  def diameters
    # 直径を計算する処理を別にした
    wheels.collect {|wheel| diamete(wheel) }
  end
  
  # diameter を呼べるようになった(他で再利用可能になった)
  def diameter(wheel)
    wheel.rim + (wheel.tire * 2)
  end
  :
end

上記はよくある責任が複数あるわかりやすい例。
大抵はこれほど明確ではない。先ほどのGearクラスを思い出してみる。

# 「オブジェクト指向設計実践ガイド」 p53
class Gear
  :
  # 何か不確定でのちにトラブルを起こしそうな気がする
  # このメソッドが2つ以上の責任を持ってしまっている
  def gear_inches
    ratio * rim + (tire * 2)
  end
  :
end

gear_inches に隠れている直径の計算を抽出してみる

# 「オブジェクト指向設計実践ガイド」 p53
class Gear
  :
  # Gearがgear_inchesの計算するのはよし
  def gear_inches
    ratio * diameter
  end
  
  # しかし、車輪の直径の計算までするのはおかしい
  def diameter
    rim + (tire * 2)
  end
  :
end

といったように、小さなリファクタリングではあるが Gear が持つべきでない責任が明らかになりました。

このように、あらゆるものを単一責任にしていくことで

  • 隠蔽された性質を明らかにする
    • 上記の例だと 「Gear は diameterを持つべきではない」
  • コメントをする必要がない
    • メソッドがコメントの役割を果たす
  • 再利用を促進する
    • 他のプログラマーは複製ではなく再利用をするようになる
  • 他のクラスへの移動が簡単
    • 小さなメソッドは簡単に動かすことができる

といった恩恵が受けれます

クラス内の余計な責任を隔離する

一旦全てを単一責任するとクラスのスコープが明白になってきます。
GearクラスにはいくつかのWheel(車輪)のような振る舞いが隠れていました。
ここで、新しくクラスを作ってもいいのですがそうなると変更のコストが大きいかもしれないのでコストを最小限にしつつGearの単一責任を保つようにします。

# 「オブジェクト指向設計実践ガイド」 p55
class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end
  
  def ratio
    chainring / cog.to_f
  end
  
  def gear_inches
    ratio * wheel.diameter
  end
  
  # 隠れていた Wheel のような振る舞いを手に入れた
  # 書籍ではこの後追加要望があったタイミングで新しいクラスを定義する
  # 「あとで」決定できる力を取っておくことはアプリケーション開発において重要
  Wheel = Struct.new(:rim, :tire) do
    def diameter
      rim + (tire * 2)
    end
  end
end

本書では、新しくアプリケーションに車輪に関する追加機能の要望が来たタイミングで Wheel クラスを新しく定義するよう書かれています。
アプリケーション開発において未来を予測することは困難なのでできる限り「あとで」決定できる力を残しておくのは非常に重要なことである

3章 依存関係を管理する

依存関係を認識する

オブジェクトが次のものを知っている時、オブジェクト間には依存がある

  • 他のクラスの名前を知っている
    • GearはWheelというクラスが存在すると予想している
  • self以外のメッセージの名前
    • GearはWheelがdiameterに応答できると知っている
  • メッセージが要求する引数
    • Gearは Wheel.newするときに rim とtireが必要なことを知っている
  • 引数の順番
    • 最初が rim で 2番目が tire だと知っている
# 「オブジェクト指向設計実践ガイド」 p60
class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    # Wheel.newするときに rim とtireが必要なことを知っている
    @rim = rim
    @tire = tire
  end
  
  def ratio
    chainring / cog.to_f
  end
  
  def gear_inches
    # Wheelというクラスが存在すると予想している
    # Wheelがdiameterに応答できると知っている
    # 最初が rim で 2番目が tire だと知っている
    ratio * Wheel.new(rim, tire).diameter
  end
  :
end

class Wheel
  attr_reader :rim, :tire
  def initialize(rim, tire)
    @rim = rim
    @tire = tire
  end
  
  def diameter
    rim + (tire * 2)
  end
  :
end

依存が発生している時、依存が強固になりすぎると2つのオブジェクトは変更することが難しくなってくるので2つのオブジェクトがあたかも1つのオブジェクトとして振る舞うようになってしまう。

疎結合なコードを書く

依存を減らすための技法を紹介する

依存オブジェクトの注入

クラス内でインスタンスを生成するのではなく diameterを知っているオブジェクトを受け取るようにしておく

# 「オブジェクト指向設計実践ガイド」 p66
class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, wheel)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end
  
  def ratio
    chainring / cog.to_f
  end
  
  def gear_inches
    ratio * wheel.diameter
  end
  :
end

# Gearはdiameterを知っているオブジェクトを要求する
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

依存を隔離する

  • インスタンス変数の作成の分離
    • 制約が厳しく、オブジェクトを注入できないような場合
    • クラス内で分離するしか方法がないとき
# 「オブジェクト指向設計実践ガイド」 p68
class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    # Gearはまだ知りすぎているが、gear_inchesの依存が減っている
    @rim = rim
    @tire = tire
  end
  :
  def gear_inches
    ratio * wheel.diameter
  end
  
  # wheel が呼ばれるまでインスタンスは作成されない
  def wheel
    @wheel || Wheel.new(rim, tire)
  end
end
  • 脆い外部メッセージを隔離する
    • 外部への参照がクラスに埋め込まれていて、どれが変更されやすい時
# 「オブジェクト指向設計実践ガイド」 p70
def gear_inches
  # 恐ろしい計算
  foo = some_intermedicate_result * wheel.diameter
  # 恐ろしい計算
end

上記のように複雑な状況になった時にメソッドの中に依存が隠れてしまう。

そうした場合以下のように脆い部分を隔離する

# 「オブジェクト指向設計実践ガイド」 p71
def gear_inches
  # 恐ろしい計算
  foo = some_intermedicate_result * diameter
  # 恐ろしい計算
end

def diameter
  # もし、Wheelがdiameterに変更加えた場合副作用はここだけになる
  wheel.diameter
end

引数の順番への依存を取り除く

  • 初期化の際にハッシュ使う
# 「オブジェクト指向設計実践ガイド」 p73
class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(args)
    @chainring = args[:chainring]
    @cog       = args[:cog]
    @wheel     = args[:wheel]
  end
  :
end

# 冗長にはなったが、引数の順番を知っていなくても大丈夫になった
# ハッシュのkey名に依存はしているが、順番よりは健康的
Gear.new(
  chanring: 52,
  cog:      11,
  wheel:    Wheel.new(26, 1.5)
).gear_inches

安定性の高い引数をいくつか受け取って、安定性の低い引数はオプション引数で受け取るといった手法が多く用いられる

  • 明示的にデフォルト値を設定する
# 「オブジェクト指向設計実践ガイド」 p74
def initialize(args)
  # 真偽値以外の場合はシンプルに || を使える
  @chainring = args[:chainring] || 40
  @cog       = args[:cog] || 18
  @wheel     = args[:wheel]
end

# 真偽値を使う or nilを設定する場合
def initialize(args)
  @chainring = args.fetch(:chainring, 40)
  @cog       = args.fetch(:cog, 40)
  @wheel     = args[:wheel]
end
  • 複数のパラメーターを用いた初期化を隔離する
    • もし、引数を簡単に変更できないような状況下にあった場合ラッパークラスを定義して包み隠すようにする
# 「オブジェクト指向設計実践ガイド」 p77

# Gearがもし外部インターフェースの一部の場合(このクラスを変更できない場合)
module SomeFramework
  class Gear
    attr_reader :chainring, :cog, :wheel
    def initialize(args)
      @chainring = args[:chainring]
      @cog       = args[:cog]
      @wheel     = args[:wheel]
    end
    :
  end
end

# 外部のインタフェースをラップし、自信を変更から守っている
# 他のオブジェクトを生成することが目的のオブジェクトはファクトリーと呼ぶ
module GearWrapper
  def self.gear(args)
    SomeFramework::Gear.new(
      args[:chainring],
      args[:cog],
      args[:wheel]
    )
  end
end

GearWrapper.new(
  chanring: 52,
  cog:      11,
  wheel:    Wheel.new(26, 1.5)
).gear_inches

このテクニックは自分で変更がきかない外部のインターフェースに依存する場合に適している。

依存方向の管理

「自分より変更が少ないものに依存すべし」

4章 柔軟なインターフェースを作る

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

クラスのパブリックインターフェースを作り上げるメソッドは以下のような特性を備えている

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

パブリックインターフェースを見つける

「アプリケーション例: 自転車旅行会社」
ユースケースとして
「参加者は適切な難易度の、特定の日付の、自転車を借りられる旅行の一覧をみたい」

見当をつけるためにシーケンス図を使う

オブジェクト間でやり取りされるメッセージを気軽に実験することができる

SS 404.png

ここで起きる疑問
「Tripが利用可能な自転車まで調べなくてもいいんじゃないか?」

シーケンス図を描くことによって
「このオブジェクトが〇〇という責任を負うべきなのだろうか?」と疑問が湧くようになる

ある旅行に対して自転車が利用可能かどうかTripクラスが見つけ出すべきでない場合、
Bycycleクラスがありそう。

SS 405.png

  • Tripはsuiable_tripsに責任があり
  • Bycycleはsuitable_bycycle に責任を持つ

変更後は Tripから余計な責任は取り除けたものの Customer に移しただけ
変更後は Customer が何を望むのかと他のオブジェクトがどのようにそれを準備するのかまで知ってしまっている

「どのように」を伝えるのではなく「何を」を頼む

新たなユースケース
「旅行が開始されるためには使われる自転車が全て整備されていることを確実にする」

SS 406.png

TripはMechanicがどのように整備するのか(
clean_bicycleして
pump_tiresして
lube_chainして
check_brakesするという手順
)を知ってしまっている。

Mechanicが新たな整備手順を増やした時は、Tripも変更しなければならない

以下は対案

SS 407.png

Tripにあった責任をほとんどMechanicに渡している
自転車の準備することに関して「どのように」はMechanicの責任になった
TripはMechanicにどんな改善があろうともprepare_bicycleから正しい振る舞いを得ることができる

上記のようにMechanic と Trip の会話が 「どのように」から「何を」に変わった副作用として
Mechanicのパブリックインターフェースのサイズが小さくなった

パブリックインターフェースが小さいということは他のところから依存されるメソッドがわずかしかないことを意味している

コンテキストの独立を模索

旅行の準備には「いつでも」自転車の準備が求められるためTripは「常に」prepare_bicycleメッセージを自身のMechanicへ送らなければならない

SS 408.png

Tripが旅行が準備されることをMechanicに伝え、Mechanicは準備にbicyclesが必要なのでTripにコールバックをし、自転車の整備を行います。

こうして、整備士がどのように自転車を準備するかはMechanicクラスに隔離されました。

オブジェクトを見つけるためにメッセージを使う

SS 405.png

Tripが知りすぎていたのを改善したが、今度はCustomerが知りすぎている。
上記のアプリケーションは要件を満たす新たなオブジェクトを必要としていることがなんとなく見えてきた。

SS 409.png

新たなTripFinderは安定したパブリックインターフェースを提供し、複雑な内部に関しては隠している

シーケンス図便利ー

まとめ

単一責任を意識しながらコードを書けるようになりましょう

  • そのために常にメソッドの役割をメソッド自身に問い続ける
  • 抽出できるものはできる限り抽出しておくと未来に変更が簡単になる

クラスの中で依存関係があることを認識する

  • 状況に応じて疎結合にするための技法を使い分ける

オブジェクト間で交わされるメッセージを中心にアプリケーションを設計する

  • 議論を進める時にはシーケンス図が有効
  • どのようにではなくオブジェクトが「何を」要求するかに注目する

おわり

  • 書籍の中に重要な説明がたくさんあるので本読みましょう(オブジェクト指向設計実践ガイド
    • 今回はあくまでコードベースで要点まとめただけなので
  • 柔軟に進化し続けましょう

明日は20日目の @neuneu39 の番です。お楽しみに!!!

21
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?