ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ruby 面向对象编程基础:类、方法、作用域与命令行项目实战指南(Odin 课程篇)

Ruby 面向对象编程基础:类、方法、作用域与命令行项目实战指南(Odin 课程篇) Ruby 面向对象编程基础类、方法、作用域与命令行项目实战指南Odin 课程篇【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum面向对象编程OOP是 Ruby 的灵魂——这门语言里一切皆对象因此掌握类、方法、作用域这三个核心概念是把你已经学会的 Ruby 语法积木高效、优雅地组合成完整程序的关键。本指南以 The Odin Project 课程中 ruby/object_oriented_programming_basics/object_oriented_programming.md 一课为主线结合同一课程下 Ruby 基础篇的方法、变量、数据类型等前置知识以及井字棋Tic Tac Toe与 Mastermind 两个实战项目文档系统讲解 OOP 的通用原则、类的定义与状态管理、方法设计、作用域与继承机制。读完本文你将能独立用 Ruby 设计出职责清晰、低耦合的类结构并完成命令行游戏的 OOP 重构。为什么需要面向对象把积木组合成程序在课程的引言中作者把已经掌握的 Ruby 基础知识比作积木而 OOP 就是如何以最高效、最优雅的方式组合这些积木来写出我们想要的程序。这一课强调OOP 的核心概念并不专属 Ruby而是适用于任何面向对象语言的通用思想因为它们本身就是基础的、根本的Dont Repeat YourselfDRY不要重复自己把重复的逻辑抽出来复用而不是到处拷贝粘贴模块化Modularize把程序拆成边界清晰、可独立理解的小单元单一职责让每个类和方法只做一件事one thing最小化接口暴露尽量少地向外界展示你的接口Show as little of your interfaces to the world as you can低耦合不要让方法或类彼此重度依赖。这些原则需要时间和练习才能真正用好但仅靠学完本节你就已经在写出高质量代码的道路上迈出了一大步。需要特别强调的是这一节配有两个实战项目井字棋与 Mastermind但并不意味着只有这两个项目才会用到 OOP——Ruby 是面向对象语言之后所有剩余项目都应该以 OOP 的方式去实现。前置基础Ruby 的对象模型与方法本质在进入类之前需要先建立两个重要的前置认知它们都来自本课程 Ruby 基础篇的方法课与数据类型课。第一一切皆对象。Ruby 是高度面向对象的语言哪怕是数字、字符串、符号、布尔值这些最基础的数据类型本质上也是对象。例如17 / 5得到整数3而17 / 5.0得到浮点3.4——这些运算实际都是对象方法调用的结果。正因为一切都是对象方法调用随处可见anything.reverse、6.even?、hello.include?(lo)都是对象上的方法调用。第二Ruby 只有方法没有函数。许多语言区分与对象无关的函数和绑定在接收者对象上的方法而 Ruby 是纯粹面向对象的语言所有方法都至少关联一个对象。因此在本课程语境里方法与函数指同一件事#even?、#length、#split这类写法中井号#是书写 Ruby 实例方法时的惯用记法例如String#upcase。这一认知意味着当你开始用class定义自己的类时你实际上是在定义一类新的对象并为这类对象声明属于它们的方法。类Class定义对象的蓝图什么是类class是本节知识检查的第一个问题。类是面向对象编程的基本组织单元它是一类对象的蓝图或模板类定义了这类对象拥有哪些状态通过实例变量保存以及能执行哪些行为通过方法定义。一个最小的类定义与实例化示例如下沿用课程方法课中def...end的语法习惯class Player def initialize(name) name name end def name name end end player Player.new(Alex) puts player.name # Alexclass Player ... end声明了一个新类initialize是构造方法在Player.new(...)被调用时自动执行常用于初始化实例变量name是实例变量用于保存每个对象自己的状态player.name调用读取器方法把name的值暴露给外部。课程知识检查要求回答的问题包括类的用途、getters 和 setters 是什么、继承是什么、实例变量的用途以及类变量与实例变量的区别。下面的小节将逐一展开。实例变量每个对象的私有状态**实例变量instance variables**以开头如name、score它的用途是让每个对象实例保存属于自己的一份状态并在该对象的各个方法之间共享。正是实例变量赋予了对象记住自己状态的能力——例如井字棋棋盘对象需要记住当前棋子的布局Mastermind 游戏对象需要记住秘密代码这些都应该用实例变量承载。实例变量与局部变量有本质区别局部变量在方法内部声明、方法结束即失效而实例变量一旦被某个对象初始化就会伴随该对象存活可以在该对象的任何实例方法中被访问。这也直接关联到作用域话题见后文。类变量与实例变量的区别知识检查要求明确区分类变量class variables与实例变量维度实例变量name类变量count归属每个实例对象各自独立一份整个类共享一份所有实例与类本体访问范围该对象的实例方法内类方法、实例方法中均可访问典型用途保存对象个体状态棋盘布局、玩家姓名统计这个类创建了多少实例之类的类级数据需要提醒的是本课程变量课在作业中明确告诫不要使用$global_variables全局变量而class_variables类变量很少需要且容易被误用见 ruby/basic_ruby/variables.md。设计 OOP 程序时应优先用实例变量保存对象状态把类变量和全局变量视为需要充分理由才使用的工具。Getters 与 Setters受控地暴露接口前面提到尽量少向外界展示接口但对象间协作终究需要交换数据。**getters读取器和 setters写入器**就是 Ruby 中受控暴露对象状态的标准手段class Player def initialize(name) name name end # getter def name name end # setter def name(new_name) name new_name end end player Player.new(Alex) player.name Bob # 调用 name 写入器 puts player.name # 调用 name 读取器 Bob为免去手写样板代码Ruby 提供了三个内置辅助宏出现在类体内、作用于类本身class Player attr_reader :name # 只读生成 getter attr_writer :name # 只写生成 setter attr_accessor :name # 读写同时生成 getter 和 setter endattr_reader只生成读取器适合只读不写的状态如游戏回合数attr_accessor同时生成读取器与写入器最常用若某个状态允许外部修改但需要附加校验逻辑则仍应手写 setter 而不是用attr_accessor。这也是本节知识检查中What are getters and setters?的答案所在它们是类对外提供读写内部状态能力的接口方法。方法Methods对象的行为方法定义的基础语法与命名规则课程在方法课中有完整讲解这里结合 OOP 语境重点回顾对设计类至关重要的几点。定义、命名与调用def greet(name stranger) Hello, #{name}! end puts greet(Jane) # Hello, Jane! puts greet # Hello, stranger!def ... end定义方法def和end都是内置关键字方法名使用snake_case以小写字母开头大写开头的名字在 Ruby 中是常量不能使用 Ruby 约 40 个保留字如end、while、for作为方法名名字允许以?、!、结尾分别表示谓词方法、bang 方法、写入器参数可以有默认值如上例的name stranger。隐式返回与显式返回Ruby 是少数支持隐式返回的语言方法会返回最后一个被求值的表达式即使没有return关键字。def even_odd(number) if number % 2 0 That is an even number. else That is an odd number. end end显式return用于提前结束方法的场景例如先做输入校验再继续执行def even_odd(number) unless number.is_a? Numeric return A number was not entered. end if number % 2 0 That is an even number. else That is an odd number. end end设计类方法时务必区分puts与returnputs只是把内容打印到控制台并返回nilreturn才把计算结果交还给调用方继续参与运算。前者适合界面层后者适合逻辑层——把计算与打印分开正是单一职责原则在方法层面的体现。谓词方法与 bang 方法谓词方法predicate methods以?结尾、返回布尔值的方法如6.even?、hello.include?(lo)。Ruby 并不强制这一命名约定但遵循它能让类的语义一目了然——在井字棋项目中Board#winner?这类方法名本身就是一种文档。bang 方法bang methods以!结尾、表示原地修改接收者对象的方法如whisper.downcase!等价于whisper whisper.downcase。编程的一般原则是不让方法意外覆盖调用它的对象因此是否给方法加!是类设计时的重要决策——它向调用者明确声明了副作用。作用域Scope变量可见性的规则知识检查中的两个问题——什么是作用域与何时会定义一个新的作用域——是理解 Ruby 变量行为的关键。作用域指程序中一段代码能看到哪些变量名的规则范围。Ruby 中不同类型的变量拥有不同的作用域规则而新的作用域会在定义方法def...end、类class...end、模块module...end时开启——这是本课给出的核心判定标准局部变量从赋值语句所在位置开始到所在作用域方法/类/模块体结束为止可见。方法内部定义的局部变量在方法外不可访问实例变量x属于对象整体跨该对象的所有实例方法可见与作用域边界无关类变量x属于整个类类方法、实例方法中都能访问跨作用域共享全局变量$x整个程序可见课程明确不建议使用常量大写字母开头有词法作用域规则可在定义它的类内部直接引用。理解作用域对 OOP 设计的意义在于它决定了一个对象的内部状态如何被组织。实例变量天然就是跨方法共享、对外隐藏的对象级作用域而局部变量用于方法内部的临时计算。当你在井字棋项目中思考棋盘布局该是实例变量还是局部变量时实际就是在应用作用域规则只要状态需要被多个方法共享、且属于该对象的生命周期就应当是实例变量只在一个方法内部临时使用的数据用局部变量即可。继承Inheritance复用与扩展类继承允许一个类子类基于另一个类父类扩展子类自动拥有父类的方法与状态结构并可以覆写override父类方法。这也是知识检查明确要求掌握的概念。class Animal def initialize(name) name name end def speak #{name} makes a sound end end class Dog Animal def speak #{name} barks end end animal Animal.new(Generic) dog Dog.new(Rex) puts animal.speak # Generic makes a sound puts dog.speak # Rex barks覆写了父类方法子类通过符号声明继承关系子类实例自动拥有父类的实例变量初始化逻辑与方法覆写父类方法时可在子类方法内用super调用父类同名方法以复用逻辑。在实战项目中继承的典型用法是定义游戏角色的共性、再按具体类型扩展。不过课程也强调平衡继承只是复用手段之一组合让一个类持有另一个类的实例往往能获得更低的耦合。不要滥用继承——保持每个类只做一件事的原则优先考虑对象间的协作关系如棋盘与玩家两个类如何交互。对象之间的关系类如何协作知识检查所引内容还包含两个类如何交互的示例。面向对象设计不只有继承这一种关系更常见的是组合/协作一个类持有另一个类的实例通过公开接口调用彼此的方法。以井字棋为例自然的拆解是Board类负责棋盘格子的状态存储与展示Player类保存玩家的标记X或O与姓名Game类持有Board与两名Player的实例驱动游戏循环、判定胜负。课程在项目文档中给出的三条作业要求正是 OOP 设计的检查清单动手前先想清楚拆解什么应该是类什么应该是实例变量什么应该是方法——几分钟的思考能省下一小时的返工尽量不共享信息注意不要让类之间共享超出必要限度的信息——这正是最小化接口暴露与低耦合原则的实践对照参考实现完成后再对照参考解法审视自己的设计差距。实战项目一命令行井字棋Tic Tac Toeproject_tic_tac_toe.md 要求构建一个命令行双人对战的井字棋游戏两名人类玩家轮流落子每回合之间展示棋盘。项目文档特别强调它非常适合用 OOP 的新腿来游刃有余地解决——两位玩家、一个棋盘、在游戏循环中检查胜利条件这些要素天然对应不同的类与职责。建议的 OOP 拆解与实现要点class Board attr_reader :grid def initialize grid Array.new(3) { Array.new(3) } end def display # 输出当前棋盘布局 end def update(cell, marker) # 在指定格子落子 end def winner? # 检查是否存在三连行、列、对角线 end def full? # 检查棋盘是否已满平局条件 end end class Player attr_reader :name, :marker def initialize(name, marker) name name marker marker end end class Game def initialize board Board.new players [Player.new(Player 1, X), Player.new(Player 2, O)] end def play # 游戏主循环交替让玩家落子、展示棋盘、 # 检查胜利/平局并宣布结果 end end Game.new.play设计时反复自问三件事这个状态该放在哪个类的哪个实例变量中这个方法是不是只做一件事Game是否通过Board的公开方法如display、winner?与其协作而不是直接伸手去改棋盘内部数据当你能不假思索地回答这三个问题OOP 的肌肉记忆就初步建立了。实战项目二Mastermind 猜密码project_mastermind.md 要求构建一个命令行 Mastermind 游戏玩家有 12 次机会猜测由颜色组成的秘密代码每回合得到反馈——哪些位置完全正确、哪些颜色正确但位置错误。它比井字棋更进一步因为它要求逐步重构第一版计算机随机生成秘密代码人类玩家猜测每回合给出准确反馈重构角色互换让玩家可以选择成为秘密代码的创作者还是猜测者计算机智能当玩家出题时让计算机来猜。可以按游戏规则实现标准解谜策略也可以自定义规则可选增强若自定义规则可以让计算机获得更多反馈信息——例如先随机猜但保留完全命中的猜测当计算机猜对了颜色但位置错误时下一次猜测必须把该颜色放在某个位置。这一步先做出来、再换角色、再加策略的节奏正是课程反复强调的工程方法先用最简单的结构让程序跑通再用 OOP 重构改善设计。状态与行为的归属Code类负责生成与比对、Feedback负责给出反馈、ComputerPlayer负责策略、Game负责回合循环将在这一过程中自然浮现。让 OOP 落地项目结构与依赖管理面向对象不是把代码写进一个文件里的类就完事了。同一课程下的管理 Ruby 项目一课给出了让 OOP 代码库保持可维护性的规范一个类一个文件每创建一个新类就为它新建一个文件惯例目录结构把 Ruby 源文件放进lib目录顶层保留一个入口文件如main.rbproject_name ├── lib │ └── lovely_file_of_yours.rb └── main.rb跨文件加载require_relative用于加载自己的代码相对包含它的文件所在目录解析路径与执行位置无关require用于加载$LOAD_PATH中的标准库与已安装的 gem。惯例是自己的代码用require_relative外部依赖用require命名空间所有被加载的代码处于同一个命名空间同名方法、类后加载者会覆盖先加载者用module包裹代码可以建立命名空间避免冲突依赖管理gem 是他人编写的 Ruby 包通过gem install colorize安装Bundler 通过Gemfile声明项目依赖精确到版本约束如gem colorize, ~ 1.1这种悲观版本约束用bundle install安装、用bundle exec ruby main.rb按锁定版本运行Gemfile.lock则记录可复现的依赖环境。对一个包含多个类的 OOP 项目来说这套结构意味着Game、Board、Player各自是lib/下的独立文件main.rb负责require_relative全部类并启动游戏。这也是后续所有 Ruby 项目含 Ruby on Rails 课程的统一组织方式。用 RuboCop 守护 OOP 代码质量面向对象代码尤其容易在大类、大方法中失控。课程配套的RuboCop 一课引入了 Ruby 社区的标准工具链RuboCop是一个 gem按 Ruby Style Guide 检查代码风格Style、潜在错误Lint与可测量的代码属性Metrics。格式化formatting让代码外观整洁一致缩进、空格不改变行为linting让代码更易推理可能改变行为例如强制用#each替代for循环在项目内用 Bundler 安装后运行bundle exec rubocop检查当前目录及其子目录的所有文件rubocop -a执行安全自动修正保证不改变语义rubocop -A执行包含不安全 Cop 的自动修正可能改变语义需人工确认.rubocop.yml是配置文件可通过bundle exec rubocop --init生成支持AllCops: NewCops: enable、EnforcedStyle等自定义项也支持inherit_from继承全局配置。课程特别提醒初学者不要轻易关闭 Metrics 部门它之所以成为最大的敌人是因为它在帮你写出更好的代码。其中两个核心指标值得理解ABC 指标AAssignment赋值、BBranch方法调用、CCondition条件判断与比较三个维度的度量。例如输出[1, 18, 0] 18.03/17表示1 次赋值、18 次方法调用、0 个条件总分为 18.03而允许值为 17——这往往意味着该方法过度依赖对数据的操作应当拆分成更小的步骤圈复杂度cyclomatic complexity基于程序可能经过的路径数衡量复杂度if、循环、、||等控制流语句每多一条分支就加一分Ruby 语境下用#each迭代集合同样计分。从 OOP 的角度看这些指标本质上是在量化类是否太大、方法是否职责单一——当复杂度超标时正确的应对通常是拆分职责、提取新类或新方法而不是用# rubocop: disable Metrics/AbcSize内联注释一禁了之注释只应在确实必要处使用。小结学完本节你应当能回答的问题对照原课程文档末尾的知识检查清单逐条自测什么是类class——一类对象的蓝图定义状态实例变量与行为方法什么是 getters 和 setters——类对外读写内部状态的接口方法可用attr_reader、attr_writer、attr_accessor快速生成什么是继承inheritance——子类基于父类复用与扩展用声明可覆写方法并用super复用父类逻辑什么是作用域scope——代码中变量名可见性的规则集合何时定义一个新的作用域——在定义方法、类、模块时开启新的作用域实例变量的用途是什么——保存每个对象独立的状态在对象的方法间共享类变量与实例变量的区别是什么——类变量整个类共享一份实例变量每个对象各自独立类变量很少需要且容易被误用。掌握这些问题后建议按先井字棋、再 Mastermind 重构的顺序动手实践并在每个项目中使用一个类一个文件 require_relative Bundler 管理依赖的标准项目结构。OOP 的功力不在背诵概念而在一次次先想清楚拆解、再动手编码、最后用 RuboCop 审视的循环中积累起来。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表