Prywatny setter

Hej,

czy jest możliwość stworzenia w jakiś sposób prywatnego settera? Gdy miałem:

private
  def elements=(elem)
    elements = elem
  end

to rspec pluł takimi błędami.

https://s23.postimg.org/ehpc3c3uj/Virtual_Box_Debian_Rails_20_01_2017_19_36_51.png

attr_reader :elements

private

attr_writer :elements

Rspec pluł Ci błędami, bo próbowałeś się odwołać do prywatnej metody spoza klasy (MathSet.new.elements = costam)

po zmianie dalej pluje :frowning:

No bo jak coś jest prywatne to możesz się do tego odwołać tylko z wewnątrz klasy. Poza tą klasą, ta metoda nie istnieje. Dlaczego w ogóle chcesz żeby ten setter był prywatny?

Wiem i uzywam go tylko i wyłącznie wewnątrz klasy MathSet, więc nie wiem o co chodzi?
Potrzebujego bo muszę zrobić przypisania w metodzie - i * .

class MathSet
  attr_reader :elements

  def initialize(*e)
    @elements = e.uniq;
  end

  def count
    @elements.length
  end

  def +(e)
    if e.is_a? MathSet
      e.elements.each do |elem|
        @elements << elem unless @elements.include?(elem)
      end
      self
    else
      @elements << e unless @elements.include?(e);
      self
    end
  end

  def -(e)
    if e.is_a? MathSet
      new_set = MathSet.new
      new_set.elements = @elements.reject { |elem| e.elements.include?(elem)}
      new_set
    else
      if @elements.include?(e)
        @elements.delete(e)
        self
      else
        raise 'Element does not exist in this set!'
      end
    end
  end

  def *(set)
    if set.is_a? MathSet
      new_set = MathSet.new
      new_set.elements = @elements.select{ |elem| set.elements.include?(elem)}
      new_set
    else
      raise ArgumentError ,'Argument is not a set!'    
    end
  end

  private
    attr_writer :elements

end

Uściślając: jeśli coś jest prywatne, to możesz się do tego odwołać tylko z wewnątrz tej samej klasy oraz tylko w obrębie tego samego obiektu. Cytując podręcznik: Każdy obiekt danej klasy może wywoływać metody prywatne tylko na rzecz samego siebie.

W linijce 26 tworzysz nowy obiekt MathSet (new_set), i linijkę niżej próbujesz użyć jego prywatnego settera elements z zewnątrz (czyli z aktualnego obiektu):
new_set.elements = @elements.reject { |elem| e.elements.include?(elem)}
Stąd błąd.

Masz dwa wyjścia:

  1. Zamiast private, użyj protected. Ten modyfikator działa właśnie tak, że pozwala na dostęp do metody (tutaj: settera) wszystkim obiektom danej klasy.

  2. Zamiast używać settera w linijce 27 (i analogicznie dla operatora *), utwórz nowy obiekt MathSet z nowymi elementami przekazanymi w konstruktorze:

    new_elements = @elements.reject { |elem| e.elements.include?(elem) }
    MathSet.new(new_elements)
    

    Dzięki temu będziesz mógł zostawić ten setter prywatny.

1 Like

Dzięki teraz już wiem o co chodzi :wink:

Do tego nie testuje sie prywatnych metod. Testuj interfejsy wyzszego poziomu

Wiem i nie testuję, dlatego właśnie nie wiedziałem czemu tak się dzieje.
Mój spec:

require_relative '../math_set'

describe MathSet do
  
  describe '#count' do
    before(:all) do
      @set1 = MathSet.new
      @set2 = MathSet.new(1,2,3,3)
    end

    it 'returns 0 when empty set' do
      expect(@set1.count).to eq 0
    end

    it 'returns set size' do
      expect(@set2.count).to eq 3
    end
  end

  describe '#+' do
    
    before(:each) do
      @num = 5
      @num1 = 3
      @num2 = 4
      @set1 = MathSet.new
      @set2 = MathSet.new(@num1,@num2)
    end

    it 'adds element to set' do
      @set1 + @num
      expect(@set1.elements).to include(@num) 
    end

    it 'unites two sets' do
      set3 = @set1 + @set2
      expect(set3.elements).to include(@num1)
      expect(set3.elements).to include(@num2)
    end

    it 'does not repeat same elements' do
      num1 = 3
      num2 = 5
      set = MathSet.new(num1,num2)
      set = set + @set2
      expect(set.elements).to include(num2)
      expect(set.count).to eq 3
    end

  end

  describe '#-' do
    before(:each) do
      @set1 = MathSet.new(2,3)
      @num1 = 3
      @not_existing_num = 7
    end

    describe 'element' do
      
      describe 'if element exists in set' do
        
        it 'removes it' do
          @set1 - @num1
          expect(@set1.elements).to_not include(@num1)
        end

      end

      describe 'if element does not exits in set' do
        it 'raises an exception' do
          expect{@set1 - @not_existing_num}.to raise_error('Element does not exist in this set!')
        end
      end
    
    end

    describe 'sets' do
      before(:each) do
        @num = [1,2,3,4]
        @set1 = MathSet.new(@num[1],@num[3],@num[2])
        @set2 = MathSet.new(@num[1],@num[0])
      end

      it 'substracts two sets' do
        set3 = @set1 - @set2
        expect(set3.elements).to include(@num[3])
        expect(set3.elements).to include(@num[2])
        expect(set3.elements).to_not include(@num[1])
      end

    end
  end

  describe '#*' do
    before(:each) do
      @num = [1,2,3,4,5,6]
      @set1 = MathSet.new(@num[1],@num[3],@num[2],@num[5])
      @set2 = MathSet.new(@num[1],@num[0],@num[5],@num[2],@num[4])
    end

    describe 'sets' do
      it 'returns intersection of two sets' do
        set = @set1 * @set2
        expect(set.elements).to include(@num[1])
        expect(set.elements).to include(@num[5])
        expect(set.elements).to include(@num[2])
        expect(set.count).to eq 3
      end
    end

    describe 'sth different' do
      it 'raises ArgumentError' do
         expect{ @set1 * ""}.to raise_error('Argument is not a set!')
      end
    end
  end

end

Sporo mozna by te testy “ulepszyc” na poczatku poczytaj o let i let! to juz duzo pomoze

Ja bym w ogóle nie wchodził w let i let! w tych testach. NIe ma praktycznie żadnych przesłanek dla których @set1, @set2 i im podobne muszą być tworzone w ogólnym kontekście. To się wydaje wygodne. Ale potem jesteś w 70 lini pliku testowego i musisz szukać po kaskadach kontekstów, letów i beforów w jakim tak naprawdę jesteś stanie. W zamian tego możesz sobie stworzyć stan w teście i wtedy od razu patrząc na test wiesz dokładnie co się dzieje. Najlepiej jeszcze zdefiniować helper metody dzięki którym lepiej się czyta test.

Więc zamiast tego

it 'returns 0 whhen empty set' do
      expect(@set1.count).to eq 0
end

I patrząc na to nie masz pojęcia co to jest @set1 i trzeba iść w górę i szukać w jakim jesteś kontekście, możesz zrobić:

it 'returns 0 when empty set' do
  empty_set = build_math_set([])
  expect(empty_set.count).to eq(0)
end

Wszystko jasne jak słońce.

1 Like

Dzięki za porady :slight_smile: dopiero zaczynam z RSpec’kiem :wink: