第8章 类和模块

建立class/instance构造与state边界,掌握self、accessor、visibility、继承、alias/undef、singleton class、module namespace、Mixin与duck typing。

学习目标

  • 能解释类和实例与继承在“第8章 类和模块”中的责任边界
  • 能围绕“怎样用类不变量和方法查找链解释继承、模块混入与单例方法?”运行正常与故障轨迹并定位首个分岔
  • 能用“第8章 类和模块的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”

来源、版次与运行边界

“第8章 类和模块”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单练习答案勘误只作为公开支持材料,不被冒充为原书全文。

对“第8章 类和模块”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。

“第8章 类和模块”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。

围绕“怎样用类不变量和方法查找链解释继承、模块混入与单例方法?”,本页验收“第8章 类和模块的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“混入同名方法后不核对 ancestors,错误实现静默覆盖原行为”;若无法定位第一处状态分岔,就拒绝当前解释。

从“把数据装进class”这个不足开始

Class的价值不是把若干instance variables包起来,而是让每个object在构造后始终满足业务不变量,并通过有限messages改变state。决定constructor、accessor、visibility和error policy。

先预测:给余额类直接attr_accessor :balance是否足够?它允许任何caller写负数、String或nil,绕过存取规则。应暴露deposit/withdraw等意图方法,只给真正不破坏约束的数据reader。

类是什么:类和实例

Class对象保存instance method definitions并能构造instances。Instance通过其class和ancestor chain查找method,每个instance拥有自己的instance variables。Class本身也是class,因此class method通常是class object的singleton method。

instance_of?只匹配精确class,is_a?/kind_of?沿class与included modules判断。业务protocol通常不应先硬性class-check;只在serialization/security boundary或不同domain type确需分流时使用。

继承

Subclass继承method lookup关系,不复制superclass object state。class PremiumAccount < Account表示is-a替换承诺:任何期望Account protocol的caller都应能安全使用PremiumAccount。限制继承。

如果只是“使用另一个对象的能力”,composition通常比inheritance更清楚。深层hierarchy容易让state owner、super顺序和override不变量分散;先写protocol与tests,再决定继承是否合适。

创建类:class语句与initialize方法

class Name ... end打开或reopen一个Class constant。Name.new通常allocate instance并调用private initialize;initialize返回值不会替代new返回的instance。Constructor应验证required data并建立完整state,避免“半初始化后再调用setup”。

class Account
  attr_reader :balance
 
  def initialize(balance = 0)
    amount = Integer(balance)
    raise ArgumentError, "negative balance" if amount < 0
 
    @balance = amount
  end
 
  def deposit(amount)
    value = Integer(amount)
    raise ArgumentError, "amount must be positive" unless value > 0
 
    @balance += value
  end
end

Constructor做I/O会让对象创建慢且失败边界混乱;可用factory先加载/解析,再把validated value交给initialize。若对象有多种清晰来源,命名factory如from_csv比一个接受任意options的constructor更可读。

实例变量与实例方法

@balance附着于当前receiver,未初始化读取为nil。不要依赖这个宽松行为:constructor建立所有required instance variables,optional state也用明确nil semantics。Instance method默认访问当前self的instance variables,无法直接读取另一个object的@balance,除非reflection绕过边界。

存取器

attr_reader生成reader,attr_writer生成writer,attr_accessor两者皆有。Accessor仍是method,可在后续自定义validation,但一开始公开writer就形成API。鼓励intent methods。

特殊变量self

self是当前receiver:顶层、class body、class method、instance method和block中可能不同。Setter调用常需self.name = value,否则name = value被解析为local assignment。Private method通常不能用显式receiver调用,self也不例外(setter语法等版本细节需测试)。

类方法、常量与类变量

Class method定义在class object上,适合factory/registry查询;class body中的constant提供namespace内固定绑定。@@variable沿inheritance hierarchy共享,subclass写入可能影响其它class。若每个class应独立持有state,使用class-instance variable,并明确subclass是否inherit default。

class Parser
  FORMATS = [:json, :csv].freeze
  @default_format = :json
 
  class << self
    attr_reader :default_format
 
    def supports?(format)
      FORMATS.include?(format)
    end
  end
end

Mutable registry作为class state仍需同步、reset test hook与lifecycle。Constant Array freeze只冻结外层;这里Symbol elements immutable。第4章已建立binding与object mutation差异,本章把owner放进class object。

限制方法的调用

public是外部API,private通常只能以implicit receiver调用,protected允许相关instances间以receiver方式协作。帮助缩小可依赖surface,不是security sandbox。

Visibility可用关键字影响后续definitions,或private :method_name指定。Public method应少而稳定;private method可重构,但tests最好经public behavior验证,避免锁死实现。Protected较少使用,账户间比较等同class/private helper常是典型场景。

扩展类:在原有类的基础上添加方法

再次写class String ... end会reopen现有class,新增或替换method,影响process中所有instances。Core extension可能与未来Ruby/library方法冲突;优先wrapper、module function、refinement(版本/作用域需验证)或domain value object。

Monkey patch确有必要时记录owner、目标version、method是否已存在,并有contract tests与移除计划。Load order决定最后definition,部署环境差异可能只在特定require顺序出现。

继承中的override与super

Override改变subclass behavior,super继续ancestor lookup。Bare super转发当前arguments/block,super()不传arguments;差异会导致constructor或wrapper的隐蔽arity bug。必须由测试覆盖。

alias与undef

alias new_name old_name在执行时复制当前method entry到新name;以后重定义old不自动更新alias。alias_method是Module method,便于dynamic name。Wrapper更常用prepend+super,保留lookup可组合性。

undef method_name使当前class/module阻止该name继续沿ancestor lookup,remove_method只删除当前owner definition并可能暴露ancestor implementation。两者语义不同,framework patch尤其要谨慎。

单例类

每个object可有singleton class,singleton methods定义在其中并优先于普通class lookup。def object.labelclass << object打开它。Class methods本质上也是class object的singleton methods。

Singleton method适合测试替身或一个对象的窄适配,但大量per-object behavior会让method owner难追踪且影响对象可复制/序列化。通常把策略放独立object/module并注入更透明。

模块是什么:Mixin与命名空间

Module不能像Class那样new普通instances,却可组织constants/methods并进入method lookup chain。区分namespace和Mixin。

Namespace如Billing::Invoice避免constant collision,并让file layout/load path有清楚映射。Mixin则假设receiver提供某些primitive methods,再组合出higher-level behavior;这个隐式requirement必须写成protocol并在include时验证。

创建模块与Mix-in

module Printable
  def print_to(output)
    output.puts(to_text)
  end
end
 
class Receipt
  include Printable
 
  def initialize(total)
    @total = total
  end
 
  def to_text
    "total=#{@total}"
  end
end

Printable要求receiver响应to_text,并提供print_to。这是duck-typed internal protocol。Module中的constants按lexical/qualified rules访问;module method可用module_functionextend self创建,但两者visibility/copy语义不同,要按目标设计。

查找方法的规则

简化lookup顺序为object singleton class、prepended modules、class、included modules、superclass及其同类链;实际还有BasicObject等终点。Include顺序和多次include会影响chain,使用ancestors验证,不靠记忆猜。

extend方法

object.extend(Mod)把Mod加入该object singleton class的ancestor chain,使module instance methods成为该object可调用methods。SomeClass.extend(Mod)因此常提供class-level API;它不同于include给SomeClass instances加methods。

面向对象程序设计与鸭子类型

Object-oriented design围绕协作objects与messages组织,封装state变化,使用polymorphism替代散布type switch。Duck typing问“能否履行protocol”,不要求共同superclass。远比respond_to?更完整。

例如output只需响应write/puts,可传File、StringIO或test spy;clock只需提供monotonic time,测试可传fake。Contract tests应对至少两个独立implementations运行,证明caller依赖行为而非具体class。遇到NoMethodError时先查protocol是否未满足,不要立即加入宽泛class check。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 9 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 10 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 11 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 12 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 13 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。
  • :第 14 个正式节点要能回到“公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。”,并说明故障发生前后的第一处差异。

对象与状态模型

从输入、接收者到可观察证据

怎样用类不变量和方法查找链解释继承、模块混入与单例方法?

输入与接收者

固定类和实例所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明继承的允许状态。

观察证据

保存第8章 类和模块的初值、参数、编码或资源位置。

正式节点:类和实例、继承、class语句与initialize方法、实例变量与实例方法、存取器、特殊变量self、限制方法的调用、扩展类、alias与undef、单例类、Mix-in与命名空间、查找方法的规则、extend方法、鸭子类型

控制与消息轨迹

在相同初值下定位首个分岔

  1. 01固定类和实例的输入和接收者
  2. 02执行继承并记录状态
  3. 03观察class语句与initialize方法的返回或副作用
  4. 04用鸭子类型核对不变量并复位

运行不变量:公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。

边界故障探针

一次只破坏一个前提

基线满足:公开操作保持对象有效,方法来源和 self 在每个调用点都可追踪。

本章回顾:Class维护不变量,Module组合名称与行为

  1. Initialize建立object invariant,instance methods维护state,accessor只暴露必要能力。
  2. Self决定当前receiver;class/instance state与@@hierarchy sharing必须分清owner。
  3. Visibility缩小稳定API,inheritance须满足substitution;reopen、alias/undef和singleton class会改变lookup。
  4. Include、extend、prepend和super把module/class放在不同lookup位置,使用ancestors与method owner验证。
  5. Duck typing依赖完整behavior protocol;namespace组织constants,Mixin在明确requirement上复用behavior。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样用类不变量和方法查找链解释继承、模块混入与单例方法?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“混入同名方法后不核对 ancestors,错误实现静默覆盖原行为”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联类和实例、继承、class语句与initialize方法、实例变量与实例方法、存取器、特殊变量self、限制方法的调用、扩展类、alias与undef、单例类、Mix-in与命名空间、查找方法的规则、extend方法、鸭子类型,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

类和实例

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

继承

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

class语句与initialize方法

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

实例变量与实例方法

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

存取器

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

特殊变量self

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

限制方法的调用

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

扩展类

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

alias与undef

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

单例类

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

Mix-in与命名空间

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

查找方法的规则

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

extend方法

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

鸭子类型

“第8章 类和模块”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

本章以Ruby 基础教程第 5 版作者支持资料合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…