Ruby Beans

Sep. 13th, 2009 09:02 pm
rbandrews: (Lambda)
[personal profile] rbandrews
I was thinking about beans a little bit this weekend.

There are two things, I think, that make Ruby special: it has a really easy parser for a structured file format built right in (YAML), and it has a meta-object protocol.

A meta-object protocol just means that Ruby is just one language, not two. See, Java is two, a class-definition language, and a function-body language.

When you're defining classes, all you can do is define things: you can add methods and declare variables, but you can't call functions or make loops. Likewise, inside functions, you can put code in loops or use variables, but you can't define methods or new classes:
class PointBean {
  private int x, y;
  public PointBean(int x, int y){
    this.x = x; // Can use variables here, but can't define more methods!
    this.y = y;
  }

  public int getX(){return x;} // Can't use a loop here!
  public int getY(){return y;}
}

So, you end up writing a lot of things twice, or making a lot of repetitive code (like getX and getY there). In Ruby, it's always Ruby, so you can use the loops and things that you use to structure your repetitive functions to also structure repetitive class definitions.

Like beans.

Java has this thing called "JavaBeans". It's not really an API, it's just sort of a standard naming scheme, something we would now call a design pattern. It works like this:

Suppose you have a class that contains a pair of numbers, x and y. As a bean, this class would look like this:
public class PointBean implements Serializable {
    private int x, y;

    public PointBean(){
	x=0; y=0;
    }

    public int getX(){return x;}
    public void setX(int new_x){x=new_x;}

    public int getY(){return y;}
    public void setY(int new_y){y=new_y;}
}
There's a getThing and a setThing for each member of the class. The naming of these methods is what makes it a bean. Java has some really basic reflection tools that things like NetBeans can use to show all these methods in a GUI; there are some other tools that are designed to work with any class that follows the bean standard.

So, I started thinking, what would the Ruby equivalent of a bean be? My first idea was this:
class Bean
  def self.create_bean *fields
    Class.new Bean do |c|
      fields.each do |field|
        c.send :attr_accessor, field
      end
    end
  end
end
This satisfies most of the bean requirements: you can create a class like this:
PointBean = Bean.create_bean :x, :y
and then use it like the Java one:
// Java:
PointBean pb = new PointBean();
pb.setX(3);
pb.getX();

# Ruby:
pb = PointBean.new
pb.x = 3
pb.x
The code is a little different to account for Ruby style, using x= instead of setX, but the concept is the same. Make a class with a getter and a setter for a specified set of fields.

Now, what we've done here is something that's simply impossible in Java. We haven't written a bean, we've written the concept of a bean. Something that can't be done in Java. You can't say "I want a class with methods shaped like this, following these naming conventions, for all these names." That's why Java beans aren't an API, they're a standard written out in a document in English.

We're not quite there yet though. A hypothetical Java programmer might complain that part of what a bean does is validate its input, at the very least that it fits certain static types. We can't make a PointBean with an x coordinate of "banana".

So, let's add that in:
class Bean
  def self.create_bean fields={}
    Class.new Bean do |c|
      fields.each do |name, type|
        c.send :attr_reader, field
        c.add_field name, type
      end
    end
  end

  def self.add_field name, type
    define_method "#{name}=" do |new_value|
      unless new_value.is_a? type
        raise "Invalid type #{new_value.class}, expected #{type}"
      else
        instance_variable_set "@#{name}", new_value
      end
    end
  end
end
Now instead of just calling attr_accessor, we call attr_reader to make just the getX() portion, then define the setX equivalent ourselves in add_field. We make the new beans like this:
PointBean = Bean.create_bean :x=>Integer, :y=>Integer
PointBean.new.x=3 # works
PointBean.new.x="foo" # fails
Very simple! And this mirrors pretty much exactly what we could do with beans in Java up until Java 1.4.

After Java 1.4, though, something big changed: imagine if we had a bean to represent a bank account. We'd have a list of transactions in the account, like this:
AccountBean = Bean.create_bean :transactions => Array
This is bad though, because we could easily put the wrong thing in that array and never know it. It might be an array of integers and we put in a string, say. Java 1.4 and earlier would have no way of catching that, and neither does our current bean system, but it's easy to add:
class Bean
  def self.type_matches? value, type
    if type.nil?
      true
    elsif type.is_a? Class
      value.is_a? type
    elsif type.is_a? Hash
      value.is_a? Hash and
        value.keys.all?{|key| Bean.type_matches?(key,type.keys.first)} and
        value.values.all?{|val| Bean.type_matches?(val,type.values.first)}
    elsif type.is_a? Enumerable
      value.is_a?(type.class) and
        value.all?{|v| Bean.type_matches?(v,type.first)}
    else
      false
    end
  end
end
We've added a type_matches? function in, that will get called in add_field, where we're calling new_value.is_a? type now. This will act the same unless the type is a hash or array, in which case it will look inside to see what the type of all the values are. So:
AccountBean = Bean.create_bean :transactions=>[Fixnum]
AccountBean.new.transactions=[3] # works
AccountBean.new.transactions="foo" # fails
AccountBean.new.transactions=["foo"] # fails
Because type_matches? is recursive, we can even go to an arbitrary depth:
AccountBean = Bean.create_bean :transactions=>[{Time => Fixnum}]
AccountBean.new.transactions = [{Time.now => 5}]
Or, even define beans containing beans:
TransactionBean = Bean.create_bean :amount=>Fixnum, :name=>String, :timestamp=>Time
AccountBean = Bean.create_bean :transactions=>[TransactionBean]
Before testing this one, let's also add in a nicer constructor. Since all Ruby beans are subclasses of the basic Bean class, we can just add this there:
class Bean
  def initialize fields={}
    fields.each do |name, value|
      send "#{name}=", value
    end
  end
end
So now, we use our two beans like this:
t = TransactionBean.new :amount=>5, :name=>"Bill", :timestamp=>Time.now
a = AccountBean.new :transactions=>[t]
Also, of course, since Ruby classes are open, we can add methods to these if we need anything other than field accessors:
class AccountBean
  def balance
    return 0 unless transactions
    transactions.map(&:amount).inject(&:'+')
  end
end

So, now we can do pretty much everything Java beans can do, and in a much cooler way: instead of a document that we wrote that we have to write repetitive boilerplate code against, we have a class that generates the boilerplate part for us. There's no need for a bean standard, because "bean" is a concept that's actually part of our program.

We've done something pretty remarkable here, too: in Java, the fact that a bean contains certain fields, and what their names and types are, is part of the code. It's immutable by other code, it's decided when the programmer first writes it. With our Ruby bean system, though, the fields and their whole type structure is just data, just a hash that we pass into the bean when we make its class. We've turned code into data.

And data can come from anywhere. Including a config file.

The second thing, other than the meta-object protocol, that makes Ruby special is that it comes out of the box able to understand structured text files, in YAML. So, we can take a file like this:
TransactionBean:
  amount: Fixnum
  name: String
  timestamp: Time

AccountBean:
  transactions: ['TransactionBean']
and we can make a function that makes beans out of it:
class Bean
  def self.create_from_file filename
    Object.module_eval do
      YAML.load(File.read(filename)).each do |name, fields|
        const_set name, Bean.create_bean(fields)
      end
    end
  end
end
And, we'll need to modify type_matches? a little bit to handle strings, because the type names will now come through as strings instead of constants:
class Bean
  def self.type_matches? value, type
    if type.nil?
      true
    elsif type.is_a? Class
      value.is_a? type
    elsif type.is_a? String
      type_matches? value, Object.module_eval("::#{type}")
    elsif type.is_a? Hash
      value.is_a? Hash and
        value.keys.all?{|key| Bean.type_matches?(key,type.keys.first)} and
        value.values.all?{|val| Bean.type_matches?(val,type.values.first)}
    elsif type.is_a? Enumerable
      value.is_a?(type.class) and
        value.all?{|v| Bean.type_matches?(v,type.first)}
    else
      false
    end
  end
end

And now we can store the structure of our data classes in a text file! And retain all the type-checking we would get from a language like Java.

In fact, without too much trouble, we could query a database for this. As in, not store the fields in a database table, but actually look up the columns that the table has, and automatically make a bean class for every table in a database. Maybe with some hints for things that are arrays or references to other objects.

That kind of thing is a huge deal in Java, whole frameworks like Spring and EJB just to deal with it, as much as can even be dealt with (eventually Java has to resort to code generators to make it work), and in Ruby it's under a page of code. Because in Ruby, it's all code, because the code can modify itself.

Anyway, I was just thinking a little bit about beans this weekend.

Profile

rbandrews: (Default)
rbandrews

July 2024

S M T W T F S
 123456
78910111213
14151617181920
212223242526 27
28293031   

Style Credit

Page generated Jun. 16th, 2025 12:40 pm
Powered by Dreamwidth Studios

Expand Cut Tags

No cut tags