[转]Ruby Best practice

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)