Ruby Best Practices - The Complete Class

A remark: we enabled comment moderation because the blog was recently target of spam. You probably have not seen much of it because we were pretty quick in removing it manually. So if your comment does not show up please be patient.

There are some basic concepts (often called “aspects”) that need to be implemented for many classes although not all classes need all (or even any) of them:

  • initialization
  • conversion to a printable string
  • equivalence
  • hash code calculation
  • comparability
  • cloning (clone and dup )
  • freezing
  • customized persistence (Marshal and Yaml )
  • matching
  • math and operator overloading

Which of these is needed for a particular class depends of course completely on the circumstances. Classes which are never written to disk or used in a DRb context will not need customized persistence handling. For other classes there might not be a reasoable ordering.

We will look at these concepts individually in subsequent sections. For the sake of this presentation I will create a slightly artificial class which will have particular properties in order to be able to show all the concepts:

  • mutable fields
  • redundant fields, i.e. fields which carry cached values that can be derived from other fields
  • at least two fields for ordering priorities

Class Album implements a music album with a title, interpret, sequence of tracks and a fixed pause duration between tracks. I picked a slightly different approach than Eric in his article in two ways:

  • Eric has a stronger focus on collaboration with standard library classes while my guiding question was “what does a class need to be complete and consistent?”,
  • I present all the features in a single class to show how aspects play together.

You will likely never implement all these aspects in a single class. Even for those aspects you do use, you might not use the same implementations I will present. That’s OK. The implementations presented in this article are intended to cover aspects thoroughly even though you will not always have to do that in practice. For example, certain code is there in order to make the class work properly as part of an inheritance hierarchy. If you write a one off class for a script which is never intended for inheritance you can simplify many of the presented methods.

I left out the topic of math and operator overloading since that does not mix well with the concept of music album. Instead I will cover that in the next article in the blog which will present a class that shows how to override operators in Ruby and that plays well with Ruby’s built in numeric classes.

Initialization

Implementing method initialize is typically one of the first things I do when implementing a new class unless I can use the default implementation of Struct . There are a few things though that are worth considering.

First of all, who owns arguments to initialize ? It is important to make clear what happens to arguments that are provided to the call to new . There are three cases

  1. the value is immutable (like Fixnum ) or
  2. the caller retains ownership or
  3. ownership is transferred to the new instance.

Case 1 is the simple one: basically you do not need care thinking about who owns the object as there are no bad effects which can be caused by aliasing . If the instance is mutable these effects can show up. If you want to make your code as robuts as possible you must ensure you got your own copy of the instance (typically via copying it, for example by using dup ). The downside of this is of course that this costs performance as you’ll likely copy too many objects. In practice you will probably most of the time do nothing special and keep the code as simple as an assignment.

  1. class  Album  
  2.   
  3.   def  initialize(title, interpret, pause = 0)  
  4.     super ()  
  5.   
  6.     # main properties   
  7.     self .title = title  
  8.     self .interpret = interpret  
  9.     @tracks  = []  
  10.   
  11.     # additional properties   
  12.     self .pause = pause  
  13.   
  14.     # redundant properties   
  15.     # left out   
  16.   end   
  17.   
  18.   def  title=(t)  
  19.     @title  = t.dup.freeze  
  20.   end   
  21.   
  22.   def  interpret=(i)  
  23.     @interpret  = i.dup.freeze  
  24.   end   
  25.   
  26.   def  pause=(time)  
  27.     @pause  = time  
  28.     @duration  =  nil   
  29.   end   
  30.   
  31. end   
class Album

  def initialize(title, interpret, pause = 0)
    super()

    # main properties
    self.title = title
    self.interpret = interpret
    @tracks = []

    # additional properties
    self.pause = pause

    # redundant properties
    # left out
  end

  def title=(t)
    @title = t.dup.freeze
  end

  def interpret=(i)
    @interpret = i.dup.freeze
  end

  def pause=(time)
    @pause = time
    @duration = nil
  end

end

The other important aspect is inheritance. Most classes that are written probably do not have a super in their initialize method. If there are chances that you reopen the class and add mixin modules you should include super() right from the start because even if you only implicitly inherit Object and initialize in Object does nothing you may later reopen the class and add a mixin module which has an initialize method itself. If you inherit another class than Object you should explicitly mention arguments with super and not rely on having the same argument list as the super class initializer. That way you are making the call more explicit and are robust against changes in your initialize method.

  1. class  CdAlbum < Album  
  2.   attr_accessor :bar_code   
  3.   
  4.   def  initialize(t, i, code)  
  5.     super (t, i, 2)  
  6.   
  7.     self .bar_code = code  
  8.   end   
  9. end   
class CdAlbum < Album
  attr_accessor :bar_code

  def initialize(t, i, code)
    super(t, i, 2)

    self.bar_code = code
  end
end

While we’re at it: if you write a module which is intended as mixin and needs initialization itself the initializer should simply pass on all arguments in order to be compatible with arbitrary inheritance chains:

  1. module  AnotherMixin  
  2.   # Prepare internal state and pass on all   
  3.   # arguments to the super class   
  4.   def  initialize(*a, &b)  
  5.     super   
  6.     @list  = []  
  7.   end   
  8. end   
module AnotherMixin
  # Prepare internal state and pass on all
  # arguments to the super class
  def initialize(*a, &b)
    super
    @list = []
  end
end

Conversion to a printable String

Often it is desirable to be able to convert an instance to a human readable string. Which representation is most appropriate depends on the uses of the class. One policy is to create a string representation so the instance can be later reconstructed given this string as is the case for all the numeric types from the standard library. In the case of our sample class we will provide all interesting information:

  1. def  to_s  
  2.   "Album '#{title}' by '#{interpret}' (#{tracks.size} tracks)"   
  3. end   
  def to_s
    "Album '#{title}' by '#{interpret}' (#{tracks.size} tracks)"
  end

If you want to reuse a string field as external representation you could copy it in order to avoid bad effects from aliasing. However, since typically to_s is invoked for printing and no references are held I would say most of the time it is safe to not explicitly copy the field in these cases.

Equivalence

There are two methods that deal with object equivalence eql? and == . (Method equal? tests for object identity and should not be overridden.) Some core classes do have different equivalence relations implemented:

  1. irb(main):003:0> 2 == 2.0  
  2. => true   
  3. irb(main):004:0> 2.eql? 2.0  
  4. => false   
irb(main):003:0> 2 == 2.0
=> true
irb(main):004:0> 2.eql? 2.0
=> false

But most of the time both methods will implement the same equivalence relation. This also helps avoid confusion. Note that eql? is special because it is used by Hash and Set to test for instance equivalence. We will come to that in a minute when we look at hash code calculation.

Equivalence of instances must be tested against significant fields and should ignore redundant fields. Looking at derived field values does not add anything to equivalence and makes the process slower at best.

  1. def  eql?(album)  
  2.   self . class .equal?(album. class ) &&  
  3.     title == album.title &&  
  4.     interpret == album.interpret &&  
  5.     tracks == album.tracks  
  6. end   
  7.   
  8. alias  == eql?  
  def eql?(album)
    self.class.equal?(album.class) &&
      title == album.title &&
      interpret == album.interpret &&
      tracks == album.tracks
  end

  alias == eql?

Now here you see a test for class identity which you likely have not seen in other classes. Why is it there? Mathematical equivalence is a symmetric relation which means that if a.eql? b returns true so should b.eql? a . Implementing eql? and == that way also helps prevent strange effects when working with Hash instances. So if you are going to compare two instances of a class and a subclass then you might get true when called on the super class instance and false on the subclass instance. The same happens for two completely unrelated classes where the set of fields of one instance is a subset of those of the other instance. You may actually be tricked into thinking they are equivalent because the common fields are equivalent while the two instances represent completely different concepts. The only way to remedy this is to check for identity of the class.

The reason that omitting the class identity check does not cause issues most of the time is simple: usually you stuff uniform instances into a Hash or Set and even if you mix different classes most of the time they will have different fields and different field values. Still it is good to remember the point in case you experience unexpected effects with Hash keys.

Note that I omitted the test for self identity which you might be used to from Java classes. I believe this is a premature optimization because most of the time you are going to test different instances for equivalence so most of the time you pay the penalty of the failing identity check and win only in rare circumstances.

Hash Code Calculation

This topic is closely related to instance equivalence: classes Hash , Array , Set and other core classes rely on the fact that equivalent instances also return the same hash code. Note that this is not symmetric: instances wich have the same hash code may actually not be equivalent. But if the hash code differs they must not be equivalent.

An instance’s hash code should be based on the same fields that are used for determining equivalence. Our class Album has more fields and it is advisable to do some bit operations (often involving XOR ) to combine all member hash codes into a single value in order to ensure better diversity of these values. If you base the hash code only on a single field you increase the likelyhood that non equivalent instances fall into the same bucket of the Hash which makes additional equivalence checks via eql? necessary. You can find more about how hash tables work on Wikipedia .

  1. def  hash  
  2.   title.hash ^  
  3.     interpret.hash ^  
  4.     tracks.hash  
  5. end   
  def hash
    title.hash ^
      interpret.hash ^
      tracks.hash
  end

Comparability

Many classes have a natural ordering such as integers which are ordered by their numeric value. If your class does also have a natural order you can implement operator <=> and include module Comparable to get implementations of < , <= etc. for free.

  1. include Comparable  
  2.   
  3. def  <=>(o)  
  4.   self . class  == o. class  ?  
  5.     (interpret <=> o.interpret).nonzero? ||  
  6.     (title <=> o.title).nonzero? ||  
  7.     (tracks <=> o.tracks).nonzero? ||  
  8.     (pause <=> o.pause) ||   
  9.     0 :  
  10.     nil   
  11. end   
  include Comparable

  def <=>(o)
    self.class == o.class ?
      (interpret <=> o.interpret).nonzero? ||
      (title <=> o.title).nonzero? ||
      (tracks <=> o.tracks).nonzero? ||
      (pause <=> o.pause) || 
      0 :
      nil
  end

Cloning

When cloning or duping an instance the default mechanism sets all fields of the new instance to refer to the same objects as the cloned instance. While this is not an issue with immutable instances (e.g. Fixnum or instances which are frozen) bad things can happen if you have a mutable instance (for example Array or String ) which is suddenly referenced by two instances which both believe they are the sole owner of it. All bad effects can happen including violation of class invariants and the instance state likely becomes inconsistent. Do deal with such cases even for shallow copies such as #clone and #dup you need to copy a bit more. Fortunately Ruby provides a hook (#initialize_copy ) which is invoked after the instance has been copied and which can make appropriate adjustments. In our case we only need top copy the Array of Track instances because all other fields are immutable:

  1. def  initialize_copy(source)  
  2.   super   
  3.   @tracks  =  @tracks .dup  
  4. end   
  def initialize_copy(source)
    super
    @tracks = @tracks.dup
  end

There is a recent discussion on comp.lang.ruby that started out with the subject of deep cloning but uncovered some general aspects of cloning.

Freezing

For freezing similar reasoning applies as for cloning: immutable fields need no additional attention as you cannot change those objects anyway. For others you need to decide how deep you want the freeze to go. In case of class Album we certainly want to prevent addition of more tracks after an album has been frozen. We also explicitly trigger calculation of duration so to avoid errors when accessing the duration on the frozen instance; the value cannot change any more anyway. This also makes it important to invoke super as last method.

  1. def  freeze  
  2.   # unless frozen?   
  3.   @tracks .freeze   
  4.   duration  
  5.   # end   
  6.   
  7.   super   
  8. end   
  def freeze
    # unless frozen?
    @tracks.freeze 
    duration
    # end

    super
  end

Note that the code is robust against multiple invocations of #freeze because duration is calculated only once (on first transition from unfrozen to frozen). If you have more complex calculations going on that involve state changes of the instance you should place unless frozen? and end around the custom freeze code.

Custom Persistence

If you want to serialize the complete state of your instance there is nothing more to do: you can simply use Marshal and YAML from scratch. However, if you have redundant data (such as field duration in our case) which you want to omit from the serialization you have to adjust the process yourself.

For Marshal the proper approach is to use the newer approach which invoves writing methods marshal_dump and marshal_load . The former is supposed to return something which is serialized instead of the current instance and the latter is invoked on a new empty object and handed the deserialized object so fields can be initialized properly.

  1. def  marshal_dump  
  2.   a = (super   rescue  [])  
  3.   a.push(@title@interpret@pause@tracks )  
  4. end   
  5.   
  6. def  marshal_load(dumped)  
  7.   super   rescue   nil   
  8.   @title@interpret@pause@tracks  = *dumped.shift(4)  
  9. end   
  def marshal_dump
    a = (super rescue [])
    a.push(@title, @interpret, @pause, @tracks)
  end

  def marshal_load(dumped)
    super rescue nil
    @title, @interpret, @pause, @tracks = *dumped.shift(4)
  end

For YAML it is even simpler: you basically just need to ensure to_yaml_properties returns a list of symbols containing only those members that you want serialized.

  1. def  to_yaml_properties  
  2.   a = super   
  3.   a.delete :@duration   
  4.   a  
  5. end   
  def to_yaml_properties
    a = super
    a.delete :@duration
    a
  end

Both approaches do have their own strengths: with Marshal it is easier to completely replace an object with something else. While that can be done with YAML as well it is not as simple and elegant as in the case of Marshal . YAML on the other hand shines because you need to override only a single method.

One word about the implementations: while these methods could look simpler I picked an approach which also works when inheritance comes into play. You can repeat the pattern of marshal_dump , marshal_load and to_yaml_properties throughout a class hierarchy and still have each method only deal with fields of the class in which it is defined. That makes it easier to deal with later additions or removals of fields in some class in the hierarchy. This is even more so important when dealing with mixin modules, which can happen on a per instance basis (via #extend ).

Matching

I use the term “matching” for the functionality of the three equals operator (=== ) and for the matching operator (=~ ). The first is used in case statements and with Array#grep while the latter is usually used only explicitly.

The semantic is completely up to the class at hand and there are no general guidelines that could be given. Implement it when it is reasonable for your class. If you want to elegantly use instances of your class in case expressions you have to implement === doing something meaningful.

  1. # Check whether we have that track or track title by title.   
  2. def  ===(track_or_title)  
  3.   t = (track_or_title.title rescue  track_or_title)  
  4.   tracks.find {|tr| t == tr.title}  
  5. end   
  6.   
  7. # Match title against regular expression.   
  8. def  =~(rx)  
  9.   rx =~ title  
  10. end   
  # Check whether we have that track or track title by title.
  def ===(track_or_title)
    t = (track_or_title.title rescue track_or_title)
    tracks.find {|tr| t == tr.title}
  end

  # Match title against regular expression.
  def =~(rx)
    rx =~ title
  end

Summary

Today we looked at a class which implements various common aspects that you will meet over and over again when coding Ruby. Many of these are in fact present in other programming languages as well: Java has serialization, equals and hash code calculation. C++ also has an equivalence operator and other operators that can be overloaded to implement ordering etc. You can find the full code of the class at github.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
东南亚位于我国倡导推进的“一带一路”海陆交汇地带,作为当今全球发展最为迅速的地区之一,近年来区域内生产总值实现了显著且稳定的增长。根据东盟主要经济体公布的最新数据,印度尼西亚2023年国内生产总值(GDP)增长5.05%;越南2023年经济增长5.05%;马来西亚2023年经济增速为3.7%;泰国2023年经济增长1.9%;新加坡2023年经济增长1.1%;柬埔寨2023年经济增速预计为5.6%。 东盟国家在“一带一路”沿线国家中的总体GDP经济规模、贸易总额与国外直接投资均为最大,因此有着举足轻重的地位和作用。当前,东盟与中国已互相成为双方最大的交易伙伴。中国-东盟贸易总额已从2013年的443亿元增长至 2023年合计超逾6.4万亿元,占中国外贸总值的15.4%。在过去20余年中,东盟国家不断在全球多变的格局里面临挑战并寻求机遇。2023东盟国家主要经济体受到国内消费、国外投资、货币政策、旅游业复苏、和大宗商品出口价企稳等方面的提振,经济显现出稳步增长态势和强韧性的潜能。 本调研报告旨在深度挖掘东南亚市场的增长潜力与发展机会,分析东南亚市场竞争态势、销售模式、客户偏好、整体市场营商环境,为国内企业出海开展业务提供客观参考意见。 本文核心内容: 市场空间:全球行业市场空间、东南亚市场发展空间。 竞争态势:全球份额,东南亚市场企业份额。 销售模式:东南亚市场销售模式、本地代理商 客户情况:东南亚本地客户及偏好分析 营商环境:东南亚营商环境分析 本文纳入的企业包括国外及印尼本土企业,以及相关上下游企业等,部分名单 QYResearch是全球知名的大型咨询公司,行业涵盖各高科技行业产业链细分市场,横跨如半导体产业链(半导体设备及零部件、半导体材料、集成电路、制造、封测、分立器件、传感器、光电器件)、光伏产业链(设备、硅料/硅片、电池片、组件、辅料支架、逆变器、电站终端)、新能源汽车产业链(动力电池及材料、电驱电控、汽车半导体/电子、整车、充电桩)、通信产业链(通信系统设备、终端设备、电子元器件、射频前端、光模块、4G/5G/6G、宽带、IoT、数字经济、AI)、先进材料产业链(金属材料、高分子材料、陶瓷材料、纳米材料等)、机械制造产业链(数控机床、工程机械、电气机械、3C自动化、工业机器人、激光、工控、无人机)、食品药品、医疗器械、农业等。邮箱:market@qyresearch.com
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值