If you’ve worked with Ruby for at least a little while, you might already know that classes in Ruby are objects themselves, in particular, instances of Class. Before I get into the fun stuff, let’s quickly recap what that means.
Here’s the ordinary way we define classes, as you all have seen.
- class Point
- def initialize(x,y)
- @x, @y = x,y
- end
- attr_reader :x, :y
- def distance(point)
- Math.hypot(point.x - x, point.y - y)
- end
- end
class Point def initialize(x,y) @x, @y = x,y end attr_reader :x, :y def distance(point) Math.hypot(point.x - x, point.y - y) end end
The interesting thing is that the previous definition is essentially functionally equivalent to the following:
- Point = Class.new do
- def initialize(x,y)
- @x, @y = x,y
- end
- attr_reader :x, :y
- def distance(point)
- Math.hypot(point.x - x, point.y - y)
- end
- end
Point = Class.new do def initialize(x,y) @x, @y = x,y end attr_reader :x, :y def distance(point) Math.hypot(point.x - x, point.y - y) end end
This always struck me as a beautiful design decision in Ruby, because it reflects the inherent simplicity of the object model while exposing useful low level hooks for us to use. Building on this neat concept of anonymously defined classes, I’ll share a few tricks that have been useful to me in real projects.
Cleaner Exception Definitions
This sort of code has always bugged me:
- module Prawn
- module Errors
- class FailedObjectConversion < StandardError; end
- class InvalidPageLayout < StandardError; end
- class NotOnPage < StandardError; end
- class UnknownFont < StandardError; end
- class IncompatibleStringEncoding < StandardError; end
- class UnknownOption < StandardError; end
- end
- end
module Prawn module Errors class FailedObjectConversion < StandardError; end class InvalidPageLayout < StandardError; end class NotOnPage < StandardError; end class UnknownFont < StandardError; end class IncompatibleStringEncoding < StandardError; end class UnknownOption < StandardError; end end end
Although there are valid reasons for subclassing a StandardError, typically the only reason I am doing it is to get a named exception to rescue. I don’t plan to ever add any functionality to its subclass beyond a named constant. However, if we notice that Class.new can be used to create subclasses, you can write something that more clearly reflects this intention:
- module Prawn
- module Errors
- FailedObjectConversion = Class.new(StandardError)
- InvalidPageLayout = Class.new(StandardError)
- NotOnPage = Class.new(StandardError)
- UnknownFont = Class.new(StandardError)
- IncompatibleStringEncoding = Class.new(StandardError)
- UnknownOption = Class.new(StandardError)
- end
- end
module Prawn module Errors FailedObjectConversion = Class.new(StandardError) InvalidPageLayout = Class.new(StandardError) NotOnPage = Class.new(StandardError) UnknownFont = Class.new(StandardError) IncompatibleStringEncoding = Class.new(StandardError) UnknownOption = Class.new(StandardError) end end
This feels a bit nicer to me, because although it’s somewhat clear that these are still subclasses, I don’t have an ugly empty class definition that will never be filled. Of course, we could make this more DRY if we mix in a little const_set hackery:
- module Prawn
- module Errors
- exceptions = %w[ FailedObjectConversion InvalidPageLayout NotOnPage
- UnknownFont IncompatibleStringEncoding UnknownOption ]
- exceptions.each { |e| const_set(e, Class.new(StandardError)) }
- end
- end
module Prawn module Errors exceptions = %w[ FailedObjectConversion InvalidPageLayout NotOnPage UnknownFont IncompatibleStringEncoding UnknownOption ] exceptions.each { |e| const_set(e, Class.new(StandardError)) } end end
Even with this concise definition, you’ll still get the functionality you’d expect:
>> Prawn::Errors::IncompatibleStringEncoding.ancestors => [Prawn::Errors::IncompatibleStringEncoding, StandardError, Exception, Object, Kernel] >> raise Prawn::Errors::IncompatibleStringEncoding, "Bad string encoding" Prawn::Errors::IncompatibleStringEncoding: Bad string encoding from (irb):33 from :0
You can even take it farther than this, dynamically building up error classes by a naming convention. If that sounds interesting to you and you don’t mind the fuzziness of a const_missing hook, you’ll want to check out James Gray’s blog post “Summoning Error Classes As Needed”. Though I tend to be a bit more conservative, there are definitely certain places where a hack like this can come in handy.
The drawback of using any of these methods discussed here is that they don’t really play well with RDoc. However, there are easy ways to work around this, and James details some of them in his post. In general, it’s a small price to pay for greater clarity and less work in your code.
Continuing on the theme of dynamically building up subclasses, we can move on to the next trick.
Safer Class Level Unit Testing
In my experience, it’s a little bit tricky to test code that maintains state at the class level. I’ve tried everything from mocking out calls, to creating a bunch of explicit subclasses, and even have done something like klass = SomeClass.dup in my unit tests. While all of these solutions may do the trick depending on the context, there is a simple and elegant way that involves (you guessed it) Class.new.
Here’s a quick example from a patch I submitted to the builder library that verifies a fix I made to the BlankSlate class:
- def test_reveal_should_not_bind_to_an_instance
- with_object_id = Class.new(BlankSlate) do
- reveal(:object_id)
- end
- obj1 = with_object_id.new
- obj2 = with_object_id.new
- assert obj1.object_id != obj2.object_id,
- "Revealed methods should not be bound to a particular instance"
- end
def test_reveal_should_not_bind_to_an_instance with_object_id = Class.new(BlankSlate) do reveal(:object_id) end obj1 = with_object_id.new obj2 = with_object_id.new assert obj1.object_id != obj2.object_id, "Revealed methods should not be bound to a particular instance" end
Notice here that although we are doing class level logic, this test is still atomic and does not leave a mess behind in its wake. We get to use the real reveal() method, which means we don’t need to think up the mocking interface. Because we only store this anonymous subclass in a local variable, we know that our other tests won’t be adversely affected by it, it’ll just disappear when the code falls out of scope. The code is clear and expressive, which is a key factor in tests.
If you’re looking for more examples like this, you’ll want to look over the rest of the BlankSlate test case . I just followed Jim Weirich’s lead here, so you’ll see a few other examples that use a similar trick.
For those interested, the thing that sparked my interest in writing this article today was a tiny bit of domain specific code I had cooked up for my day to day work, which needed to implement a nice interface for filtering search queries at the class level before they were executed.
- class FreightOffer::Rail < FreightOffer
- hard_constraint do |query|
- query[:delivery] - query[:pickup] > 5.days
- end
- soft_constraint do |query|
- query[:asking_price] < 5000.to_money
- end
- end
class FreightOffer::Rail < FreightOffer hard_constraint do |query| query[:delivery] - query[:pickup] > 5.days end soft_constraint do |query| query[:asking_price] < 5000.to_money end end
While it’s easy to test these constraints once they’re set up for a particular model, I wanted to be able to test drive the constraint interface itself at the abstract level. The best way I could come up with was to write tests that used Class.new to generate subclasses of FreightOffer to test against, which worked well. While I won’t post those tests here, it’s a good exercise if you want to get the feel for this technique.
The next trick is a little bit different than the first two, but can really come in handy when you want to inject a little syntactic diabetes into the mix.
Parameterized Subclassing
You might recognize the following example, since it is part of the sample chapter of Ruby Best Practices . More than just a shameless plug though, I think this particular example shows off an interesting technique for dynamically building up subclasses in a low level system.
Let’s start with some vanilla code and see how we can clean it up, then we’ll finally take a look under the hood. What follows is a Fatty::Formatter, which abstracts the task of producing output in a number of different formats.
- class MyReport < Fatty::Formatter
- module Helpers
- def full_name
- "#{params[:first_name]} #{params[:last_name]}"
- end
- end
- class Txt < Fatty::Format
- include MyReport::Helpers
- def render
- "Hello #{full_name} from plain text"
- end
- end
- # use a custom Fatty::Format subclass for extra features
- class PDF < Prawn::FattyFormat
- include MyReport::Helpers
- def render
- doc.text "Hello #{full_name} from PDF"
- doc.render
- end
- end
- formats.update(:txt => Txt, :pdf => PDF)
- end
class MyReport < Fatty::Formatter module Helpers def full_name "#{params[:first_name]} #{params[:last_name]}" end end class Txt < Fatty::Format include MyReport::Helpers def render "Hello #{full_name} from plain text" end end # use a custom Fatty::Format subclass for extra features class PDF < Prawn::FattyFormat include MyReport::Helpers def render doc.text "Hello #{full_name} from PDF" doc.render end end formats.update(:txt => Txt, :pdf => PDF) end
The MyReport class in the previous code sample is little more than a glorified “Hello World” example that outputs text and PDF. It is pretty easy to follow and doesn’t really do anything fancy. However, we can certainly clean it up and make it look better:
- class MyReport < Fatty::Formatter
- helpers do
- def full_name
- "#{params[:first_name]} #{params[:last_name]}"
- end
- end
- format :txt do
- def render
- "Hello #{full_name} from plain text"
- end
- end
- format :pdf, :base => Prawn::FattyFormat do
- def render
- doc.text "Hello #{full_name} from PDF"
- doc.render
- end
- end
- end
class MyReport < Fatty::Formatter helpers do def full_name "#{params[:first_name]} #{params[:last_name]}" end end format :txt do def render "Hello #{full_name} from plain text" end end format :pdf, :base => Prawn::FattyFormat do def render doc.text "Hello #{full_name} from PDF" doc.render end end end
I think you’ll agree that this second sample induces the kind of familiar sugar shock that Ruby coders know and love. It accomplishes the same goals and actually wraps the lower level code rather than replacing it. But is there some sort of dark magic behind this? Let’s peek behind the curtain:
- def format(name, options={}, &block)
- formats[name] = Class.new(options[:base] || Fatty::Format, &block)
- end
def format(name, options={}, &block) formats[name] = Class.new(options[:base] || Fatty::Format, &block) end
As you can see, it is nothing more than a hash of formats maintained at the class level, combined with a call to Class.new to generate the subclass. No smoke and mirrors, just the same old Ruby tricks we’ve been using throughout this article. The curious may wonder how helpers() works here, and though it’s slightly tangential, you’ll see it is similarly simple.
- def helpers(helper_module=nil, &block)
- @helpers = helper_module || Module.new(&block)
- end
def helpers(helper_module=nil, &block) @helpers = helper_module || Module.new(&block) end
Though I won’t get into it here, fun tricks can be done with anonymous modules as well. Can you think of any that are particularly interesting? If so, let me know in the comments.
Although I only showed a part of the picture, you might want to check out the rest of Fatty’s source. I call it a ‘67 line replacement for Ruport’, which is a major exaggeration, but it is really surprising how much you can get out of a few dynamic Ruby tricks when you combine them together properly. A lot of these ideas were actually inspired by the Sinatra web framework, so that’s another project to add to your code reading list if you’re looking to learn new tricks.
Anyway, I’m getting off topic now, and it’s about time to wrap up anyway. I’ll go on one more tiny tangent, and then send you on your way.
Shortcutting with Struct
With all this talk about using anonymous sub-classes, I can’t help but recap one of the oldest tricks in the book. The method Struct.new can be handy for shortcutting class creation. Using it, the very first example in this post could be simplified down to:
- class Point < Struct.new(:x, :y)
- def distance(point)
- Math.hypot(point.x - x, point.y - y)
- end
- end
class Point < Struct.new(:x, :y) def distance(point) Math.hypot(point.x - x, point.y - y) end end
The constructor and accessors are provided for free by Struct here. Of course, the real reason I mentioned this was to get you thinking. If Struct.new can work this way, and we know that Class.new can accept a block definition that provides a closure, what other cool uses of parameterized subclasses can we cook up?
Please Share Your Thoughts
I wrote this article in hopes that it’ll be a jumping off point for discussion on more cool tricks I haven’t seen before, or failing that, ones that other readers haven’t seen before. While there certainly is a lot of nasty, scary looking “meta-programming” out there that makes people feel like this stuff is hard and esoteric, there are many techniques that lead to simple, elegant, and beautiful dynamic code. Please let me know what you think of these techniques, and definitely share one of your own if you’d like.
相关推荐
本书旨在通过接触许多经验丰富的Rubyists所认为的常识和习惯来帮助各种技能水平的Ruby开发人员提高对语言的基本理解。
《Best of Ruby Quiz》是一本聚焦于Ruby编程语言的精选问答集锦,旨在帮助开发者深入理解和掌握Ruby的各种特性。这本书的描述简洁明了,"Best of Ruby Quiz"直接点出了其核心内容——一系列关于Ruby的精彩挑战和问题...
从给定的文件信息来看,这是一本名为《Ruby in Practice》的专业书籍,出版于2009年3月,由Manning Publications Co.发行。本书由多位作者合作完成,包括Jeremy McAnally、Assaf Arkin,并有Yehuda Katz、David ...
读者大多可以想出一种办法来解决这些问题,往往还能 通过思考和重构找到第二种优雅的设计,但这本书却给你列出了第三种、第四种真正精巧的解决方案——充分利用Ruby技巧才能得出的解决方案。
Ruby Best Practices – FreePdfBook
Ruby Ruby Ruby Ruby Ruby Ruby
在这个名为"Ruby_Practice"的压缩包中,我们可以看到作者通过一系列的练习来熟悉和掌握Ruby的基本概念和特性。以下是对Ruby语言核心知识点的详细介绍: 1. **变量**: Ruby中的变量分为局部变量(以小写字母或...
这个"ruby-practice"项目显然旨在帮助你实践和提高你的Ruby编程技能。在这个项目中,你可能会找到一系列的练习、代码示例或者教程,以加深对Ruby语言的理解。 在Ruby中,一切都是对象,这包括基本类型如整数、字符...
"ruby-practice"项目显然是为了帮助开发者深入学习和实践Ruby语言以及其生态系统。 在Ruby的世界里,"Ecosystem"指的是围绕Ruby的各种工具、库、框架和服务。这包括RubyGems(Ruby的包管理器),Rails(一个流行的...
《Rails反模式:最佳实践与Ruby on Rails重构》是一本由Chad Pytel和Tammer Saleh撰写的书籍,深入探讨了在Ruby on Rails开发过程中常见的反模式,并提供了相应的最佳实践和重构策略。本书旨在帮助开发者识别并避免...
"Ruby_practice"这个项目可能是一个为了帮助学习者掌握Ruby语言而设计的一系列实践练习。在这个压缩包中,"Ruby_practice-main"可能是项目的主要目录,包含了各种Ruby编程的示例和练习。 在Ruby中,你可以快速地...
在“ruby_practice_bigginer-master”这个项目中,你可能会遇到一系列练习,旨在帮助你逐步掌握上述概念。通过解决实际问题,你将更好地理解Ruby的工作方式,并能熟练运用到实际项目中去。在学习过程中,不断实践是...
本资源是ruby代码,提供了一系列封装好的函数,用于快速进行转换,一个函数搞定,包括如下转换,二进制字符串与hex字符串的互转。二进制字符串与整数互转,包括uint8,uin16,uint32, 以及本地字节序和网络字节序两种...
《Best.of.Ruby Quiz》是一本专为Ruby初学者精心编写的英文教程,旨在帮助新手逐步掌握这门强大而优雅的编程语言。Ruby作为一种动态、面向对象的脚本语言,因其简洁的语法和强大的元编程能力,深受开发者的喜爱。...