14、面向对象编程中的组合与继承:原理、应用与抉择

面向对象编程中的组合与继承:原理、应用与抉择

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 关系,且继承的收益大于成本时,优先考虑使用继承。
    • 对于小范围、自然形成静态、明显特化层次结构的现实对象,继承可以提供清晰的代码结构和高效的代码复用。
    • 但要注意避免过度使用继承,以免导致代码的耦合度增加,难以维护。
  • 组合的使用场景

    • 当对象包含多个部分且整体行为大于部分之和时,选择组合。
    • 组合可以提供更高的灵活性和可扩展性,使得对象的结构更加清晰,易于理解和维护。
    • 组合对象的独立性使得它们在面对需求变化时具有更好的适应性。
  • 鸭子类型的使用场景

    • 当多个不同对象需要扮演共同角色时,使用鸭子类型。
    • 鸭子类型可以减少代码的耦合度,提高代码的可复用性,使得不同的对象可以无缝地替换使用。

在实际编程中,要根据具体的问题和需求,综合考虑继承、组合和鸭子类型的优缺点,选择最合适的方式来组织代码,以提高代码的质量和可维护性。

总之,掌握继承和组合的原理和应用,能够帮助开发者更好地设计和实现面向对象的程序,应对各种复杂的编程场景。

基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究(Matlab代码实现)内容概要:本文围绕基于序贯蒙特卡洛模拟法的配电网可靠性评估展开研究,重点介绍了利用Matlab代码实现该方法的技术路径。文中详细阐述了序贯蒙特卡洛模拟的基本原理及其在配电网可靠性分析中的应用,包括系统状态抽样、时序模拟、故障判断修复过程等核心环节。通过构建典型配电网模型,结合元件故障率、修复时间等参数进行大量仿真,获取系统可靠性指标如停电频率、停电持续时间等,进而评估不同运行条件或规划方案下的配电网可靠性水平。研究还可能涉及对含分布式电源、储能等新型元件的复杂配电网的适应性分析,展示了该方法在现代电力系统评估中的实用性扩展性。; 适合人群:具备电力系统基础知识和Matlab编程能力的高校研究生、科研人员及从事电网规划运行的技术工程师。; 使用场景及目标:①用于教学科研中理解蒙特卡洛模拟在电力系统可靠性评估中的具体实现;②为实际配电网的可靠性优化设计、设备配置运维策略制定提供仿真工具支持;③支撑学术论文复现算法改进研究; 阅读建议:建议读者结合提供的Matlab代码逐段理解算法流程,重点关注状态转移逻辑时间序列模拟的实现细节,并尝试在IEEE标准测试系统上进行验证扩展实验,以深化对方法机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值