Are Ruby Class Variables Bad

Why is using a class variable in Ruby considered a 'code smell'?

As you can find in their documentation on Class Variables:

Class variables form part of the global runtime state, and as such make it easy for one part of the system to accidentally or inadvertently depend on another part of the system. So the system becomes more prone to problems where changing something over here breaks something over there. In particular, class variables can make it hard to set up tests (because the context of the test includes all global state).

Essentially, it's a manifestation of global state, which is almost universally considered evil, because it makes tests more difficult and results in a much more fragile class/program structure.

This Stack Overflow question may also be worth reading, which shows the main problem with class variables: if any class inherits from your class and modifies the class variable, every instance of that variable changes, even from the parent! This understandably gives you a way to shoot yourself in the foot easily, so it may be best to avoid them unless you're very careful.

It's also worth comparing class variables with class instance variables. This question has a few good examples which illustrate the usage differences, but in essence class variables are shared, whereas class instance variables are not shared. Therefore, to avoid unwanted side effects, class instance variables are almost always what you want.

Why should @@class_variables be avoided in Ruby?

Class variables are often maligned because of their sometimes confusing behavior regarding inheritance:

class Foo
@@foo = 42

def self.foo
@@foo
end
end

class Bar < Foo
@@foo = 23
end

Foo.foo #=> 23
Bar.foo #=> 23

If you use class instance variables instead, you get:

class Foo
@foo = 42

def self.foo
@foo
end
end

class Bar < Foo
@foo = 23
end

Foo.foo #=> 42
Bar.foo #=> 23

This is often more useful.

How to avoid class and global variables

You misunderstand the term "class instance variable". It means "instance variable on a Class object", not "instance variable on an instance of some class".

  class Person
attr_accessor :memories # instance variable, not shared

class << self
attr_accessor :memories # class instance variable, shared between
# all instances of this class
end
end

Obviously, sometimes you do need to use class instance variables. Refrain from using class variables (@@memories) as they are shared between all classes in the hierarchy (the class and its children), which may lead to surprising behaviour.

Are Ruby class variables similar to the Java static variables?

There's a lot of similarity between Ruby and Java by virtue of them being object-oriented, but their family tree is different. Ruby leans very heavily on Smalltalk while Java inherits from the C++ school of thinking.

The difference here is that Ruby's concept of public/private/protected is a lot weaker, they're more suggestions than rules, and things like static methods or constants are more of a pattern than a construct in the language.

Global variables are frowned on quite heavily, they can cause chaos if used liberally. The Ruby way is to namespace things:

$ugly_global = 0  # Not recommended, could conflict with other code
# Ownership of this variable isn't made clear.

$ugly_global += 1 # Works, but again, it's without context.

module UglyCounter # Defines a module/namespace to live in
def self.current # Defines a clear interface to this value
@counter ||= 0 # Initializes a local instance variable
end

def self.current=(v) # Allow modification of this value
@counter = v.to_i # A chance to perform any casting/cleaning
end
end

UglyCounter.current += 1 # Modifies the state of a variable, but
# the context is made clear.

Even a thin layer like this module gives you the ability to intercept read/write operations from this variable and alter the behaviour. Maybe you want to default to a particular value or convert values into a normalized form. With a bare global you have to repeat this code everywhere. Here you can consolidate it.

Class variables are a whole different thing. They're also best avoided because sharing data between the class and instances of this class can be messy. They're two different contexts and that separation should be respected.

class MessyClass
@@shared = 0

def counter
@@shared
end

def counter=(v)
@@shared = v
end
end

This is a pretty rough take on how to use a shared class-level instance variable. The problem here is each instance is directly modifying it, bypassing the class context, which means the class is helpless. This is fundamentally rude, the instance is over-extending its authority. A better approach is this:

class CleanerClass
def self.counter
@counter ||= 0
end

def self.counter=(v)
@counter = v.to_i
end

# These are reduced to simple bridge methods, nothing more. Because
# they simply forward calls there's no breach of authority.
def counter
self.class.counter
end

def counter=(v)
self.class.counter = v
end
end

In many languages a static class method becomes available in the scope of an instance automatically, but this is not the case in Ruby. You must write bridge/proxy/delegate methods, the terminology here varying depending on what you're used to.

How can I avoid using class variables in Ruby

@@cost behaves more like a constant (i.e. it won't change during runtime), so you should use one instead:

COST = 0.0946

@@kwh should be an instance variable, since it is used only within the instantiated object, so you could use @kwh instead:

@kwh = (watt / 1000) * hours

And daily_cost = @@kwh * @@cost will become:

daily_cost = @kwh * COST

That will avoid the use of class variables, but you could also eliminate @kwh altogether since you don't use it anywhere else.

So, instead of:

def watt_to_kwh(hours)
@kwh = (watt / 1000) * hours
end

You could just do:

def watt_to_kwh(hours)
(watt / 1000) * hours
end

And use it like this in cost_of_energy method:

def cost_of_energy
puts "How many hours do you use the #{self.name} daily?"
hours = gets.chomp.to_i
daily_cost = watt_to_kwh(hours) * COST
montly_cost = daily_cost * 30
puts "Dayly cost: #{daily_cost}€"
puts "montly_cost: #{montly_cost}€"
end


Related Topics



Leave a reply



Submit