fbelanger
fbelanger

Reputation: 3568

Ruby on Rails OOP not working as expected

I'm writing a module to handle payment within our website.

# lib/payment/payment.rb
module Payment
  class Payment
    # some payment handling
  end
end

And I want to separate out the logic that determines the amount due. We'll just say that we're paying for a Car as an example.

Now, I was under the impression that this would work:

# lib/payment/car.rb
module Payment
  class Car < Payment
    # builds amount for cars
  end
 end

But even in the Payment module, with Payment::Payment defined, it is is throwing:

TypeError: superclass must be a Class (Module given)

I'm know that this resolves the issue:

class Payment::Car < Payment::Payment
  # stuff
end

But I'm curious as to why my solution wasn't working.

Upvotes: 1

Views: 171

Answers (2)

jvillian
jvillian

Reputation: 20263

I think you exactly nailed it in your comment. Naming both the module and class Payment is the problem.

When you have

module Payment
  class Car < Payment
  end
end

autoload tries (when loading Car) to load Payment. The first Constant it comes across in the tree with the name Payment is a module, not a class. It doesn't know you're trying to load a class called Payment - so it tries to use the module Payment. That's why you get the error you get.

When you do

Payment::Car < Payment::Payment

You're explicitley saying you want a class Payment located in a module Payment, so now autoload is able to load the correct Constant.

As a follow-on thought, it seems conceptually weird to have Car inherit from Payment. Technically, it works. But it doesn't reflect the real world. To enable classes with payment behaviors, you may want to go the composition route. So, make a module something like:

module Payments
  module PaymentClassMethods
    def some_method
    end
    def some_other_method
    end
  end
end

Then do

module Payments
  class Car
    extend PaymentClassMethods
  end
end

Now, Car doesn't inherit from Payment, but still has the payment behaviors.

If you also want Car to get instance methods, you could do something like:

module Payments
  module PaymentClassMethods
    def some_class_method
    end
    def some_other_class_method
    end
  end
  module PaymentInstanceMethods
    def some_instance_method
    end
    def some_other_instance_method
    end
  end
end

module Payments
  class Car
    include PaymentInstanceMethods
    extend PaymentClassMethods
  end
end

If you're doing this a lot, you may want to re-open the Car class so you could just say include PaymentMethods and get both the class and instance methods. But, that's a bit of a different topic.

Also, if you're including (or extending) methods that interact with models other than Car, then I'd suggest you consider creating a service object. IMO, models should know very little about one another. If you're Car model ends up knowing a lot about other models, then you could end up with some tight coupling - which can be a problem if you start monkeying with your Car model. Again, topic for a separate discussion. But, hopefully the thought helps.

Upvotes: 2

fbelanger
fbelanger

Reputation: 3568

Okay, so it seems like naming a both a module and a class the same thing complicates things.

module A
  class B
  end

  class C < B
  end
end

A::C.superclass
=> A::B

But if we name the class the same as the module:

module A
  class A
  end

  class B < A
  end
end

TypeError: superclass mismatch for class B

If anyone has faced this sort of thing or has any comments or suggestions, feel free to share!

Upvotes: 0

Related Questions