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

## 背景
[オブジェクト指向のこころ](https://www.amazon.co.jp/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%81%AE%E3%81%93%E3%81%93%E3%82%8D-SOFTWARE-PATTERNS-%E3%82%A2%E3%83%A9%E3%83%B3%E3%83%BB%E3%82%B7%E3%83%A3%E3%83%AD%E3%82%A6%E3%82%A7%E3%82%A4/dp/4621066048)10章「Bridgeパターン」を読んで
自分の中に落とし込むためのメモです。

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

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

## いつパターンを使用するのか
ある抽象的側面と様々な実装が共存している時

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

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

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

## 悲劇が起こる前

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/83ae6702-c174-55d4-3a3f-743bb91151b6.png)



四角形の描画に必要なプログラム（**DP1, DP2**）は、四角形の実体化時に決定するとし、
- **DP1** を用いて実体化する四角形(**V1Rectangle**)
- **DP2** を用いて実体化する四角形(**V2Rectangle**)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/b307615e-6c5b-844a-d5a9-bcd64727e004.png)



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

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


<details><summary>Rubyで書いてみるとこんな感じ</summary>

```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
```
</details>

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

```ruby
  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)
```


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

:::note info
追加要求:
- 円を描画できるように拡張したい
- **Rectangle** と **Circle** を区別なく扱いたい
:::

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

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



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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/dfef6694-29c5-d834-c6fb-a64f51a5c9cc.png)



抽象クラス`Circle`は共通のインターフェイス（`draw`）を持つだけ
```ruby
class Shape
  def draw(*args)
    raise NotImplementedError, "#{self.class} は抽象メソッド `draw` を実装していません。"
  end
end
```

`Circle`と`Rectangle`も`draw`に反応できるように拡張する

```ruby
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`も円を描画できるように機能を追加する
```ruby
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
```

<details>
<summary>全体を書き直してみるとこんな感じ</summary>

```ruby
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
```
</details>

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

```ruby
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/f292fdd5-cf48-04b2-33b6-7739986bde30.png)

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

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

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

**そして、これは図形だけではなく、描画プログラムの拡張が起きても同様に図形のパターンは膨れ上がっていく...**
</br>
どうしてこんなことが起こるのか
</br>
それは、**結合度の高さ**に起因する。
</br>

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

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

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

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


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

## パターンを導き出す
基本戦略
- **流動的要素を見つけ出し、カプセル化する**
- **継承ではなく、オブジェクトの集約を多様する**

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

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

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

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

つまり **「`Shape` が `Drawing` を使用する」という関係性を構築する**。

そうすると、クラス図は改めて以下のようになるはず
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/50a58b6a-d565-1050-8a06-b89025ae06af.png)

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

実行イメージは以下のような感じ
```ruby
v1_drawing = V1Drawing.new
# 実装を図形に喰わせる
rectangle_v1 = Rectangle.new(v1_drawing, 0, 0, 5, 5)
rectangle_v1.draw
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/d75d53e9-6ec3-fca5-3ba7-0899f417f518.png)
**Shape:**
実際には `Rectangle` や`Circle` などの図形だが、共通のインターフェイス(`draw`)を持っているため、利用する側からすると同じオブジェクトに見える。

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

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

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

<details>
<summary>全体を書き直してみるとこんな感じ</summary>

```ruby
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
```

実行できるか試してみる
```ruby
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)
```
</details>

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

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

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

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

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

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

## 参考
[オブジェクト指向のこころ](https://www.amazon.co.jp/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%81%AE%E3%81%93%E3%81%93%E3%82%8D-SOFTWARE-PATTERNS-%E3%82%A2%E3%83%A9%E3%83%B3%E3%83%BB%E3%82%B7%E3%83%A3%E3%83%AD%E3%82%A6%E3%82%A7%E3%82%A4/dp/4621066048)