一、 让自己熟悉Ruby
1、理解 Ruby 中的 True
在 Ruby 中,除了 false 和 nil, 其他值都是真值。
false 和 nil 是唯二的假值,因此用 true 对象表示真值是冗余的,任何非 false、非 nil 的对象都可以表示真值。
false == x ,尽可能的将判断的标准写在 '==' 的左边,以避免被其他类所覆盖
2、所有对象的值都可能为 nil
我们需要通过优秀的测试挑出各种各样的错误。
根据Ruby的类型系统运作方式,任何对象都可以为nil。
在适合的时候用转换方法,如to_s 和 to_i,可以将nil对象强制转换为你期待的类型。
Array#compact方法返回去除所有nil元素的数组。
3、避免使用 Ruby 中奇怪的 Perl 风格语法
方法 String#match 比 “ =~ ” 更符合语言习惯,并且该方法不使用任何操作符 “ =~ ” 所产生的特殊全局变量。
用 $LOAD_PATH 代替 全局变量 $:
避免使用隐式读写全局变量 $_ 的方法(比如, Kernel#print、Regexp#~ 等)。
4、留神,常量是可变的
总是将常量冻结,从而防止其被改变。
如果常量引用了一个集合对象比如数组或者散列,那么冻结这个集合及其所有元素。
要防止常量被重新赋值,可以冻结定义它的那个模块。
5、留意运行时警告
使用命令行选项 “ -w ” 来运行 Ruby 解释器以启用编译时和运行时的警告,设置环境变量 RUBYOPT 为 “ -w ”也可以达到相同目的。
如果必须禁用运行时的警告,可以临时将全局变量 $VERBOSE 设置为nil。
二、类、对象和模块
6、了解 Ruby 如何构建继承体系
要寻找一个方法、Ruby 只需要向上搜索类体系。 如果没有找到这个方法,就从起点开始搜索 method_missing 方法。
包含模块时 Ruby 会悄悄地创建单例类,并将其插入在继承体系中包含它的类的上方。
单例方法(类方法和针对对象的方法)存储于单例类中,它也会被插入继承体系中。
对象是变量的容器。类是方法和常量的容器。超类是一个类的父类的花哨名字。Ruby 可以通过 include 方法将模块引入类,实现类似多继承的效果。单例类是继承体系不可见的类,也仅仅是没有名字的、被加以限制的常规类。
7、了解 super 的不同行为
当你想重载继承体系中的一个方法时,关键字 super 可以帮你调用它。
不加括号地无参调用 super 等价于讲宿主方法的所有参数传递给要调用的方法。这样使用 super 仅仅在目标方法和宿主方法接受相同数量的参数时才可用。
如果希望使用 super 并且不向重载方法传递任何参数,必须使用空括号,即super()。
当 super 调用失败时,自定义的 method_missing 方法将丢弃一些有用的信息。第30条中有 method_missing 的替代解决方案。
如果你希望调用一个定义在超类的方法,而同时如果包含的模块中也定义了同名方法,super 会在找到第一个匹配的同名方法后停下来,而那时包含的模块中的方法,不是超类中的。如果真的遇到这种情况,可能是设计上存在严重的问题。可以考虑使用组合而非继承了。
8、初始化子类时调用 super
当创建子类对象时,Ruby 不会自动调用超类中的 initialize 方法。作为替代,常规的方法查询规则也适用于 initialize 方法,只有第一个匹配的副本会被调用。
当为显式使用继承的类定义 initialize 方法时,使用 super 来初始化其父类。在定义 initialize_copy 方法时,应使用相同的规则。
initialize 并不是构建新对象的唯一方式。Ruby允许我们使用 dup 和 clone 方法创建对象的副本。当你使用这些方法任意一个时,可以通过定义 initialize_copy 方法对新创建的副本对象执行一些特别的逻辑。
9、提防 Ruby 最棘手的解析
setter 方法在调用时需要显式的接收者。没有接收者时,会被 Ruby 解析为变量赋值。
在实例方法仲调用 setter 方法时,使用 self 作为接收者。
在调用非 setter 方法时,不需要显式指定接收者。换句话说,不要使用不必要的 self , 那会弄乱你的代码。
10、推荐使用 Struct 而非 Hash 存储结构化数据
在处理结构化数据时,如果创建一个新类不那么合适时,推荐使用 Struct 而非 Hash。
将 Struct::new 的返回值赋给常量,并像类一样使用它。
使用 Struct 在属性名拼错的时候会引发一个 NoMethodError 异常,使用哈希时不会有这个问题,因为访问非法键的时候只会返回nil,不过也意味着在之后的代码中,你将被卷入更难发现的 TypeError 异常。
11、通过在模块中嵌入代码来创建命名空间
通过在模块中嵌入代码来创建命名空间。
让你的命名空间结构和目录结构相同。
如果使用时可能出现歧义,可使用 “ :: ” 来限定顶级常量(比如:::Array)
12、理解等价的不同用法
绝不要重载 equal? 方法。该方法的预期行为是,严格比较两个对象,仅当它们同时指向内存中同一个对象时其值为真(即,当它们具有相同的 object_id 时)。
Hash 类在冲突检测时使用 eql? 方法来比较键对象。默认实现可能和你的想象不同。遵循第13条的建议之后再使用别名 eql? 来替代 “ == ” 书写更合理的 hash 方法。
使用 “ == ” 操作符来测试两个对象是否表示相同的值。有些类比如表示数字的类会有一个粗糙的等号操作符进行类型转换。
case 表达式使用 “ == ” 操作符来测试每个 when 语句的值。左操作数是 when 的参数,右操作数是 case 的参数。
13、通过 “ <=> ” 操作符实现比较和比较模块
通过定义 “ <=> ” 操作符和引入 Comparable 模块实现对象的排序。( Comparable 模块包含 “<” 、"<=" 、"==" 、">" 、">=")
如果左操作数不能与右操作数进行比较,“ <=> ” 操作符应该返回 nil。
如果要实现类的 “ <=> ” 运算符,应该考虑将 eql? 方法设置为 “ == ” 操作符的别名,特别是当你希望该类的所有实例可以被用来作为哈希键的时候,就应该重载哈希方法。
14、通过 protected 方法共享私有状态
封装是面向对象编程中的主要准则之一,它是指一个对象的内部实现仅可被内部访问,不可被外部访问。
当设计一个具有内部状态的类时,正确的做法是尽可能减少对该状态的直接访问,而是使用最小数量的访问方法。
通过protected方法共享私有状态。
一个对象的protected方法若要被显示接收者调用,除非改对象与接收者是同类对象或具有相同的定义该protected方法的超类。
15、优先使用实例变量而非类变量
Ruby 语言存在两种用 @ 标识的变量: 实例变量和类变量。
如果你的程序存在多线程控制,那么在不使用互斥锁的情况下改变任何变量都是不安全的。
优先使用实例变量而非类变量。
类也是对象,所以它们拥有自己的私有实例变量集合。
三、集合
16、在改变作为参数的集合之前复制它们
Ruby 中参数时按引用传递的,而不是值传递。这个规则有一个例外值注意: 它不适用于 Fixnum 对象。
在改变集合之前先复制它们。
dup 方法和 clone 方法只会进行浅拷贝。
对于多数对象来说,可以使用 Marshal 来完成深拷贝。
17、使用 Array 方法将 nil 及标量对象转换成数组
使用 Array 方法将 nil 及标量对象转换成数组。
不要讲哈希传给 Array 方法,它会被转化成一个嵌套数组的集合。
18、考虑使用集合高效检察元素的包含性
Ruby 自带了两套不同的类库,核心库已经被每个程序预先加载,另一套就是标准库,庞大而且不会被自动加载,在你使用的时候需要正确的引入它们。
Array 的 include? 方法的性能是最差的。当数组中的元素增加时,可以考虑转用哈希来代替。
如果你不需要元素按照某一特定顺序来排列,无需随机访问任一元素,且需要高效的检测元素的包含性,Set 类就是你需要的。
插入 Set 的对象必须也被当作哈希的键来使用。并且使用之前必须引入它。
19、了解如何通过 reduce 方法折叠集合
总是要给累加器一个初始值。
给予 reduce 的块总是要返回一个累加器。对当前累加器的修改是可行的,要记住从块中返回。
20、考虑使用默认哈希值
考虑使用默认的 Hash 值。
使用 has_key? 方法或它的任意别名来检查哈希是否包含某个键。也就是说,不要以为当访问一个不存在的键是都会返回 nil。
如果某段代码在接受哈希的非法键时会返回 nil,不要为传入该方法的哈希使用默认值。
相比使用默认值,有些时候使用 Hash#fetch 方法能更加安全。
21、对集合优先使用委托而非继承
对集合优先使用委托而非继承
不要忘记编写用来复制委托目标的 initialize_copy 方法
编写 freeze、taint 以及 untaint 方法时,先传递信息给委托目标,之后调用 super 方法。
四、异常
22、使用定制的异常而不是抛出字符串
避免使用字符串作为一场,它们会被转换成原声 RuntimeError 对象。取而代之,创建一个定制的异常类。
定制的异常类应该继承自 StandardError , 且类名应以 ’Error‘ 结尾。
当为一个工程创建了不止一个异常类时,从创建一个继承自 StandardError 的基类开始。其他的异常类应该继承自该定制的基类。
如果你对你的定制异常类编写了 initialize 方法,务必确保其调用了 super 方法,最好在调用时以错误信息作为参数。
在 initialize 方法中设置错误信息时,请牢记: 如果在 raise 方法中再度设置错误信息会覆盖原本在 initialize 中设置的那一条。
23、捕获可能的最具体的异常
只捕获那些你知道如何恢复的异常
当捕获异常时,首先处理最特殊的类型。在异常的继承关系中位置越高的,越应该排在 rescue 链的后面。
避免捕获如 StandardError 这样的通用异常。如果你已经这么做了,就应该想想你真正想做的是不是能够用 ensure 语句来实现。
在异常发生的情况下,从 rescue 语句中抛出的异常将会替换当前异常并离开当前的作用域。
24、通过块和 rescue 管理资源
通过 ensure 语句来释放任何已获得的资源。
公国在类方法上使用块和 ensure 语句讲资源管理的逻辑抽离出来。
确保 ensure 语句中使用的变量已经被初始化过了。
25、通过临近的 end 退出 ensure 语句
避免在 ensure 语句中显式使用 return 语句。这意味着方法体内存在着某些错误的逻辑。
同样,不要再 ensure 语句中直接使用 throw。你应该将 throw 放在方法主体呢。
当执行迭代时,不要再 ensure 语句中执行 next 或 break。仔细想想在迭代内到底需不需要 begin 块。将关系反转或许更加合理,就是将迭代放在 begin 块中。
一般来说,不要再 ensure 语句中改变控制流。在 resucue 语句中完成这样的工作,你的意图会更加清晰。
26、限制 retry 次数,改变重试频率并记录异常信息。
永远不要无条件 retry,要把它看做代码中的隐式循环;在代码块的外围定义重试次数,当超出最大重试次数时重新抛出异常。
retry 时记录具有审计作用的异常信息, 如果重试有问题的代码解决不了问题,需要追根溯源地去了解异常是如何发生的。
当在 retry 之前使用延时时,需要考虑增加延时避免加剧问题。
27、throw 比 raise 更适合用来跳出作用域
在复杂的流程控制中,可以考虑使用 throw 和 raise,这种方法一个额外的好处是可以把一个对象传递到上层调用栈并作为 catch 的最终返回值。
尽量使用简单的方法来控制程序结果,可以通过方法调用和 return 重写 catch 和 throw。
五、元编程
28、熟悉 Ruby 模块和类的钩子方法
所有的钩子方法都需要被定义为单例方法。
添加、删除、取消定义方法的钩子方法参数是方法名,而不是类名,如果需要,使用 self 去获取类的信息。
定义 singleton_method_added 会触发自身。
不要覆盖 extend_object、append_features 和 prepend_features 方法,使用 extended、included 和 prepended 替代。
29、在类的钩子方法中执行 super 方法
在类的钩子方法中执行 super。
30、推荐使用 define_method 而非 method_missing
define_method 优于 method_missing。
如果必须使用 method_missing,最好也定义 respond_to_missing?方法。
31、了解不同类型的 eval 间的差异
使用 instance_eval 和 instance_exec 定义的是单例方法。
class_eval、 module_eval、 class_exec 和 module_exec 方法只可以被模块或者方法使用。通过这些定义的方法都是实例方法。
32、猴子补丁
尽管 refinement 已经不再是实验性的功能,它仍然有可能被修改得更为成熟。
在不同的语法作用域,在使用 refinement 之前必须先激活它。
33、使用别名链执行被修改的方法
在设置别名链时,需要确保别领是独一无二的。
必要的时候要考虑提供一个撤销别名链的方法。
34、 支持多种 Proc 参数数量
与弱 proc 对象不同,在参数数量不匹配时,强 Proc 对象会抛出 ArgumentError 异常。
可以使用 Proc#arity 方法得到 Proc 期望的参数数量,如果返回的是正数,则意味着有多少参数时必须的。如果返回的是负数,则意味着 Proc 有些参数是可选的,可以通过 ’ ~ ‘ 来得到有多少是必须参数。
35、使用模块前置时请谨慎思考
prepend 方法在使用时对类体系结构的影响是:它将模块插入到接收者之前。这和 include 方法有很大不同:include 则是将模块插入到接收者和其超类之间。
与 included 和 extended 模块钩子一样,前置模块也会触发 prepended 钩子。
六、测试
36、熟悉单元测试工具 MiniTest
测试方法需要以 ' test_ ' 作为前缀。
简短的测试更容易理解,也更容易维护。
使用合适的断言方法生成更易读的出错信息。
断言和反演的文档在 MiniTest::Assertions 中。
37、熟悉 MiniTest 的需求测试
使用 describe 方法创建测试类,使用 it 定义测试用例。
虽然在需求说明测试中,断言仍然可用,但是更推荐使用诸如到 Object 中的期望方法。
在 MiniTest::Expectations 模块中,可以找到关于期望方法更详细的文档。
38、使用 Mock 模拟特定对象
使用 Mock 来隔离外部系统的不稳定因素。
Mock 或者替换没有被测试过的方法,有可能会让这些被 Mock 的代码在生产环境中出现问题。
请确保在测试方法代码的最后调用了 MiniTest::Mock#verify 方法。
39、力争代码被有效测试过
使用模糊测试和属性测试工具,帮助测试代码的快乐路径和异常路径。
测试覆盖率工具会给你一种虚假的安全感,因为被执行过的代码不代表这行代码是正确的。
在编写特性的同事就加上测试,会让测试容易很多。
在你开始寻找导致 bug 的根本原因之前,先写一个针对该 bug 测试。
尽可能多地自动化你的测试。
40、学会使用 Ruby 文档
ri 工具用来读取文档,rdoc 工具用来生成文档。
使用命令行选项 ’ -d doc ‘ 来为 RI 工具指定在 ’ doc ‘ 路径下查找文档。
运行 doc 时,后面跟上命令行选项 ’ -f ri ‘ 来为 RI 工具生成文档。另外,用 ’ -f darkfish ‘ 来生成 HTML 格式文档。
完整的 RDoc 文档可以在 RDoc::Markup 类中找到。
41、认识 IRB 的高级特性
利用下划线变量( ’ _ ‘ )来获取上一个表达式的结果。
irb 命令可以用来创建一个新的会话,并将当前的评估上下文改变成任意对象。
考虑 Pry gem 作为 IRB 的替代品。
42、用 Bundler 管理 Gem 依赖
在加载完 Bundler 之后,使用 Bundler,require 会牺牲一点点灵活性,但是可以加载 Gemfile 仲所有的 gem。
当开发应用时, 在 Gemfile 中列出所有的 gem,然后把 Gemfile.lock 添加到版本控制系统中。
当打包 RubyGem, 在 gem 规格文件中列出 gem 所有依赖,但不要把 Gemfile.lock 添加到你的版本系统中。
43、为 Gem 依赖设定版本上限
忽略掉版本上限需求相当于你说了你可以支持未来所有的版本。
相当于悲观版本操作符,更加倾向于使用明确的版本范围。
当公开发布一个 gem 时,致命依赖包的版本先知要求,在安全的范围内越宽越好,上线可以扩展到下一个主要发布版本之前。
八、内存管理与性能
44、熟悉 Ruby 的垃圾收集器
垃圾收集器通过维护一个由页组成的堆来管理内存。页又由槽组成,每个槽存储一个对象。
在垃圾收集过程中,可以访问的对象呗标记,而没有标记的对象将被清楚,释放槽来储存新对象。
新对象被称为年轻代对象,如果再一个垃圾收集周期后依然存活,会被升级为年老代对象,年老代对象会在次要标记阶段自动被标记为活跃,因此,只有在主要标记阶段后才能被清除。
GC::stat 方法会以散列的形式返回垃圾收集器所有的统计数据。
你可以通过设定环境变量来调优垃圾收集器,使其更实用于你的应用程序。
45、用 Finalizer 构建资源安全网
最好使用 ensure 子句来保护有限的资源。
如果必须要在 ensure 子句外暴露一个资源,那么就给它创建一个 finalizer。
永远不要再这样一个绑定中创建 finalizer Proc。改绑定引用一个注定会被销毁的对象。啫会造成垃圾收集器无法释放改对象。
记住,finalizer 可能在一个对象销毁后以及程序终止前的任何时间被调用。
46、认识 Ruby 性能分析工具
在修改性能差的代码之前,先使用性能分析工具收集性能相关的信息。
在 ruby-prof gem 和 Ruby 自带的标准 profile 库之间,选择前者,因为前者更快而且可以提供多种不同的报告。
如果使用 Ruby 2.1 或者更新的版本,应该考虑使用 stackprof gem 和 memory_profiler gem。
47、避免在循环中使用对象字面量
将循环中的不会变化的对象字面量变成常量。
在 Ruby 2.1 及更高的版本中冻结字符串字面量,相当于把它作为常量,可以被整个运行程序共享。
48、考虑从记忆化大开销计算
考虑提供一个方法通过将缓存的变量职位 nil 来重置记忆化。
确保时钟认真考虑过这些由记忆化而跳过副作用所导致的后果。
如果不希望调用者修改缓存的变量,那应该考虑让被记忆化的方法返回冻结对象。
先用工具分析程序的性能,再考虑是否需要记忆化。
网友评论