- 9.1 Scopes
- 9.2 Callbacks
- 9.3 Calculation Methods
- 9.4 Single-Table Inheritance (STI)
- 9.5 Abstract Base Model Classes
- 9.6 Polymorphic has_many Relationships
- 9.7 Enums
- 9.8 Foreign-Key Constraints
- 9.9 Modules for Reusing Common Behavior
- 9.10 Modifying Active Record Classes at Runtime
- 9.11 Using Value Objects
- 9.12 Nonpersisted Models
- 9.13 PostgreSQL Enhancements
- 9.14 Conclusion
9.10 Modifying Active Record Classes at Runtime
The metaprogramming capabilities of Ruby, combined with the after_find callback, open the door to some interesting possibilities, especially if you’re willing to blur your perception of the difference between code and data. I’m talking about modifying the behavior of model classes on the fly, as they’re loaded into your application.
Listing 9.5 is a drastically simplified example of the technique, which assumes the presence of a config column on your model. During the after_find callback, we get a handle to the unique singleton class11 of the model instance being loaded. Then we execute the contents of the config attribute belonging to this particular Account instance, using Ruby’s class_eval method. Since we’re doing this using the singleton class for this instance rather than the global Account class, other account instances in the system are completely unaffected.
Listing 9.5 Runtime Metaprogramming with after_find
1 class Account < ActiveRecord::Base 2 ... 3 4 protected 5 6 def after_find 7 singleton = class << self; self; end 8 singleton.class_eval(config) 9 end 10 end
I used powerful techniques like this one in a supply chain application that I wrote for a large industrial client. A lot is a generic term in the industry used to describe a shipment of product. Depending on the vendor and product involved, the attributes and business logic for a given lot vary quite a bit. Since the set of vendors and products being handled changed on a weekly (sometimes daily) basis, the system needed to be reconfigurable without requiring a production deployment.
Without getting into too much detail, the application allowed the maintenance programmers to easily customize the behavior of the system by manipulating Ruby code stored in the database, associated with whatever product the lot contained.
For example, one of the business rules associated with lots of butter being shipped for Acme Dairy Co. might dictate a strictly integral product code, exactly 10 digits in length. The code (stored in the database) associated with the product entry for Acme Dairy’s butter product would therefore contain the following two lines:
1 validates_numericality_of :product_code, only_integer: true 2 validates_length_of :product_code, is: 10
A relatively complete description of everything you can do with Ruby metaprogramming, and how to do it correctly, would fill its own book. For instance, you might realize that doing things like executing arbitrary Ruby code straight out of the database is inherently dangerous. That’s why I emphasize again that the examples shown here are very simplified. All I want to do is give you a taste of the possibilities.
If you do decide to begin leveraging these kinds of techniques in real-world applications, you’ll have to consider security and approval workflow and a host of other important concerns. Instead of allowing arbitrary Ruby code to be executed, you might feel compelled to limit it to a small subset related to the problem at hand. You might design a compact API or even delve into authoring a domain-specific language (DSL), crafted specifically for expressing the business rules and behaviors that should be loaded dynamically. Proceeding down the rabbit hole, you might write custom parsers for your DSL that could execute it in different contexts—some for error detection and others for reporting. It’s one of those areas where the possibilities are quite limitless.
9.10.2 Ruby and Domain-Specific Languages
My former colleague Jay Fields and I pioneered the mix of Ruby metaprogramming, Rails, and internal12 domain-specific languages while doing Rails application development for clients. I still occasionally speak at conferences and blog about writing DSLs in Ruby.
Jay has also written and delivered talks about his evolution of Ruby DSL techniques, which he calls business natural languages (or BNL for short13). When developing BNLs, you craft a domain-specific language that is not necessarily valid Ruby syntax but is close enough to be transformed easily into Ruby and executed at runtime, as shown in Listing 9.6.
Listing 9.6 Example of Business Natural Language
employee John Doe compensate 500 dollars for each deal closed in the past 30 days compensate 100 dollars for each active deal that closed more than 365 days ago compensate 5 percent of gross profits if gross profits are greater than 1,000,000 dollars compensate 3 percent of gross profits if gross profits are greater than 2,000,000 dollars compensate 1 percent of gross profits if gross profits are greater than 3,000,000 dollars
The ability to leverage advanced techniques such as DSLs is yet another powerful tool in the hands of experienced Rails developers.