How to properly destroy a class
Asked Answered
D

3

5

In Ruby, I have a DAO class, which is extended by a class that makes managing the connections easier, which is extended by a class that represents and manipulates data in a DB, which is further extended by another class. To use an animal metaphor it would look like this:

class Animal
 ...
end

class Mammal < Animal
 ...
end

class Feline < Mammal
 ...
end

class Cat < Feline
 ...
end

class Lion < Cat
 ...
end

...

In PHP, there is __destruct method that runs when you destroy/delete a class. And should that class extend another class, you simply add parent::__destruct() to the class's __destruct method like this:

public function __destruct() {
  // Clean up code for this class here
  ...

  // Execute clean up code for Parent class
  parent::__destruct();
}

I could have a similar method for all the classes except Animal. Since it doesn't extend anything, the parent::__destruct(); line is no longer valid.

However, as I understand it, Ruby doesn't have a method like this for its objects. A finalizer can be set, but I decided to just put in a cleanup method I can call whenever I want to destroy/delete a class. That would take care of anything that needed doing prior to my setting the class to nil.

This raises a new problem though. If the method is always named cleanup and I call lion_instance.cleanup, I assume it calls the Lion#cleanup. How then to get it to call the cleanup in class Cat and then Feline and on down the chain?

Or is this a wrong approach and you have a better idea?

Displume answered 21/9, 2012 at 19:16 Comment(0)
C
6

The Ruby idiom for this is to yield to a block which does work, and when the block returns, do cleanup. Ruby's built-in "File.open" does this:

File.open("/tmp/foo") do |file|
  file.puts "foo"
end

When the block ends, the file is closed for you, without you having to do anything. This is an excellent idiom. Here's how you might implement something like that:

class Foo

  def self.open(*args)
     foo = new(*args)
     yield foo
     foo.close
  end

  def initialize
    # do setup here
  end

  def close
    # do teardown here
  end

end

And to use it:

Foo.open do |foo|
  # use foo
end

Foo#close will be caused automatically after the end


This will work with subclassing as well. That's because class methods are inherited just as are instance methods. Here's the superclass:

class Superclass

  def self.open(*args)
    o = new(*args)
    yield o
    o.close
  end

  def initialize
    # common setup behavior
  end

  def close
    # common cleanup behavior
  end

end

and two derived classes:

class Foo < Superclass

  def initialize
    super
    # do subclass specific setup here
  end

  def close
    super
    # do subclass specific teardown here
  end

end

class Bar < Superclass

  def initialize
    super
    # do subclass specific setup here
  end

  def close
    super
    # do subclass specific teardown here
  end

end

to use:

Foo.open do |foo|
  # use foo
end

Bar.open do |bar|
  # use bar
end

If you really need to make sure that cleanup happens no matter what, then use an ensure clause in the class method:

  def self.open(*args)
     foo = new(*args)
     begin
       yield foo
     ensure
       foo.close
     end
  end

This way, cleanup happens even if there is an exception in the block.

Chiles answered 21/9, 2012 at 19:49 Comment(7)
I see how this would work nicely for class Foo ... end but not class Foo < Bar ... endDisplume
@gabe, Good point. I've added an example of subclassing with this pattern.Chiles
So there's nothing special that needs to go into the Foo initialize method to say call Superclass.open? What about order of operation? In other words, does the Foo.open complete before Superclass.open? And likewise for close.Displume
@gabe, Nothing special at all. There isn't actually a Foo.open, really. There's just a Superclass.open, but when you call Foo.open, class Foo finds that it doesn't know how to do that, so it asks its superclass. The superclass does know how to do it, so it does.Chiles
Ignorant (edging on really dumb) question: May I assume Open3 works this way as well? If I use it to run a system command and use a block to consume the pipes it returns, can I assume I've done my part to clean up after myself?Trimolecular
@Christopher, Yes. You can see this yourself: In your ruby installation, find open3.rb, and look for popen_run. You'll see an ensure block that closes file handles.Chiles
@WayneConrad Thanks! I figured it was the case, but wanted someone to validate it.Trimolecular
A
3

You can use ObjectSpace.define_finalizer

Something like:

class Animal
  def initialize
    ObjectSpace.define_finalizer(self, proc { # your code })
   end
end
Aixenprovence answered 21/9, 2012 at 19:35 Comment(1)
I've read that the define_finalizer is prett bug prone to the way it has be implemented. However, those comments were from 2010 so maybe they no longer apply.Displume
G
1

Well since no one answered your question about the method moving its way up the inheritance chain...

class Cat
  def rawr
    puts "rawr"
  end
end

class Kitty < Cat
  def rawr
    puts "meow"
    super
  end
end

Cat.new.rawr
"Rawr"

Kitty.new.rawr
"rawr"
"meow"

Within a method, you can access the superclass's method of the same name by calling super.

Girt answered 21/9, 2012 at 23:23 Comment(1)
That's the missing piece for me. Wayne had stated above that the Superclass.open would get executed when it didn't find .open in Foo. However, I wanted a .open in each class to do specific things. Thanks again.Displume

© 2022 - 2024 — McMap. All rights reserved.