2

I came across this problem on an online test. I have this class:

class DocumentStore
  def initialize(capacity)
    @capacity = capacity;
    @documents = []
  end

  def get_documents
    return @documents
  end

  def add_document(document)
    raise 'Document store is full' if @documents.length >= @capacity
    @documents.push(document)
  end

  def inspect
    return "Document store: #{@documents.length}/#{@capacity}"
  end
end

I want to return the store data via get_documents, and prevent the user from changing/affecting its value via the returned object, e.g.,

ds = DocumentStore.new(3)
ds.add_document("Doc1")

docs = ds.get_documents
docs.push("Doc2")

puts ds.inspect # this should just print ["Doc1"]
sawa
  • 160,959
  • 41
  • 265
  • 366
clueless
  • 763
  • 1
  • 9
  • 20
  • docs.class is array so obviously user will be able to push any items in array. and seriously this question is tough. and it also start to killing me – Vishal Sep 04 '18 at 05:16
  • I recommend to return duplicate array from the 'get-documents' method. Hint: there's 'dup' method; or you make your array immutable by using the 'freeze' method. Or you create your own class "ImmutableDocumentStore" which extends current document store, but overrides all the immutable methods or returns only immutable arrays. It was just couple of ideas you could try out. – timgluz Sep 04 '18 at 05:49
  • Possible duplicate of [Is Ruby pass by reference or by value?](https://stackoverflow.com/questions/1872110/is-ruby-pass-by-reference-or-by-value) – Jagdeep Singh Sep 04 '18 at 06:32
  • 2
    Your question is unclear. You ask "How do you make a class variable unmodifiable?" but there is no class variable in your code. Which class variable are you talking about? However, it is simply impossible to make variables unmodifiable in Ruby. Even constants can be modified (they only generate a warning). – Jörg W Mittag Sep 04 '18 at 06:49

1 Answers1

4

This is simply done by calling Object#freeze in combination with Object#dup. freeze doesn't returns a frozen copy, but instead freezes self and returns self. This means that without the dup call you can't modify the array inside the class either.

def get_documents
  return @documents.dup.freeze
end

You could also opt to only use dup, returning a shallow copy. Allowing the caller to modify the array without effecting @documents.

def get_documents
  return @documents.dup
end

Note: Keep in mind that there should be no other way to retrieve the array. Ruby always returns the last statement made by a method. This means that your method DocumentStore#add_document will return the result of @documents.push(document).

Array#push

Append — Pushes the given object(s) on to the end of this array. This expression returns the array itself, so several appends may be chained together. See also #pop for the opposite effect.

3limin4t0r
  • 16,643
  • 2
  • 22
  • 46
  • 1
    It's maybe worth mentioning that this will only make a shallow copy of the array. Meaning, if you extract one of the elements of the cloned/duped array and modify it, the changes would apply to that element in the original array as well. Also, another way to copy the array would be `[*@documents]` – max pleaner Sep 04 '18 at 07:13
  • Correct, if OP has any issues understanding the concept I'd recommend checking out https://stackoverflow.com/q/184710/3982562 – 3limin4t0r Sep 04 '18 at 08:59