Class vs instance methods
20 Jul 2011
Are class methods evil?
Nick Sutterer who is honorable citizen of my city published a post Are class methods evil? It’s really worth reading as he mentions couple of important aspects of Object Oriented approach and how they are used by Rails community.
Nick mentions my tweet
I’m more and more against using class methods. It often leads to procedural-like programming.
Why class methods?
I often hear “Why to create an object when I can just call a class method?”. The reasoning behind that is quite obvious:
- 1) shorter code
- 2) less objects = less GC
- 3) stateless behavior and no need to initialization
1) and 2) are completely valid arguments. However in my opinion these arguments shouldn’t impose the architecture. My main objection is assumption that given behavior is stateless thus doesn’t need to be encapsulated in object.
Stateless vs Stateful
- Class methods are executed in context of their arguments
- Instance methods, in addition to arguments, are executed in context of state of the object
Of course there are more differences but from my point of view that one is quite significant.
Using class methods means we can’t keep a state thus the state must be passed in method arguments. If some module is designed around class methods we deal with procedural-like programming passing arguments from one method to another.
Naturally the state may be kept in class variables but it’s introducing a global state which as Nick remarks should be abandoned.
Not a real world example but I think it shows the difference.
Procedural-like version
class CssProcessor
def self.process(code)
bust_cache(minify(code))
end
def self.minify(code)
AwesomeMinifier.minify(code, :type => :cs)
end
def self.bust_cache(code)
AwesomeMinifier.bust_cache(code, :type => :css)
end
end
CssProcessor.process("some_css_code")
Object oriented version
class CssProcessor
def initialize(code)
@minifier = AwesomeMinifier.new(code, :type => :css)
end
def process
minify
bust_cache
end
def minify
@minifier.minify
end
def bust_cache
@minifier.bust_cache
end
end
processor = CssProcessor.new("some_css_code")
processor.process
Initialization process and keeping a state isn’t useless even in such a simple example. At the first sight it may seem awkward to use an object when class method can be used more easily. Although it’s often a trap. The bigger codebase the more time needed to maintain a procedural code.
I prefer to think in object oriented manner from the ground up. It is like MVC a bit harder at the beginning but it quickly starts to pay for itself.
ActiveRecord
In his article Nick shows some examples of ActiveRecord usage. It is hard to avoid class methods due to ActiveRecord design but Andrzej Krzywda showed how to deal with it. I recommend reading his article.
UPDATE
Everything is an object
Rubyists like to emphasize that in their favorite programming language everything is an object. It’s not like C++ or JAVA which are “not fully object oriented”. But that fact is often forgotten and misunderstood. I think utility methods are a great example.
Is someting wrong with that code?
Math.sin(2) * Math::PI
In that case we don’t use the fact the number 2 is an object in Ruby. It’s used like a primitive. This is how we do it in C++.
2.sin * Math::PI
Of course Fixnum doesn’t have sin method. But we can extend Fixnum to provide it. Monkey patching Fixnum is harmful but there are plenty of other options
- singleton object methods
- DCI
- monkey patch Fixnum with refinements (Ruby 2.0)
When using ActiveSupport it’s quite obvious to do
5.megabytes
instead of
Converter.to_megabytes(5)
so why not to spread object approach over more parts of the codebase?
Problem
I’ve recently noticed I often use code like that.
class Offer
def available?
end
def cancelled?
end
def postponed?
end
end
if offer.available?
order.submit
elsif offer.cancelled?
order.reject
elsif offer.postponed?
order.postpone
else raise UnknownStateError.new
end
if/else statements looks like perfect candidates for case … when statement. In particular I’d like to have something like that:
case offer
when :available? then order.submit
when :cancelled? then order.reject
when :postponed? then order.postpone
else raise UnknownStateError.new
Looks better, right? Of course it doesn’t work out of the box. We’ll try to overcome it but first some theory.
case/when uses === method internally. In particular ruby calls code like this:
:available?.===(offer)
…which will always return false when compared to non-symbol. Triple equals (===) is slightly different that double equals (==) so be awared when each of them is called.
Solution
When we know what is going under the hood it is easy to slightly modify the behaviour to fit our needs.
class Symbol
alias :original_triple_equals :"==="
def ===(object)
original_triple_equals(object) ||
(object.respond_to?(self) && object.__send__(self))
end
end
We override triple equals and try to call a desired method if original behaviour returns false. Let’s test it
case []
when :empty? then puts "that works!"
else puts "something went wrong"
Works like a charm.
Performance
There is no doubt performance will be affected. Let’s benchmark performance of comparing symbols.
REPEAT = 1_000_000
Benchmark.bm do |x|
x.report("original ===") do
REPEAT.times { :s1.original_triple_equals(:s2) }
end
x.report("overriden ===") do
REPEAT.times { :s1.===(:s2) }
end
end
Ruby 1.8:
user system total real
original === 0.610000 0.170000 0.780000 ( 0.782712)
overriden === 1.530000 0.330000 1.860000 ( 1.852536)
Performance hit: 238%
Ruby 1.9:
user system total real
original === 0.180000 0.000000 0.180000 ( 0.187665)
overriden === 0.580000 0.000000 0.580000 ( 0.576526)
Performance hit: 322%
Benchmark isn’t the most accurate but let’s approximate that decrease in performance is 200%-400%. Is it much? It depends how often you symbols are compared but under some circumstances it may lead for serious performance problems. I warned you.
Ruby 1.9
Ruby 1.9 comes with a new method of calling blocks.
proc { puts "it looks awkward" }.===
It gives us some cool capabilities. Take a look at that example.
How does it help us calling methods in case … when statement? Frankly… not much :) But we can do some interesting tricks for fun and profit.
case []
when :empty?.to_proc then puts "empty array"
else puts "Ruby 1.9?"
end
We convert Symbol to proc which was introduced in Ruby 1.8.7 and then case … when calls proc.=== implicity. Of course calling to_proc each time isn’t very helpful and readability is even worse.
Quite better solution which comes to my mind is to introduce a method which takes a symbol and call to_proc on it. The method should be globally accessible so we have to define it at kernel level.
module Kernel
def is(symbol)
symbol.to_proc
end
end
case []
when is(:empty?) then puts "empty array"
else puts "Ruby 1.9?"
end
Unfortunately we can’t skip parentheses here.
There is a big benefit here. We don’t override symbol.=== so performance remains unaffected. On the other hand polluting Kernel namespace is a certain drawback.
Of course Ruby 1.9 solution may be implemented in Ruby 1.8 as well. You would just need to implement Proc#=== method.
Update: Jacob’s Solution
Jacob Rothstein introduced very neat solution using hashes and lambdas. It doesn’t use case … when statement at all but behavior is very similar.
class Object
def switch( hash )
hash.each {|method, proc| return proc[] if send method }
yield if block_given?
end
end
# which allows us to write functional-like code
offer.switch(
available: -> { order.submit },
cancelled: -> { order.reject },
postponed: -> { order.postpone }
) { raise UnknownStateError.new }
It depends on new lambda syntax and ordered hashes so Ruby 1.9 is required. This is very clear and doesn’t pollute Symbol#=== method. Read full explanation here.
IRC notifications via SSH and libnotify
09 Feb 2011
Recently I’ve started using IRC again. What a great feeling! I use pretty standard technique. My irssi client is run on my server in screen session. I attach to the session via SSH to read and write some gossip.
ssh -t michal@example.com screen -r
Only one thing bothers me. There is no sign of new messages on IRC channel. I have to switch terminals and check if there is something new.
Remote side
It is quite easy to get notifications having irssi on local machine but getting them from remote machine seems to be slightly more complicated. There is no easy way for remote machine to send us data about new traffic. I don’t want port forwarding or something similar to achieve so silly task. Polling the server seems to be the only way. It would be also nice if the task could be done with persisted connection to avoid connecting again and again.
Other problem: how to know that new message has been sent? I’ve enabled irssi logs. I’m not the irssi expert some maybe there is more accurate way. To enable logs open irssi and type:
/set autolog
The logs are saved to ~/irclogs/IRC_SERVER/CHANNEL.log
How to monitor them without reconnecting? Simple tail -f to the rescue
ssh michal@example.com tail -n0 -f ~/irclogs/*/*.log
I’ve also added -q and -n0 arguments.
- -q - don’t print file headers
- -n0 - don’t print trailing lines (by default tail prints 10 trailing lines)
For now on new messages appears on your local terminal.
Notifications
OK, half of the job is done. Now how to display notifications? LibNotify is excellent for that purpose.
$ apt-get install libnotify-bin $ notify-send "hello" "notification sent from my libnotify-bin"
OS X users may use Growl which seems to be very nice tool too.
Join them
We have irc logs forwarded to our host and notification tool. Firstly I wanted to use xargs Finally there will be something about ruby :) Of course perl/python/C or others does their job as well.
ssh michal@example.com "tail -n0 -f ~/irclogs/*/*.log" | \
ruby -rshellwords -ne '`notify-send -t 500 #{$_.shellescape}`'
Quick explanation
- -rshellwords means require ‘shellwords’
- -n - read each line from stdin and assign to $_ (for perl lovers)
- -e - exectues ruby code
- `system_command` - executes some_command in your shell (backticks not quotes)
- String.shellescape - prevents your ircmates from executing commands on your machine ;-)
notify-send -t 500 is quite more interesting. Basically it displays notification with 500ms timeout. Official documentation says that. However Ubuntu users have to install patched version. Debian/Gentoo/Fedora/Arch/etc users are OK. Why only Ubuntu? Well, Ubuntu core-team recons theirs users are too dumb to decide about notification timeout. Read ridiculous story here.
Improve
Above oneliner isn’t very sophisticated. There is plenty of room for improvements. For instance to display nickname as notification title and message as notification content only simple change is needed
ssh michal@example.com "tail -q -n0 -f ~/irclogs/*/*.log " | \
ruby -rshellwords -ne '`notify-send -t 1000 #{$1.shellescape} #{$2.shellescape}`\
if $_ =~ /<([^>]+)>(.*)/'
Don’t stick to oneliner unnecessarily. More complex scripts should be written to file.
