`

[转]Ruby Best practice

    博客分类:
  • Ruby
 
阅读更多

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.

  1. class Point   
  2.   def initialize(x,y)   
  3.     @x@y = x,y   
  4.   end  
  5.      
  6.   attr_reader :x:y  
  7.      
  8.   def distance(point)   
  9.     Math.hypot(point.x - x, point.y - y)   
  10.   end  
  11. 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:

  1. Point = Class.new do  
  2.   def initialize(x,y)   
  3.     @x@y = x,y   
  4.   end  
  5.   
  6.   attr_reader :x:y  
  7.   
  8.   def distance(point)   
  9.     Math.hypot(point.x - x, point.y - y)   
  10.   end  
  11. 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:

  1. module Prawn   
  2.   module Errors   
  3.       
  4.      class FailedObjectConversion < StandardError; end  
  5.       
  6.      class InvalidPageLayout < StandardError; end           
  7.       
  8.      class NotOnPage < StandardError; end  
  9.   
  10.      class UnknownFont < StandardError; end      
  11.   
  12.      class IncompatibleStringEncoding < StandardError; end        
  13.   
  14.      class UnknownOption < StandardError; end  
  15.   
  16.   end  
  17. 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:

  1. module Prawn   
  2.   module Errors   
  3.      FailedObjectConversion = Class.new(StandardError)   
  4.              
  5.      InvalidPageLayout = Class.new(StandardError)        
  6.       
  7.      NotOnPage = Class.new(StandardError)   
  8.   
  9.      UnknownFont = Class.new(StandardError)   
  10.   
  11.      IncompatibleStringEncoding = Class.new(StandardError)        
  12.   
  13.      UnknownOption = Class.new(StandardError)    
  14.   
  15.   end  
  16. 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:

  1. module Prawn   
  2.   module Errors   
  3.      
  4.     exceptions = %w[ FailedObjectConversion InvalidPageLayout NotOnPage   
  5.                     UnknownFont IncompatibleStringEncoding UnknownOption ]   
  6.   
  7.     exceptions.each { |e| const_set(e, Class.new(StandardError)) }   
  8.   
  9.   end  
  10. 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:

  1. def test_reveal_should_not_bind_to_an_instance   
  2.   with_object_id = Class.new(BlankSlate) do  
  3.     reveal(:object_id)   
  4.   end  
  5.   
  6.   obj1 = with_object_id.new  
  7.   obj2 = with_object_id.new  
  8.   
  9.   assert obj1.object_id != obj2.object_id,   
  10.      "Revealed methods should not be bound to a particular instance"  
  11. 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.

  1. class FreightOffer::Rail < FreightOffer   
  2.   hard_constraint do |query|   
  3.     query[:delivery] - query[:pickup] > 5.days   
  4.   end  
  5.   
  6.   soft_constraint do |query|   
  7.     query[:asking_price] < 5000.to_money   
  8.   end  
  9. 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.

  1. class MyReport < Fatty::Formatter    
  2.   
  3.   module Helpers    
  4.     def full_name    
  5.       "#{params[:first_name]} #{params[:last_name]}"    
  6.     end    
  7.   end    
  8.      
  9.   class Txt < Fatty::Format    
  10.     include MyReport::Helpers    
  11.      def render    
  12.       "Hello #{full_name} from plain text"    
  13.      end    
  14.   end    
  15.      
  16.   # use a custom Fatty::Format subclass for extra features    
  17.   class PDF < Prawn::FattyFormat    
  18.     include MyReport::Helpers    
  19.     def render    
  20.       doc.text "Hello #{full_name} from PDF"    
  21.       doc.render    
  22.     end    
  23.   end    
  24.      
  25.   formats.update(:txt => Txt, :pdf => PDF)    
  26. 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:

  1. class MyReport < Fatty::Formatter    
  2.      
  3.   helpers do    
  4.     def full_name    
  5.       "#{params[:first_name]} #{params[:last_name]}"    
  6.     end    
  7.   end    
  8.      
  9.   format :txt do    
  10.     def render    
  11.       "Hello #{full_name} from plain text"    
  12.     end    
  13.   end    
  14.      
  15.   format :pdf:base => Prawn::FattyFormat do    
  16.     def render    
  17.       doc.text "Hello #{full_name} from PDF"    
  18.       doc.render    
  19.     end    
  20.   end    
  21.      
  22. 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:

  1. def format(name, options={}, &block)    
  2.   formats[name] = Class.new(options[:base] || Fatty::Format, &block)    
  3. 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.

  1. def helpers(helper_module=nil, &block)    
  2.   @helpers = helper_module || Module.new(&block)    
  3. 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:

  1. class Point < Struct.new(:x:y)   
  2.   def distance(point)   
  3.     Math.hypot(point.x - x, point.y - y)   
  4.   end  
  5. 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.

分享到:
评论

相关推荐

    Ruby最佳实践Ruby Best Practices

    本书旨在通过接触许多经验丰富的Rubyists所认为的常识和习惯来帮助各种技能水平的Ruby开发人员提高对语言的基本理解。

    Best of Ruby Quiz

    《Best of Ruby Quiz》是一本聚焦于Ruby编程语言的精选问答集锦,旨在帮助开发者深入理解和掌握Ruby的各种特性。这本书的描述简洁明了,"Best of Ruby Quiz"直接点出了其核心内容——一系列关于Ruby的精彩挑战和问题...

    manning.ruby.in.practice.mar.2009.pdf

    从给定的文件信息来看,这是一本名为《Ruby in Practice》的专业书籍,出版于2009年3月,由Manning Publications Co.发行。本书由多位作者合作完成,包括Jeremy McAnally、Assaf Arkin,并有Yehuda Katz、David ...

    《Best of Ruby Quiz》

    读者大多可以想出一种办法来解决这些问题,往往还能 通过思考和重构找到第二种优雅的设计,但这本书却给你列出了第三种、第四种真正精巧的解决方案——充分利用Ruby技巧才能得出的解决方案。

    RubyBestPracticesFreePdfBook.pdf 英文原版

    Ruby Best Practices – FreePdfBook

    Ruby Ruby Ruby Ruby Ruby Ruby

    Ruby Ruby Ruby Ruby Ruby Ruby

    Ruby_Practice:只是一些Ruby练习

    在这个名为"Ruby_Practice"的压缩包中,我们可以看到作者通过一系列的练习来熟悉和掌握Ruby的基本概念和特性。以下是对Ruby语言核心知识点的详细介绍: 1. **变量**: Ruby中的变量分为局部变量(以小写字母或...

    ruby-practice:回购我学习Ruby的书籍

    这个"ruby-practice"项目显然旨在帮助你实践和提高你的Ruby编程技能。在这个项目中,你可能会找到一系列的练习、代码示例或者教程,以加深对Ruby语言的理解。 在Ruby中,一切都是对象,这包括基本类型如整数、字符...

    ruby-practice:Ruby和Ecosystem的实践资料库

    "ruby-practice"项目显然是为了帮助开发者深入学习和实践Ruby语言以及其生态系统。 在Ruby的世界里,"Ecosystem"指的是围绕Ruby的各种工具、库、框架和服务。这包括RubyGems(Ruby的包管理器),Rails(一个流行的...

    Addison.Wesley.Rails.AntiPatterns.Best.Practice.Ruby.on.Rails.Refactoring

    《Rails反模式:最佳实践与Ruby on Rails重构》是一本由Chad Pytel和Tammer Saleh撰写的书籍,深入探讨了在Ruby on Rails开发过程中常见的反模式,并提供了相应的最佳实践和重构策略。本书旨在帮助开发者识别并避免...

    Ruby_practice

    "Ruby_practice"这个项目可能是一个为了帮助学习者掌握Ruby语言而设计的一系列实践练习。在这个压缩包中,"Ruby_practice-main"可能是项目的主要目录,包含了各种Ruby编程的示例和练习。 在Ruby中,你可以快速地...

    ruby_practice_bigginer:Ruby红入门

    在“ruby_practice_bigginer-master”这个项目中,你可能会遇到一系列练习,旨在帮助你逐步掌握上述概念。通过解决实际问题,你将更好地理解Ruby的工作方式,并能熟练运用到实际项目中去。在学习过程中,不断实践是...

    Best.of.Ruby.Quiz

    《Best.of.Ruby Quiz》是一本专为Ruby初学者精心编写的英文教程,旨在帮助新手逐步掌握这门强大而优雅的编程语言。Ruby作为一种动态、面向对象的脚本语言,因其简洁的语法和强大的元编程能力,深受开发者的喜爱。...

    ruby DBI ruby DBI ruby DBI

    ruby DBI ruby DBI ruby DBIruby DBI ruby DBI ruby DBIruby DBI ruby DBI ruby DBIruby DBI ruby DBI ruby DBIruby DBI ruby DBI ruby DBIruby DBI ruby DBI ruby DBIruby DBI ruby DBI ruby DBIruby DBI ruby DBI ...

Global site tag (gtag.js) - Google Analytics