8章
コンポジションとは
組み合わせた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為
組み合わせることで、何らかの意味を持つ。
関係性が
is a なら継承(分類的派生)
has a ならコンポジション(パーツにばらす)
Partsクラスにコンポーズする
スタートはこれ
Bicycle独自の「パーツ」を別クラスに隔離し、注入するようにする。sizeは自転車のサイズなのでそのまま残す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
class Bicycle
attr_reader :size, :parts
def initialize(args)
@size = args[:size]
@parts = args[:parts]
end
def spares
parts.spares
end
end
class Parts # Bicycleからごっそり抜いただけ
attr_reader :chain, :tire_size
def initialize(args={})
@chain = args[:chain]
@tire_size = args[:tire_size]
post_initialize(args)
end
def spares
{
tire_size: tire_size,
chain: chain
}.merge(local_spares)
end
def default_tire_size
raise NotImplementedError
end
def post_initialize(args)
nil
end
def local_spares
{}
end
def default_chain
'10-speed'
end
end
# BicycleではなくPartsがそれぞれ独自のクラスを持つようになった(BicycleはRoadもMountainも共通)
class RoadBikeParts < Parts
# 以前のRoadBikeと同じ
end
class MountainBikeParts < Parts
# 以前のMountainBikeと同じ
end
|
1
2
3
4
5
6
7
8
9
10
11
|
road_bike =
Bicycle.new(
size: 'L',
parts: RoadBikeParts.new(tape_color: 'red')
)
mountain_bike =
Bicycle.new(
size: 'M',
parts: MountainBikeParts.new(rear_shock: 'Fox')
)
|
こんな感じになる。
PartsクラスをさらにPartの集合とする
Partsオブジェクトが複数のPartを保持するという前提。
road_bike.spares
が呼ばれたときこんな感じにしたい。
Bicycle→Parts「spares
をくださーい」
Parts(了解。でも、どの部品が必要なのかわからないな)←ここを単一責任にできるのがGood
Parts→Part「あなたはsparesを作るのに必要かどうか教えてくださーい」
各Part→Parts「私は必要ですよー / 不要ですよー」
Parts(よし、じゃあ必要なものだけでsparesを組み立てよう)
Parts→Bicycle「ほい、これがspares
ですよー」
Bicycle「よしよし、じゃあこれをまるっと返せばいいな」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
class Bicycle # Bicycleクラスの中身は変わっていない
attr_reader :size, :parts
def initialize(args)
@size = args[:size]
@parts = args[:parts]
end
def spares
parts.spares
end
end
class Parts # Partの集合を束ねるだけのクラスになった
attr_reader :parts
def initialize(parts)
@parts = parts
end
def spares
parts.select { |part| part.needs_spare }
end
end
class Part
attr_reader :name, :description, :needs_spare
def initialize(args)
@name = args[:name]
@description = args[:description] # 感覚的にはdescriptionよりvalueとかのほうがしっくりくる
@needs_spare = args.fetch(:needs_spare, true)
end
end
|
各部品を定義する
1
2
3
4
5
6
|
chain = Part.new(name: 'chain', description: '10-speed')
road_tire = Part.new(name: 'tire_size', description: '23')
tape = Part.new(name: 'tape_color', description: 'red')
mountain_tire = Part.new(name: 'tire_size', description: '2.1')
rear_shock = Part.new(name: 'rear_shock', description: 'Fox')
front_shock = Part.new(name: 'front_shock', description: 'Manitou', needs_spare: false)
|
Bicycleを定義する
1
2
3
4
5
6
|
road_bike = Bicycle.new(size: 'L', parts: Parts.new([chain, road_tire, tape]))
road_bike.spares
=> [#<Part:0x00007feaac0f6358 @description="10-speed", @name="chain", @needs_spare=true>,
#<Part:0x00007feaa8f00948 @description="23", @name="tire_size", @needs_spare=true>,
#<Part:0x00007feaad8652a0 @description="red", @name="tape_color", @needs_spare=true>]
|
変化ポイント:
spares
がオブジェクトではなく配列を返すようになった。
この配列に入るオブジェクトの条件:
name
, description
, needs_spare
に応答できることだけ
(ここではPart
のインスタンスだけど、そうである必要はない。ダックタイプ的な感じで考える)
ところで、Partsって具体的に何なの?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
[50] pry(main)> road_bike.spares
=> [#<Part:0x00007feaac0f6358 @description="10-speed", @name="chain", @needs_spare=true>,
#<Part:0x00007feaa8f00948 @description="23", @name="tire_size", @needs_spare=true>,
#<Part:0x00007feaad8652a0 @description="red", @name="tape_color", @needs_spare=true>]
[47] pry(main)> road_bike.spares.size
=> 3
[49] pry(main)> road_bike.parts
=> #<Parts:0x00007feaa8eb9570
@parts=
[#<Part:0x00007feaac0f6358 @description="10-speed", @name="chain", @needs_spare=true>,
#<Part:0x00007feaa8f00948 @description="23", @name="tire_size", @needs_spare=true>,
#<Part:0x00007feaad8652a0 @description="red", @name="tape_color", @needs_spare=true>]>
[48] pry(main)> road_bike.parts.size
NoMethodError: undefined method `size' for #<Parts:0x00007feaa8eb9570>
from (pry):133:in `__pry__'
end
|
road_bike.spares
はArrayだけど、road_bike.parts
はPartsオブジェクト!!!(size
への応え方を知らない)
Parts
は配列的な感じになることが決まっているんだから、Arrayと同じように扱いたいよなあ。
①メソッドを生やす
1
2
3
4
5
6
|
class Parts
# 省略
def size
parts.size
end
end
|
→いやゴリ押しが過ぎるだろう。
それ、他にも使いたいメソッドが増えたら同じように付け足すのかよ。
②Arrayを継承させる
1
2
3
4
5
|
class Parts < Array
def spares
select { |part| part.needs_spare }
end
end
|
これで、Array
にあるメソッドは全部使えるよ!やったねたえちゃん!
→メソッドで弄り回していると、そのオブジェクトのクラスがArray
になってしまって、Parts
っぽい振る舞いをするくせにParts
のメソッドが使えなくなってしまう……。
本当はメソッドでいじった後もParts
クラスになってほしいのに。
③Enumerableをincludeする
繰り返しを行なうクラスのための Mix-in。このモジュールのメソッドは全て each を用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。
module Enumerable (Ruby 2.7.0 リファレンスマニュアル)
eachでできることが大抵できるモジュール。
そのものを別のオブジェクトに変化させることはない。
Iteratorパターンというらしい。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
require 'forwardable'
class Parts
extend Forwardable
def_delegators :@parts, :size, :each # @partsオブジェクトから:size, :eachを委譲する
# Enumerableを使うにはeachに応答できることが条件なので
include Enumerable
def initialize(parts)
@parts = parts
end
def spares
select { |part| part.needs_spare }
end
end
|
module Forwardable (Ruby 2.7.0 リファレンスマニュアル)
Forwardable#def_delegators (Ruby 2.7.0 リファレンスマニュアル)
1
|
mountain_bike = Bicycle.new(size: 'L', parts: Parts.new([chain, front_shock, rear_shock]))
|
Partsを製造する
各部品の定義の仕方、すごい雑然としていない??
例えば、
どんな風にでも作れてしまうので、Part
を作るための規範を作りましょう。
このクラスの唯一の目的が、ほかのクラスのインスタンスの作成であることです。オブジェクト指向設計では、このようなオブジェクトに「ファクトリー」という名前をつけています。
p.78
というわけで、3章と同じようにファクトリーとするmoduleを作る。
1
2
3
4
5
6
7
8
9
10
|
module PartsFactory
def self.build(config, part_class = Part, parts_class = Parts)
# Part, Partsはメソッド内に直書きすることもできるけど、結合を疎にするためにこっちで定義している
parts_class.new(
config.collect { |part_config|
part_class.new(name: part_config[0], description: part_config[1], needs_spare: part_config.fetch(2, true))
}
)
end
end
|
Part用のデータはこんな感じでまとめて定義しておけば、PartsFactory.build
の引数として流し込むだけでいい。
(順番に依存しているのはいいのか……??)
【はてなポイント】これってどこで定義するの?ベタ書きだったら同じじゃない?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
road_config = [
['chain', '10-speed'],
['tire_size', '23'],
['tape_color', 'red']
]
mountain_config = [
['chain', '10-speed'],
['tire_size', '2.1'],
['front_shock', 'Manitou', false],
['rear_shock', 'Fox']
]
[10] pry(main)> road_parts = PartsFactory.build(road_config)
=> #<Parts:0x00007fe4a6de7000
@parts=
[#<Part:0x00007fe4a6de70c8 @description="10-speed", @name="chain", @needs_spare=true>,
#<Part:0x00007fe4a6de7078 @description="23", @name="tire_size", @needs_spare=true>,
#<Part:0x00007fe4a6de7028 @description="red", @name="tape_color", @needs_spare=true>]>
|
ところでですけど、Partって大した要素をクラスとして持たないのでは?作ったら終わりだし、ただのパーツだし……。
ということで、Part
は解雇です。代わりに派遣のOpenStruct
さんをお呼びしました。
StructとOpenStructの違い
Struct
初期化時に順番を指定して引数を渡す。
1
2
3
4
|
[1] pry(main)> dog = Struct.new("Dog", :name, :age)
=> Struct::Dog # ここでまずクラスを作る
[2] pry(main)> fred = dog.new("fred", 5)
=> #<struct Struct::Dog name="fred", age=5>
|
dog = Struct.new("Dog", :name, :age)
で作成したクラスは下記と同義。
1
2
3
4
5
6
7
8
|
class Dog
attr_accessor :name, :age
def initialize(name, age) # だからハッシュで渡せない
@name = name
@age = age
end
end
|
OpenStruct
初期化時にハッシュを撮って属性を引き出す。
ハッシュまたはsetterで値を渡す。
1
2
3
4
5
6
7
8
9
10
11
|
[1] pry(main)> hoge = OpenStruct.new
=> #<OpenStruct>
[2] pry(main)> hoge.foo = 'foo!' # setterは定義していないけど渡せる。渡せば受け取ってくれる。
=> "foo!"
[3] pry(main)> hoge.foo
=> "foo!"
[5] pry(main)> huga = OpenStruct.new(foo: 'foo?')
=> #<OpenStruct foo="foo?">
[6] pry(main)> huga.foo
=> "foo?"
|
class Struct (Ruby 2.7.0 リファレンスマニュアル)
class OpenStruct (Ruby 2.7.0 リファレンスマニュアル)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
require 'ostruct'
module PartsFactory
def self.build(config, parts_class = Parts)
parts_class.new(
config.collect { |part_config|
create_part(part_config) # 切り出し
}
)
end
def self.create_part(part_config) # self.buildと同階層なので、こっちもselfを付ける
# Partの代わりにOpenStructになった
OpenStruct.new(name: part_config[0], description: part_config[1], needs_spare: part_config.fetch(2, true))
end
end
|
1
2
3
4
5
6
|
[14] pry(main)> road_parts = PartsFactory.build(road_config)
=> #<Parts:0x00007fe4aab5cbd8
@parts=
[#<OpenStruct name="chain", description="10-speed", needs_spare=true>,
#<OpenStruct name="tire_size", description="23", needs_spare=true>,
#<OpenStruct name="tape_color", description="red", needs_spare=true>]>
|
Partという名前はなくなったけど、ロールとしてはPart
と同等の働きをするので問題なし。
ファクトリメソッド | Ruby デザインパターン | 酒と涙とRubyとRailsと
最終的な形
まとめただけ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
class Bicycle
attr_reader :size, :parts
def initialize(args)
@size = args[:size]
@parts = args[:parts]
end
def spares
parts.spares
end
end
require 'forwardable'
class Parts
extend Forwardable
def_delegators :@parts, :size, :each
include Enumerable
def initialize(parts)
@parts = parts
end
def spares
select { |part| part.needs_spare }
end
end
require 'ostruct'
module PartsFactory
def self.build(config, parts_class = Parts)
parts_class.new(
config.collect { |part_config|
create_part(part_config)
}
)
end
def self.create_part(part_config)
OpenStruct.new(name: part_config[0], description: part_config[1], needs_spare: part_config.fetch(2, true))
end
end
# 独自なのは以下で定義する。
road_config = [
['chain', '10-speed'],
['tire_size', '23'],
['tape_color', 'red']
]
mountain_config = [
['chain', '10-speed'],
['tire_size', '2.1'],
['front_shock', 'Manitou', false],
['rear_shock', 'Fox']
]
|
こうすることで、RoadBikeとMountainBikeそれぞれの差異はconfigだけで設定できるようになる。
特殊なコンポジション:集約
「(個)を(塊)にコンポーズする。」という言い方(のはず)。
どちらも「has-a」の関係。
コンポジション:上位がなくなるとなくなる
集約:上位がなくなってもなくならない