面向对象编程中的组合与继承:原理、应用与抉择
1. 自行车类的初步实现与问题
在编程实践中,我们先有了自行车(
Bicycle
)、零件集合(
Parts
)和零件(
Part
)类的可用版本。以下是创建公路自行车和山地自行车的代码示例:
road_bike =
Bicycle.new(
size: "L",
parts: Parts.new([chain,
road_tire,
tape]))
mountain_bike =
Bicycle.new(
size: "L",
parts: Parts.new([chain,
mountain_tire,
front_shock,
rear_shock]))
puts mountain_bike.spares.size # => 3
puts mountain_bike.parts.size # => 4
puts mountain_bike.parts + road_bike.parts
# => undefined method '+' for #<Parts:0x007fc1d59fe040>
从上述代码可以看出,在创建自行车时,需要明确指定每个自行车的零件,这使得代码中包含了大量关于零件创建和组合的知识,这些知识容易在整个应用中泄露,造成不必要的复杂性。
2. 零件制造与配置
为了解决上述问题,我们可以通过简单的二维数组来描述构成特定自行车的零件组合。以下是公路自行车和山地自行车的配置示例:
road_config =
[['chain', '11-speed'],
['tire_size', '23'],
['tape_color', 'red']]
mountain_config =
[['chain', '11-speed'],
['tire_size', '2.1'],
['front_shock', 'Manitou'],
['rear_shock', 'Fox', false]]
这个二维数组的每一行包含三个可能的列:第一列是零件名称,第二列是零件描述,第三列(可选)是一个布尔值,表示该零件是否需要备用件。
3. 创建零件工厂(
PartsFactory
)
为了更好地管理零件的创建,我们引入了零件工厂(
PartsFactory
)模块。以下是其实现代码:
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
这个工厂模块的
build
方法接受一个配置数组、零件类和零件集合类作为参数,根据配置数组创建零件对象,并返回一个零件集合对象。
使用零件工厂创建零件集合的示例如下:
puts PartsFactory.build(config: road_config).inspect
puts PartsFactory.build(config: mountain_config).inspect
零件工厂将创建有效零件集合所需的所有知识隔离在一个类和配置数组中,避免了知识在应用中的分散。
4. 利用零件工厂简化代码
由于零件工厂已经包含了
needs_spare
默认值为
true
的逻辑,我们可以对
Part
类进行简化。原
Part
类如下:
class Part
attr_reader :name, :description, :needs_spare
def initialize(name:, description:, needs_spare: true)
@name = name
@description = description
@needs_spare = needs_spare
end
end
可以将其替换为
OpenStruct
,简化后的
PartsFactory
代码如下:
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
这样,
Part
类就可以被简单的
OpenStruct
对象替代,进一步简化了代码。
5. 组合式自行车的实现
通过上述优化,我们得到了一个使用组合的自行车类的完整实现:
class Bicycle
attr_reader :size, :parts
def initialize(size:, parts:)
@size = size
@parts = 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', '11-speed'],
['tire_size', '23'],
['tape_color', 'red']]
mountain_config =
[['chain', '11-speed'],
['tire_size', '2.1'],
['front_shock', 'Manitou'],
['rear_shock', 'Fox', false]]
使用这些类创建不同类型自行车的示例如下:
road_bike =
Bicycle.new(
size: 'L',
parts: PartsFactory.build(config: road_config))
mountain_bike =
Bicycle.new(
size: 'L',
parts: PartsFactory.build(config: mountain_config))
puts road_bike.spares
puts mountain_bike.spares
这种组合式的实现方式用 54 行代码替代了之前 66 行的继承层次结构,并且创建新类型自行车变得更加简单。例如,添加对躺式自行车的支持只需要 3 行配置代码:
recumbent_config =
[['chain', '9-speed'],
['tire_size', '28'],
['flag', 'tall and orange']]
recumbent_bike =
Bicycle.new(
size: 'L',
parts: PartsFactory.build(config: recumbent_config))
puts recumbent_bike.spares
6. 组合与聚合的概念
-
组合
:描述的是一种
has-a关系,如自行车有零件。组合对象通过明确定义的接口与其组成部分进行交互。组合可以分为广义和狭义两种概念,狭义的组合表示包含对象没有独立于容器的生命。 - 聚合 :是一种特殊的组合,包含对象具有独立的生命。例如,大学有部门,部门有教授,部门消失时教授仍然存在。
组合和聚合的区别在实际代码中可能影响不大,在大多数情况下可以统一使用组合来描述这两种关系。
7. 继承与组合的对比
| 对比项 | 继承 | 组合 |
|---|---|---|
| 定义 | 行为分散在对象中,通过类关系自动委托消息调用正确行为 | 对象之间的关系不通过类层次结构编码,需要显式委托消息 |
| 成本与收益 | 以对象层次结构为代价,获得免费的消息委托 | 允许对象具有结构独立性,但需要显式消息委托 |
| 适用场景 | 适用于小范围、自然形成静态、明显特化层次结构的现实对象 | 适用于对象包含多个部分且整体行为大于部分之和的情况 |
8. 继承的成本与收益
-
收益
:
- 合理性 :继承层次结构顶部定义的方法影响广泛,小的代码更改可以带来大的行为变化。
- 可用性 :继承的代码具有开闭原则,易于扩展新的子类。
- 示范性 :正确编写的继承层次结构易于扩展,为开发者提供指导。
-
成本
:
- 错误选择的风险 :可能选择继承来解决不适合的问题,导致后续添加行为困难,需要复制或重构代码。
- 依赖问题 :继承带来的依赖可能不被其他开发者接受,尤其是在代码被广泛使用时。
9. 组合的成本与收益
-
收益
:
- 透明性 :组合对象通常由小的、职责明确的对象组成,代码易于理解。
- 合理性 :通过接口与部分交互,添加新的部分简单,无需修改代码。
- 可用性 :组合对象结构独立,易于在新的上下文中使用。
-
成本
:
- 整体复杂性 :组合对象依赖多个部分,整体操作可能不明显。
- 消息委托成本 :需要显式委托消息,相同的委托代码可能在多个对象中重复。
10. 选择合适的关系
-
使用继承处理
is-a关系 :当对象之间存在明显的is-a关系,且继承的收益大于成本时,选择继承。例如,游戏中的各种减震器都属于减震器,可以使用继承建模。 -
使用鸭子类型处理
behaves-like-a关系 :当多个不同对象需要扮演共同角色时,识别角色的存在,定义其鸭子类型的接口,并为每个可能的扮演者提供接口实现。 -
使用组合处理
has-a关系 :当对象包含多个部分且整体行为大于部分之和时,选择组合。例如,自行车有零件,但自行车本身有独立于零件的行为。
综上所述,在编程中应根据具体问题选择合适的继承或组合方式,以降低应用成本,提高代码的可维护性和可扩展性。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B{问题类型?}:::decision
B -->|is-a关系| C(使用继承):::process
B -->|behaves-like-a关系| D(使用鸭子类型):::process
B -->|has-a关系| E(使用组合):::process
C --> F([结束]):::startend
D --> F
E --> F
通过以上的分析和示例,我们可以更清晰地理解继承和组合在面向对象编程中的应用和选择原则,从而编写出更加高效、可维护的代码。
面向对象编程中的组合与继承:原理、应用与抉择
11. 继承与组合在实际场景中的应用示例
11.1 继承在游戏零件中的应用
在游戏开发中,如前文提到的自行车游戏,玩家可以“购买”零件来组装自行车,其中减震器是一种重要的零件。假设游戏中有多种不同类型的减震器,它们都具有一些共同的属性和行为,如减震效果、价格等。我们可以使用继承来建模这些减震器:
class Shock
def initialize(price, damping_effect)
@price = price
@damping_effect = damping_effect
end
def display_info
puts "Price: #{@price}, Damping Effect: #{@damping_effect}"
end
end
class BasicShock < Shock
def initialize
super(100, "Basic")
end
end
class AdvancedShock < Shock
def initialize
super(200, "Advanced")
end
end
basic_shock = BasicShock.new
advanced_shock = AdvancedShock.new
basic_shock.display_info
advanced_shock.display_info
在这个示例中,
Shock
是一个抽象的基类,定义了减震器的基本属性和行为。
BasicShock
和
AdvancedShock
是
Shock
的子类,继承了基类的属性和方法,并可以根据需要进行扩展。这种继承结构使得代码易于理解和维护,同时也方便添加新的减震器类型。
11.2 组合在自行车组装中的应用
回到自行车的例子,自行车由多个零件组成,每个零件都有自己的特性。我们可以使用组合来实现自行车的组装:
require 'ostruct'
class Part
def initialize(name, description, needs_spare = true)
@name = name
@description = description
@needs_spare = needs_spare
end
def to_s
"#{@name}: #{@description}, Needs Spare: #{@needs_spare}"
end
end
class Parts
def initialize(parts)
@parts = parts
end
def spares
@parts.select { |part| part.needs_spare }
end
def to_s
@parts.map(&:to_s).join("\n")
end
end
class Bicycle
def initialize(size, parts)
@size = size
@parts = parts
end
def spares
@parts.spares
end
def to_s
"Size: #{@size}\nParts:\n#{@parts}"
end
end
chain = Part.new("Chain", "11-speed")
tire = Part.new("Tire", "2.1")
shock = Part.new("Shock", "Manitou")
parts = Parts.new([chain, tire, shock])
bike = Bicycle.new("L", parts)
puts bike
puts "Spares:"
bike.spares.each { |part| puts part }
在这个示例中,
Part
类表示一个零件,
Parts
类表示零件的集合,
Bicycle
类表示自行车。自行车通过组合零件集合来实现其功能,这种组合方式使得自行车的组装更加灵活,方便添加、替换零件。
12. 继承与组合的权衡决策流程
在实际编程中,如何选择继承还是组合是一个重要的决策。以下是一个权衡决策的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B{对象关系类型?}:::decision
B -->|is-a关系| C{继承收益是否大于成本?}:::decision
C -->|是| D(使用继承):::process
C -->|否| E(考虑其他方式):::process
B -->|behaves-like-a关系| F(使用鸭子类型):::process
B -->|has-a关系| G{整体行为是否大于部分之和?}:::decision
G -->|是| H(使用组合):::process
G -->|否| I(考虑其他方式):::process
D --> J([结束]):::startend
E --> J
F --> J
H --> J
I --> J
这个流程图展示了在选择继承或组合时的决策过程:
1. 首先判断对象之间的关系类型,是
is-a
、
behaves-like-a
还是
has-a
关系。
2. 如果是
is-a
关系,进一步判断继承的收益是否大于成本,如果是则使用继承,否则考虑其他方式。
3. 如果是
behaves-like-a
关系,使用鸭子类型。
4. 如果是
has-a
关系,判断整体行为是否大于部分之和,如果是则使用组合,否则考虑其他方式。
13. 总结与建议
在面向对象编程中,继承和组合是两种重要的代码组织方式,它们各有优缺点,适用于不同的场景。以下是一些总结和建议:
-
继承的使用场景 :
-
当对象之间存在明显的
is-a关系,且继承的收益大于成本时,优先考虑使用继承。 - 对于小范围、自然形成静态、明显特化层次结构的现实对象,继承可以提供清晰的代码结构和高效的代码复用。
- 但要注意避免过度使用继承,以免导致代码的耦合度增加,难以维护。
-
当对象之间存在明显的
-
组合的使用场景 :
- 当对象包含多个部分且整体行为大于部分之和时,选择组合。
- 组合可以提供更高的灵活性和可扩展性,使得对象的结构更加清晰,易于理解和维护。
- 组合对象的独立性使得它们在面对需求变化时具有更好的适应性。
-
鸭子类型的使用场景 :
- 当多个不同对象需要扮演共同角色时,使用鸭子类型。
- 鸭子类型可以减少代码的耦合度,提高代码的可复用性,使得不同的对象可以无缝地替换使用。
在实际编程中,要根据具体的问题和需求,综合考虑继承、组合和鸭子类型的优缺点,选择最合适的方式来组织代码,以提高代码的质量和可维护性。
总之,掌握继承和组合的原理和应用,能够帮助开发者更好地设计和实现面向对象的程序,应对各种复杂的编程场景。
超级会员免费看
10

被折叠的 条评论
为什么被折叠?



