Composition

What is Composition?

  1. 组合是指将不同的部分结合成一个整体的行为。使用面向对象的组合技术,可以将简单的、独立的对象组合成更大更复杂的整体。
  2. 从本质上讲,参与组合的那些对象都很小,他们在结构上都是独立的。这使得他们能够无缝低转换为可插入、可互换的组件。

重构

创建零件

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
class Bicycle
attr_reader :size, :parts

def initialize(args = {})
@size = args[:size]
@parts = args[:parts]
end

def spares
parts.spares
end
end


class Parts < Array
attr_reader :parts

def initialize(parts)
@parts = parts
end

def spares
parts.select {|part| part.needs_spare}
end

def size
parts.size
end

end

class Part
attr_reader :name, :description, :needs_spare

def initialize(args)
@name = args[:name]
@description = args[:description]
@needs_spare = args.fetch(:needs_spare,true)
end
end


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)

组装

此时size能正常响应,但是不能执行数组之间的加法会导致问题. 尽管+连接的是 Parts 对象,但是+ 返回的对象是 Array实例。Array并不知道如何响应spares

1
2
3
road_bike_parts = Parts.new([chain, road_tire, tape])
p road_bike_parts.spares
p road_bike_parts.size

让Parts对象更像一个数组

  1. The Forwardable module
    Forwardable is a module that can be used to add behavior to all the instances of a given class. This module is included to the singleton class using the extend keyword in order to add methods at class-level (to keep it simple).
  2. The Forwardable#def_delegator method allows an object to forward a message to a defined receiver.
    • The first argument correspond to the receiver of the message forwarding.
    • The second argument is the message to forward.
    • And finally the third argument is an alias of the message.
  3. The def_delegators method
  4. The delegate method
    The delegate method accepts a hash as argument where:
    • the key is one or more messages
    • the value is the receiver of the messages defined as key
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      # in forwardable.rb
      require 'forwardable'
      class Computer
      attr :cores, :screens
      extend Forwardable
      delegate %I[size] => :@cores,
      %I[length] => :@screens
      def initialize
      @cores = (1..8).to_a
      @screens = [1, 2]
      end
      end
      macrosoft = Computer.new
      puts "Cores: #{macrosoft.size}"
      puts "Screens: #{macrosoft.length}"

The 2 main differences with the def_delegator method is that it takes a set of methods to forward and the methods cannot be aliased

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
57
58
59
require 'forwardable'

class Bicycle
attr_reader :size, :parts

def initialize(args = {})
@size = args[:size]
@parts = args[:parts]
end

def spares
parts.spares
end
end

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

class Part
attr_reader :name, :description, :needs_spare

def initialize(args)
@name = args[:name]
@description = args[:description]
@needs_spare = args.fetch(:needs_spare,true)
end
end


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)

mountain_bike = Bicycle.new(
size: 'L',
parts: Parts.new([chain, mountain_tire, front_shock, rear_shock])
)

road_bike = Bicycle.new(
size: 'L',
parts: Parts.new([chain, road_tire, tape])
)

p mountain_bike.size
p road_bike.size

创建零件工厂

对象如何创建的知识,最好放在工厂里面。这样你就只需要一个说明书,就能创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
road_config = [['chain','10-speed'],
['tire_size','23'],
['tape_color','red']
]


module PartsFactory
def self.build(config, part_class = Part, parts_class = 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

road_parts = PartsFactory.build(road_config)
p road_parts.spares
Donate article here