TriPham259
TriPham259

Reputation: 61

Methods to create deep copy of objects without the help of Marshal

I have 3 simple classes CashRegister, Bill and Position. A CashRegister is composed of Bill objects and a Bill object is composed of Position objects. They're implemented as followed

class CashRegister
  def initialize
    @bills = []
  end

  def clone 
    #?
  end
end

class Bill
  def initialize(nr)
    @nr = nr
    @positions = []
  end

  def clone 
    #?
  end
end

class Position
  def initialize(product, price)
    @product = product
    @price = price
  end

  def clone 
    #?
  end
end   

How do I create methods that can deep copy the objects of these classes. The use of Marshal.load(Marshal.dump(an_obj)) is not allowed.

Edit: So far I've got this:

    class CashRegister
      def initialize
        @bills = []
      end

      def clone 
        @bills.map { |bill| bill.clone}
      end
    end

    class Bill
      def initialize(nr)
        @nr = nr
        @positions = []
      end

      def clone 
        cloned = super
        cloned.positions = @positions.map{ |pos| pos.clone}      
        cloned
      end
    end

    class Position

      attr_reader :preis
      # this method is given
      def produkt
        @produkt.clone()
      end

      def initialize(product, price)
        @product = product
        @price = price
      end

      def clone 
        cloned = super
        cloned.product    
        cloned
      end
    end   

The clone method in class Position seems to be ok (no compile error). But there is an error in the one in class Bill, it says "undefined method 'positions=', so the problem must be in the line cloned.positions = @positions.map{ |pos| pos.clone}. But I don't understand, can't we call cloned.positions like that?

Upvotes: 3

Views: 162

Answers (3)

TriPham259
TriPham259

Reputation: 61

This solution works

class CashRegister
  attr_accessor :bills      

  def initialize
    @bills = []
  end

  def clone 
    cloned = super
    cloned.bills = @bills.map { |bill| bill.clone }
    cloned 
  end
end

class Bill
  attr_accessor :positions

  def initialize(nr)
    @nr = nr
    @positions = []
  end

  def clone 
    cloned = super
    cloned.positions = @positions.map{ |pos| pos.clone }      
    cloned
  end
end

class Position
  attr_reader :price
  attr_writer :product

  # this method is given
  def product
    @product.clone
  end

  def initialize(product, price)
    @product = product
    @price = price
  end

  def clone 
    cloned = super
    cloned.product = product  
    cloned
  end
end 

Upvotes: 1

Peter Camilleri
Peter Camilleri

Reputation: 1912

Another possible answer is to use the full_dup gem (full disclosure, written by me) then simply use:

p2 = p1.full_dup

Now full_dup, like regular dup, does not copy any singleton methods. If that is important, try the full_clone gem (yup, by me too) instead.

If there are fields that need to excluded from the dup (or clone process), the optional full_dup_exclude (or full_clone_exclude) method can be defined to list fields to be excluded from processing.

Note there is no need to worry about trying to clone numbers, symbols, and other non-clonable things that may exist in your object. These are handled safely be the gems.

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110725

It's just the instance variables you have to worry about.

class Position
  attr_accessor :product, :price
  def initialize(product, price)
    @product = product
    @price = price
  end
end

p1 = Position.new("lima beans", 2.31)
  #=> #<Position:0x000000027587b0 @product="lima beans", @price=2.31>
p2 = Position.new(p1.product, p1.price)
  #=> #<Position:0x0000000273dd48 @product="lima beans", @price=2.31>

We can confirm that p2 is a deep copy of p1.

p1.product = "lettuce"
p1.price   = 1.49

p1 #=> #<Position:0x0000000271f870 @product="lettuce", @price=1.49>
p2 #=> #<Position:0x000000026e9e00 @product="lima beans", @price=2.31>

p2.product = "spinach"
p2.price = 2.10

p1 #=> #<Position:0x0000000271f870 @product="lettuce", @price=1.49>
p2 #=> #<Position:0x000000026e9e00 @product="spinach", @price=2.1>

It's more complex if, for example, the class were defined as follows (where products is an array).

p1 = Position.new ["carrots", "onions"]
  #=> #<Position:0x000000025b8928 @products=["carrots", "onions"]>
p2 = Position.new p1.products
  #=> #<Position:0x000000025b0048 @products=["carrots", "onions"]>

p1.products << "beets"

p1 #=> #<Position:0x000000025b8928 @products=["carrots", "onions", "beets"]>
p2 #=> #<Position:0x000000025b0048 @products=["carrots", "onions", "beets"]>

p2 is not what we want. We would need to write

p1 = Position.new ["carrots", "onions"]
  #=> #<Position:0x00000002450900 @products=["carrots", "onions"]>
p2 = Position.new p1.products.dup
  #=> #<Position:0x0000000243aa88 @products=["carrots", "onions"]>

(note the .dup) so that

p1.products << "beets"
  #=> ["carrots", "onions", "beets"]

p1 #=> #<Position:0x00000002450900 @products=["carrots", "onions", "beets"]>
p2 #=> #<Position:0x0000000243aa88 @products=["carrots", "onions"]>

More generally, we need to make deep copies of the instance variables.

Upvotes: 3

Related Questions