LoginSignup
7
2

【Ruby】デザインパターン学習メモ Bridge

Posted at

背景

オブジェクト指向のこころ10章「Bridgeパターン」を読んで
自分の中に落とし込むためのメモです。

本書ではJavaで書かれていますが実務ではRubyを使用しているため
Rubyに書き直しています。

このパターンの目的

機能と実装を切り出すことで、クラス数の爆発を招かず、それぞれを拡張可能にする

いつパターンを使用するのか

ある抽象的側面と様々な実装が共存している時

この記事の流れ

「オブジェクト指向のこころ」では図形を描画する一連のプログラムを題材にしています。

初期実装機能追加による弊害パターンを当てはめる
の順番で説明していきます。

説明が抽象的で意味不明 😵‍💫 だと思うので、
実際のコードを見た方が早いです。

悲劇が起こる前

要求:
二つの描画プログラム(DP1, DP2)を使用して、四角形を描画する
※ DP: Drawing Program の略

※ 四角形は2組の座標があれば定義できる

image.png

四角形の描画に必要なプログラム(DP1, DP2)は、四角形の実体化時に決定するとし、

  • DP1 を用いて実体化する四角形(V1Rectangle)
  • DP2 を用いて実体化する四角形(V2Rectangle)

の2つのクラスを用意できる。

すると、以下のようなクラス図になる

image.png

二つの四角形はdraw_line メソッドの実装方法のみが異なる。そのため、Rectangle という抽象クラスから各クラスを派生させることで抽象化クラスによるカプセル化のメリットを活かせるようになる。

つまり、draw_rectangleが共通のインターフェイスになっているので、
長方形を作るクライアント側は長方形の型(V1, V2)を意識しなくて済む。

Rubyで書いてみるとこんな感じ
# 抽象クラス Rectangle
  class Rectangle
    # 描画メソッド
    def draw_rectangle(x, y, width, height)
      draw_line(x, y, x + width, y)
      draw_line(x + width, y, x + width, y + height)
      draw_line(x + width, y + height, x, y + height)
      draw_line(x, y + height, x, y)
    end

    private

    # 抽象メソッド draw_line
    def draw_line(x1, y1, x2, y2)
      raise NotImplementedError, "#{self.class} は抽象メソッド `draw_line` を実装していません。"
    end
  end

  class V1Rectangle < Rectangle
    def initialize
      @dp1 = DP1
    end

    private

    def draw_line(x1, y1, x2, y2)
      @dp1.draw_a_line(x1, y1, x2, y2)
    end
  end

  class V2Rectangle < Rectangle
    def initialize
      @dp2 = DP2
    end

    private

    def draw_line(x1, y1, x2, y2)
      @dp2.draw_a_line(x1, y1, x2, y2)
    end
  end

  class DP1
    def self.draw_a_line(x1, y1, x2, y2)
      puts "DP1を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
    end
  end

  class DP2
    def self.draw_a_line(x1, y1, x2, y2)
      puts "DP2を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
    end
  end

準備ができたので実行してみる

  puts "長方形を描きます (DP1):"
  rect1 = V1Rectangle.new
  rect1.draw_rectangle(0, 0, 5, 5)
  # => DP1を使って線を描きます。(0, 0) - (5, 0)
  # => DP1を使って線を描きます。(5, 0) - (5, 5)
  # => DP1を使って線を描きます。(5, 5) - (0, 5)
  # => DP1を使って線を描きます。(0, 5) - (0, 0)
  
  puts "\n長方形を描きます (DP2):"
  rect2 = V2Rectangle.new
  rect2.draw_rectangle(0, 0, 5, 5)
  # => DP2を使って線を描きます。(0, 0) - (5, 0)
  # => DP2を使って線を描きます。(5, 0) - (5, 5)
  # => DP2を使って線を描きます。(5, 5) - (0, 5)
  # => DP2を使って線を描きます。(0, 5) - (0, 0)

機能拡張 「円を描画する」 と クラス爆発

追加要求:

  • 円を描画できるように拡張したい
  • RectangleCircle を区別なく扱いたい

この場合、抽象クラスでShape を導入し、そこからCircleRectangleを派生する。
そうすると、Shape.drawと共通のインターフェイスを作ることができる。

また、Circleも同様にDP1, DP2を使い分けて円を描画する。

すると、クラス図は以下のようになる

image.png

抽象クラスCircleは共通のインターフェイス(draw)を持つだけ

class Shape
  def draw(*args)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw` を実装していません。"
  end
end

CircleRectangledrawに反応できるように拡張する

class Rectangle < Shape
  def draw(x, y, width, height)
    draw_rectangle(x, y, width, height)
  end

  private

  def draw_rectangle(x, y, width, height)
    draw_line(x, y, x + width, y)
    draw_line(x + width, y, x + width, y + height)
    draw_line(x + width, y + height, x, y + height)
    draw_line(x, y + height, x, y)
  end

  def draw_line(x1, y1, x2, y2)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw_line` を実装していません。"
  end
end

DPも円を描画できるように機能を追加する

class DP1
  def self.draw_a_line(x1, y1, x2, y2)
    puts "DP1を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
  end

  def self.draw_a_circle(x, y, radius)
    puts "DP1を使って円を描きます。(#{x}, #{y}) 半径 #{radius}"
  end
end
全体を書き直してみるとこんな感じ
class Shape
  def draw(*args)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw` を実装していません。"
  end
end

class Rectangle < Shape
  def draw(x, y, width, height)
    draw_rectangle(x, y, width, height)
  end

  private

  def draw_rectangle(x, y, width, height)
    draw_line(x, y, x + width, y)
    draw_line(x + width, y, x + width, y + height)
    draw_line(x + width, y + height, x, y + height)
    draw_line(x, y + height, x, y)
  end

  def draw_line(x1, y1, x2, y2)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw_line` を実装していません。"
  end
end

class V1Rectangle < Rectangle
  def initialize
    @dp1 = DP1
  end

  private

  def draw_line(x1, y1, x2, y2)
    @dp1.draw_a_line(x1, y1, x2, y2)
  end
end

class V2Rectangle < Rectangle
  def initialize
    @dp2 = DP2
  end

  private

  def draw_line(x1, y1, x2, y2)
    @dp2.draw_a_line(x1, y1, x2, y2)
  end
end

class Circle < Shape
  def draw(x, y, radius)
    draw_circle(x, y, radius)
  end

  private

  def draw_circle(x, y, radius)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw_circle` を実装していません。"
  end
end

class V1Circle < Circle
  def initialize
    @dp = DP1
  end

  private

  def draw_circle(x, y, radius)
    @dp.draw_a_circle(x, y, radius)
  end
end

class V2Circle < Circle
  def initialize
    @dp = DP2
  end

  private

  def draw_circle(x, y, radius)
    @dp.draw_a_circle(x, y, radius)
  end
end

class DP1
  def self.draw_a_line(x1, y1, x2, y2)
    puts "DP1を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
  end

  def self.draw_a_circle(x, y, radius)
    puts "DP1を使って円を描きます。(#{x}, #{y}) 半径 #{radius}"
  end
end

class DP2
  def self.draw_a_line(x1, y1, x2, y2)
    puts "DP2を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
  end

  def self.draw_a_circle(x, y, radius)
    puts "DP2を使って円を描きます。(#{x}, #{y}) 半径 #{radius}"
  end
end

準備ができたので実行してみる

puts "\n円を描きます (DP1):"
circle1 = V1Circle.new
circle1.draw(0, 0, 7)
# => 円を描きます (DP1):
# => DP1を使って円を描きます。(0, 0) 半径 7

puts "\n円を描きます (DP2):"
circle1 = V2Circle.new
circle1.draw(0, 0, 8)
# => 円を描きます (DP2):
# => DP2を使って円を描きます。(0, 0) 半径 8

様々なクラスがやり取りを行なって複雑に見えるが
やり取りをしているのは以下のクラスだけ。

image.png

しかし、上記の方法で拡張すると、とある問題が浮上する。

そう、 図形クラス数の爆発である

最初は長方形のみだったので、図形のパターンもV1RectangleV2Rectangleの2種類だったが、円が追加されたことで4種類に増加した。
あまり考えたくないが、楕円Ellipseも描画したいという要求が増えたのなら6種類にも膨れ上がる。

そして、これは図形だけではなく、描画プログラムの拡張が起きても同様に図形のパターンは膨れ上がっていく...

どうしてこんなことが起こるのか

それは、結合度の高さに起因する。

Shape と DP の蜜月

この問題が起きる原因は
抽象的側面(Shape)と実装(DP)が密結合になっているから

例えば、V1Circle などの図形は自分がどの描画プログラムの型(DP1)になるかを知っていないといけない。この問題を解決するためには、 抽象面における流動的要素、実装面における流動的要素をそれぞれ抽出しなければならない

本例で言うなら、図形では長方形・円、描画プログラムではDP1・DP2の流動的要素を別々で切り出して、それぞれを独立して変更できるようにしたい。

これが冒頭であった
「機能と実装を切り出すことでそれぞれを拡張可能にする」 ということ

加えて、現状では継承を使いまくっているのが非常に嫌
継承は便利だが、コードを追うのが非常に厄介になるので減らしていきたい。

パターンを導き出す

基本戦略

  • 流動的要素を見つけ出し、カプセル化する
  • 継承ではなく、オブジェクトの集約を多様する

この例では様々な図形様々な描画プログラムを持つので、流動的要素は 図形と描画プログラムということになる。

図形に関しては、Shape内にカプセル化する。
Shapeには自らの描画方法を知っておく責務を持たせたい。

描画方法についてはDP1DP2が存在する。
そこで、これらの抽象概念であるDrawingを作成し、その中にカプセル化する。

そして、Shape にはこの Drawing を保持させる。

つまり ShapeDrawing を使用する」という関係性を構築する

そうすると、クラス図は改めて以下のようになるはず
image.png

実装をオブジェクトの外に追い出して、オブジェクトから使用されるものにすることで、呼び出し側(Shape)から実装における流動的な要素を隠蔽することができる。

実行イメージは以下のような感じ

v1_drawing = V1Drawing.new
# 実装を図形に喰わせる
rectangle_v1 = Rectangle.new(v1_drawing, 0, 0, 5, 5)
rectangle_v1.draw

クラス数は多く見えるがやりとりしているオブジェクトは以下のみ

image.png
Shape:
実際には RectangleCircle などの図形だが、共通のインターフェイス(draw)を持っているため、利用する側からすると同じオブジェクトに見える。

Drawing:
実際には V1Drawing などの描画プログラムだが、共通のインターフェイスを持っているため、Shapeからすると同じオブジェクトに見える。

DP:
適切なオブジェクトでなければならない。これを利用する Drawing オブジェクトはDPの型を知っている。

さて、これを踏まえてコードを書き直すと以下のような設計になる。

全体を書き直してみるとこんな感じ
class Shape
  def initialize(drawing)
    @drawing = drawing
  end

  def draw_line(x1, y1, x2, y2)
    @drawing.draw_line(x1, y1, x2, y2)
  end

  def draw_circle(x, y, radius)
    @drawing.draw_circle(x, y, radius)
  end

  def draw
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw` を実装していません。"
  end
end

# 抽象クラス Rectangle
class Rectangle < Shape

  def initialize(dp, x, y, width, height)
    super(dp)
    @x = x
    @y = y
    @width = width
    @height = height
  end

  def draw
    draw_line(@x, @y, @x + @width, @y)
    draw_line(@x + @width, @y, @x + @width, @y + @height)
    draw_line(@x + @width, @y + @height, @x, @y + @height)
    draw_line(@x, @y + @height, @x, @y)
  end
end


# 抽象クラス Rectangle
class Circle < Shape

  def initialize(dp, x, y, radius)
    super(dp)
    @x = x
    @y = y
    @radius = radius
  end

  def draw
    draw_circle(@x, @y, @radius)
  end
end

class Drawing
  def draw_line(x1, y1, x2, y2)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw_line` を実装していません。"
  end

  def draw_circle(x, y, radius)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw_circle` を実装していません。"
  end
end

class V1Drawing < Drawing
  def initialize
    @dp = DP1.new
  end

  def draw_line(x1, y1, x2, y2)
    @dp.draw_a_line(x1, y1, x2, y2)
  end

  def draw_circle(x, y, radius)
    @dp.draw_a_circle(x, y, radius)
  end
end

class V2Drawing < Drawing
  def initialize
    @dp = DP2.new
  end

  def draw_line(x1, y1, x2, y2)
    @dp.draw_a_line(x1, y1, x2, y2)
  end

  def draw_circle(x, y, radius)
    @dp.draw_a_circle(x, y, radius)
  end
end

class DP1
  def draw_a_line(x1, y1, x2, y2)
    puts "DP1を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
  end

  def draw_a_circle(x, y, radius)
    puts "DP1を使って円を描きます。座標(#{x}, #{y}) 半径 #{radius}"
  end
end

class DP2
  def draw_a_line(x1, y1, x2, y2)
    puts "DP2を使って線を描きます。(#{x1}, #{y1}) - (#{x2}, #{y2})"
  end

  def draw_a_circle(x, y, radius)
    puts "DP2を使って円を描きます。座標(#{x}, #{y}) 半径 #{radius}"
  end
end

実行できるか試してみる

v1_drawing = V1Drawing.new
v2_drawing = V2Drawing.new

rectangle_v1 = Rectangle.new(v1_drawing, 0, 0, 5, 5)
rectangle_v1.draw
# => DP1を使って線を描きます。(0, 0) - (5, 0)
# => DP1を使って線を描きます。(5, 0) - (5, 5)
# => DP1を使って線を描きます。(5, 5) - (0, 5)
# => DP1を使って線を描きます。(0, 5) - (0, 0)

circle_v1 = Circle.new(v1_drawing, 0, 0, 5)
circle_v1.draw
# => DP1を使って円を描きます。座標(0, 0) 半径 5

rectangle_v2 = Rectangle.new(v2_drawing, 0, 0, 5, 5)
rectangle_v2.draw
# =>DP2を使って線を描きます。(0, 0) - (5, 0)
# =>DP2を使って線を描きます。(5, 0) - (5, 5)
# =>DP2を使って線を描きます。(5, 5) - (0, 5)
# =>DP2を使って線を描きます。(0, 5) - (0, 0)

circle_v2 = Circle.new(v2_drawing, 0, 0, 5)

補足

「オブジェクト指向のこころ」では、デザインパターンが提供する解決策を重視しがちになることに警鐘を鳴らしています。解決策を適用する前に、まず問題そのものを理解することが大切であるとも書かれています。

p.155

問題そのものを理解することなしに、問題領域の中からパターンが適用できそうな部分を探し出すというアプローチをとってしまうと、何をするかは検討がつくものの、いつそれを使うのかなぜそれを使うのかといったことが分からないままになってしまうわけです。

パターンを使うことが目的にならないように、解決しようとする問題に着目することが大事だとわかりますね..

最後に

ここまで見ていただきありがとうございます m(_ _)m

デザパタでは

  • インターフェイスを用いて設計する
  • クラス継承よりもオブジェクトの集約を多用する
  • 流動的要素を見つけ出し、カプセル化する

などのオブジェクト指向設計を行う上で必要な知識が採用されています。
デザパタを学ぶ中でその有用性を感じ取り、設計を磨いていきたいと思います。

参考

オブジェクト指向のこころ

7
2
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
7
2