The repetitive branches in your code tell me your class could use a little refactoring to remove the need for multiple if-statements. It sounds like your class needs to delegate to another class for specific functionality. While I don't know that your class looks like, or it's intent, the following is an example that you could apply to your code so that you don't need to generate dynamic methods at all.
The hypothetical Order class with repetitive if-statements
Consider this Order class with multiple, similar looking if-statements:
class Order
attr_accessor :order_type
def discount_amount
if order_type == 1
.2
elsif order_type == 2
.5
else
0
end
end
def discount_end_date
if order_type == 1
DateTime.new(2014, 12, 31)
elsif order_type == 2
DateTime.new(2014, 3, 31)
else
# Always expires 100 years from now
DateTime.new(DateTime.now.year + 100, 1, 1)
end
end
end
We have three discounts: 20% off that expires end of 2014; 50% which expires end of March, 2014. Finally the default discount of 0% always expires 100 years in the future. Let's clean this up to remove the if-statments, and instead delegate to a Discount class for these calculations.
Refactor the Order class to utilize delegate methods
First up, let's clean up the Order class, then we will implement a Discount class:
class Order
attr_accessor :order_type
def discount
@discount ||=
if order_type == 1
Discount.twenty_percent_off
elsif order_type == 2
Discount.half_off
else
Discount.default_discount
end
end
def discount_amount
discount.amount
end
def discount_end_date
discount.end_date
end
end
Nice and clean. An Order object needs a Discount object to get the discount amount and end date. The Order class is now virtually infinitely extensible since the logic for calculating discounts is off-loaded to another class entirely. The Order#order_type value determines the discount. Now, let's define our Discount class.
Implementing the Discount class
According to our (fake) business rules, only three discounts exist:
- 20%-off, expiring end of 2014
- 50%-off, expiring end of March, 2014
- 0%-off (no discount) which always expires 100 years from today, essentially meaning it never expires
We don't want people to create arbitrary discounts, so let's limit our Discount instances to only those that we define by using a private constructor, then declaring static methods for each kind of discount:
class Discount
private_class_method :new
def self.default_discount
@@default_discount ||= new(0)
end
def self.half_off
@@half_off_discount ||= new(.5, DateTime.new(2014, 3, 31))
end
def self.twenty_percent_off
@@twenty_percent_off ||= new(.2, DateTime.new(2014, 12, 31))
end
def initialize(amount, end_date = nil)
@amount = amount
@end_date = end_date
end
def amount
@amount
end
def end_date
@end_date ||= DateTime.new(DateTime.now.year + 100, 1, 1)
end
end
Trying to run Discount.new(...) should throw an error. We only have three Discount instances available:
Discount.half_off
Discount.twenty_percent_off
Discount.default_discount
Given that the Order#order_type is used to determine the discount, we emulate this with Order#discount returning the proper Discount instance based on the Order#order_type. Furthermore, we prevent people from gaming the system by defining their own discounts and the logic for all the discounts is in one class.
order = Order.new
order.order_type = 1
puts order.discount_amount # -> .2
order = Order.new
order.order_type = 2
puts order.discount_amount # -> .5
You can use sub classing to create even more specific business logic, for instance a "random" discount:
class Discount
protected_class_method :new
...
def self.random
@random_discount ||= RandomDiscount.new(nil)
end
class RandomDiscount < Discount
def amount
rand / 2
end
end
end
Now Discount.random.amount outputs a different discount every time. The possibilities become endless.
How this applies to your question
The existence of repetitive if-statements means your class is doing too much. It should be delegating to another class that specializes in one of those branches of code. You shouldn't have to manipulate methods in Ruby at runtime to achieve this. It's too much "magic" and is confusing to new developers. With the approach I outlined above, you get a strongly typed definition of what these discounts are, and you keep each class focused on one task (and no, "strongly typed" is not a four letter word in Ruby when properly used). You get well defined relationships between objects, code that is easier to test and you get strong enforcement of business rules. All with no "magic."
#code that stays the same across my_method_1, 2, 3, 4, etc. #code that varies across my_method_1, 2, 3, 4, etc.it is not clear how to clean it up.evalormethod_missing. I would be very cautious about using any metaprogramming because it tends to make the code much less readable. There should be some other Ruby structures you can use.params[:user_id]is#code that stays the same across my_method_1, 2, 3, 4, etc.something that could be put in a method that would be called by each of the methods 1...4? I suggest you provide a complete example, which I don't think would have be especially verbose.