1

I know PHP & Javascript, but I'm just starting to learn Ruby.

This is currently working :

  class Animal
    attr_accessor :name, :mammal
    def initialize(name)
      @name = name
    end
  end

  class Fish < Animal
    def initialize(name)
      super(name)
      @mammal = false
    end
  end

  class Cow < Animal
    def initialize(name)
      super(name)
      @mammal = true
    end
  end

  animals = [
    Fish.new('Moppy'),
    Cow.new('Marguerite'),
  ]

  animals.each do |animal|
    puts "Is #{animal.name} a mammal ? #{animal.mammal}"
  end

See the @mammal var in the sub classes ?

They are 'static' variables which do not depend of the instance, but of the class itself (a cow will always be a mammal, while a fish won't)

I was wondering if I was declaring the @mammal var at the right place. Instinctively, I would rather have done this

  class Cow < Animal
    @mammal = true
    def initialize(name)
      super(name)
    end
  end

but then it does not work... Could someone tell me if how you should handle this with Ruby ?

Thanks !

gordie
  • 1,637
  • 3
  • 21
  • 41
  • I'd have a look at [this](http://www.railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/) article which explains quite a lot about class variables and class instance variables, including behaviour that you might not expect. – 3limin4t0r Jan 28 '20 at 10:41

4 Answers4

3

The simplest solution is:

  class Fish < Animal
    def mammal
      false
    end
  end

  class Cow < Animal
    def mammal
      true
    end
  end

Personally I'd be tempted to do something like:

  class Animal
    attr_accessor :name

    def initialize(name)
      @name = name
    end

    def mammal
      false
    end
  end

  class Mammal < Animal
    def mammal
      true
    end
  end

  class Fish < Amimal
  end

  class Cow < Mammal
  end
ReggieB
  • 8,100
  • 3
  • 38
  • 46
1

If you want a class-wide variable, define an instance variable on class level.

class Animal
    attr_accessor :name
    def initialize(name)
      @name = name
    end

    def self.mammal # class level
      @mammal
    end
  end

  class Fish < Animal
    @mammal = false # class level
  end

  class Cow < Animal
    @mammal = true # class level
  end

  [
    Fish.new('Moppy'),
    Cow.new('Marguerite'),
  ].each do |animal|
    puts "Is #{animal.name} a mammal ? #{animal.class.mammal}"
  end

Also, one might use so-called “class variables” @mammal, but they behave weirdly under some circumstances, so instance variable on the class level would fit your needs better.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
0

@var is an instance variable and @@var is a class variable (both private).

@var in class scope also works, but it's an instance variable on the class which is a bit different because (as all instance variables) it's going to be private to the class instance and thus not accessible to the class's instances, so you need to define a class-level accessor for it.

A big difference between class variables (@@) and class instance variables (@ in class scope) is that the first is shared throughout the inheritance tree while the second is private to one specific class. But given your structure Mammal being a class would make more sense than either.

See Ruby class instance variable vs. class variable for various examples.

Masklinn
  • 34,759
  • 3
  • 38
  • 57
0

For each subclass, @mammal is the same for all instances and is not intended to change. It therefore should be a constant or the value returned by a class method. I initially chose the former, but have been convinced by @tadman in the comments that a class method would be preferable, in particular a method with a name ending with a question mark. If interested, see my original answer in the edit history.

class Animal
  attr_accessor :name
  def initialize(name)
    @name = name
  end
end

class Fish < Animal
  def initialize(name)
    super
  end
  def self.mammal?
    false
  end
end

class Cow < Animal
  def initialize(name)
    super
  end
  def self.mammal?
    true
  end
end

i = Fish.new('Moppy')
"#{i.name} is a mammal: #{i.class.mammal?}"
  #=> "Moppy is a mammal: false" 
i = Cow.new('Marguerite')
"#{i.name} is a mammal: #{i.class.mammal?}"
  #=> "Marguerite is a mammal: true" 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Technically works, but poking around inside a class to get constants is kind of intrusive from an object-oriented design principle. A cleaner interface is having a method, that promotes the right level of encapsulation. – tadman Jan 28 '20 at 18:50
  • @tadman, an advantage of this approach is readability. If, in the code base, far, far, away from these class definitions, `Cow::MAMMA` is more meaningful than `Cow.mammal` or (worse) `Cow.new('Bob').mammal`. – Cary Swoveland Jan 28 '20 at 18:56
  • The problem with accessing constants directly is it means they're no longer private, they're part of an interface. Intermediating with a method means the class can re-implement that constant at-will. You'll often see things start as constants and shift to dynamically loaded from files or databases, which means the constant might go away. `Cow.mammal?` is actually pretty ordinary Ruby. `Cow::MAMMAL` looks invasive and presumptive. – tadman Jan 28 '20 at 19:06
  • 1
    @tadman, I'm convinced. I've done an edit. Please leave your comments as I believe the discussion is educational. – Cary Swoveland Jan 28 '20 at 20:43
  • 1
    Yeah, that's what I'm talking about. Even if those methods referenced a constant, that's fine, but preserving encapsulation is the goal here. Encapsulation means you're free to re-implement internals without breaking external dependencies. Having things hanging off of a constant means you're locked in to a particular implementation, which can hamper development. In *programming by contract* that method promises to return a boolean, but it makes no promises about how it computes that value. With the constant approach you're promising to make a constant with a particular name and content. – tadman Jan 28 '20 at 21:49
  • 1
    What I mean is that once you use a constant outside of the module/class it's declared in that becomes a *load bearing constant* which is not ideal. It means you have to be careful when working around it, and can't just remove/refactor it at will as you usually would be able to. With the method approach the implementation could be `true` or `IS_MAMMAL[:cow]` or even `Wikipedia::SpeciesParser.new('Cattle').taxonomy_class == 'Mammalia'`, or whatever so long as the result is as expected. – tadman Jan 28 '20 at 21:50