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
How to Write an Rspec Test for a Ruby Method That Contains "Gets.Chomp"
Installing Ruby-2.1.2: Cannot Load Such File -- Openssl (Loaderror)
How to Remove Non-Printable/Invisible Characters in Ruby
How to Count Existing Instances of a Class in Ruby
Declaring an Integer Range with Step != 1 in Ruby
How to Access the Current Node from a Library in a Chef Cookbook
Why Does Ruby Builder::Xmlmarkup Add Inspect Tag to Xml
How to Run Ruby on Rails Applications on a Windows Box
How to Ignore a Folder in Zeitwerk for Rails 6
Ruby: How to Find the Key of the Largest Value in a Hash
Ruby: Could Not Find a Temporary Directory
How to Get Long Filename from Argv