0

I am trying to compose an object Transaction from objects TranFee and Rate.

class Transaction

  attr_reader :tranfee, :rate

  def initialize(hash)
    @tranfee = PaymentType::TranFee.new(hash)
    @rate = PaymentType::Rate.new(hash)
  end
end

module PaymentType

  def initialize(args = {}, regex)

    args.each do |key,value|
      if key =~ regex
        instance_variable_set("@#{key}", value) unless value.nil?
        eigenclass = class << self; self; end
        eigenclass.class_eval do
          attr_reader key
        end
      end
    end   
  end

  class TranFee
    include PaymentType

    def initialize(args, regex = /\Atran.*/)
      super(args, regex)
    end
  end

  class Rate
    include PaymentType

    def initialize(args, regex = /\Arate.*/)
      super(args, regex)
    end
  end
end

The rate and TranFee objects are created from a hash like the one below.

    reg_debit = {"name" => "reg_debit", "rate_base" => 0.0005, 
"tran_fee" => 0.21, "rate_basis_points" => 0.002, "tran_auth_fee" => 0.10}

I am initializing the objects based on regex because the hash will eventually contain more values and I want the program to adjust as more items/classes are added.

Additionally there will be some instances where there are no key's starting with "tran". Does anyone know how to make Transaction create only a Rate object if TranFee has no instance variables inside of it? (in otherwords, if the hash returns nothing when keys =~ /\Atran.*/)

an example would be when the hash looks like this reg_debit = {"name" => "reg_debit", "rate_base" => 0.0005, "rate_basis_points" => 0.002}, right now the output is

#<Transaction:0x007ff98c070548 @tranfee=#<PaymentType::TranFee:0x007ff98c070520>, @rate=#<PaymentType::Rate:0x007ff98c0704a8 @rate_base=0.0005, @rate_basis_points=0.002>>

So I am getting a TranFee object with nothing in it and I would like for that to drop off in this situation. not sure if there may be a better way to design this? I was trying to think of a way to use ostruct or struct, but I havnt been able to figure it out. thanks for any help here.

heinztomato
  • 795
  • 1
  • 7
  • 12

2 Answers2

2

I believe your strategy is very problematic - creating attributes to a class from user input doesn't sound like a very good idea.

Furthermore, adding methods (like attr_reader) to every instances can have severe performance issues.

If all you want is a data structure to hold your data, keep using a Hash. If you want a structure you can query using a dot notation instead of bracket notation, you might want to consider a gem like hashie or hashr.

If you want some code to make the flat data-structure hierarchical, I can suggest something like this:

hierarchical_hash = hash.each_with_object({}) do |(k, v), h| 
  if k.match(/^([^_]+)_(.+)$/)
    root_key = $1
    child_key = $2
    h[root_key] ||= {}
    h[root_key][child_key] = v
  else
    h[k] = v
  end
end
# => {
# =>     "name" => "reg_debit",
# =>     "rate" => {
# =>                 "base" => 0.0005,
# =>         "basis_points" => 0.002
# =>     },
# =>     "tran" => {
# =>              "fee" => 0.21,
# =>         "auth_fee" => 0.1
# =>     }
# => }
Uri Agassi
  • 36,848
  • 14
  • 76
  • 93
0

Your question raises some interesting issues. I will try to explain how you can fix it, but, as @Uri mentions, there may be better ways to address your problem.

I've assumed @tranfee is to be set equal to the first value in the hash whose key begins with "tran" and that @rate is to be set equal to the first value in the hash whose key begins with "rate". If that interpretation is not correct, please let me know.

Note that I've put initialize in the PaymentType module in a class (Papa) and made TranFee and Rate subclasses. That's the only way you can use super within initialize in the subclasses of that class.

Code

class Transaction
  attr_reader :tranfee, :rate

  def initialize(hash={})
    o = PaymentType::TranFee.new(hash)
    @tranfee = o.instance_variable_get(o.instance_variables.first)
    o = PaymentType::Rate.new(hash)
    @rate = o.instance_variable_get(o.instance_variables.first)
  end
end

.

module PaymentType
  class Papa
    def initialize(hash, prefix)
      key, value = hash.find { |key,value| key.start_with?(prefix) && value }
      (raise ArgumentError, "No key beginning with #{prefix}") unless key
      instance_variable_set("@#{key}", value)
      self.class.singleton_class.class_eval { attr_reader key }
    end
  end

  class TranFee < Papa
    def initialize(hash)
      super hash, "tran"
    end
  end

  class Rate < Papa
    def initialize(hash)
      super hash, "rate"
    end
  end    
end

I believe the method Object#singleton_class has been available since Ruby 1.9.3.

Example

reg_debit = {"name" => "reg_debit", "rate_base" => 0.0005, "tran_fee" => 0.21,
             "rate_basis_points" => 0.002, "tran_auth_fee" => 0.10}

a = Transaction.new reg_debit
p Transaction.instance_methods(false) #=> [:tranfee, :rate]
p a.instance_variables                #=> [:@tranfee, :@rate]
p a.tranfee                           #=> 0.21
p a.rate                              #=> 0.0005
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100