Zaleta metody gwintowania w kolorze rubinowym

116

Właśnie czytałem artykuł na blogu i zauważyłem, że autor użył tapwe fragmencie czegoś takiego:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

Moje pytanie brzmi: jaka dokładnie jest korzyść lub korzyść z używania tap ? Nie mogłem po prostu zrobić:

user = User.new
user.username = "foobar"
user.save!

Lub jeszcze lepiej:

user = User.create! username: "foobar"
Kyle Decot
źródło

Odpowiedzi:

103

Kiedy czytelnicy napotykają:

user = User.new
user.username = "foobar"
user.save!

musieliby podążać za wszystkimi trzema liniami, a następnie rozpoznać, że jest to tylko tworzenie instancji o nazwie user .

Gdyby to było:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

wtedy byłoby to natychmiast jasne. Czytelnik nie musiałby czytać zawartości bloku, aby wiedzieć, że instancja userzostała utworzona.

sawa
źródło
3
@Matt: A także odrzuć wszystkie definicje zmiennych utworzone w procesie, gdy blok wykona swoje zadanie. A jeśli na obiekcie zostanie wywołana tylko jedna metoda, możesz napisaćUser.new.tap &:foobar
Boris Stitnicky
28
Nie uważam tego użycia za zbyt przekonujące - prawdopodobnie nie jest bardziej czytelne, dlatego znalazłem się na tej stronie. Bez mocnego argumentu dotyczącego czytelności porównałem szybkość. Moje testy wskazują na 45% dodatkowe środowisko uruchomieniowe dla prostych implementacji powyższego, malejące wraz ze wzrostem liczby seterów w obiekcie - około 10 lub więcej z nich, a różnica w czasie wykonywania jest znikoma (YMMV). „sięgnięcie” do łańcucha metod podczas debugowania wydaje się wygraną, w przeciwnym razie potrzebuję więcej, aby mnie przekonać.
dinman2022
7
Myślę, że user = User.create!(username: 'foobar')w tym przypadku byłoby najwyraźniej i najkrócej :) - ostatni przykład z pytania.
Lee,
4
Ta odpowiedź jest sprzeczna z samą sobą, a zatem nie ma sensu. Dzieje się więcej niż „tylko tworzenie instancji o nazwie user”. Również argument, że „Czytelnik nie musiałby czytać zawartości bloku, aby wiedzieć, że instancja userzostała utworzona”. nie ma żadnej wagi, ponieważ w pierwszym bloku kodu czytelnik musi również przeczytać tylko pierwszą linię, „aby wiedzieć, że instancja userzostała utworzona”.
Jackson
5
Dlaczego więc tu jestem? Dlaczego wszyscy tu szukamy tego, co jest dotknięciem.
Eddie
37

Innym przypadkiem użycia kranu jest wykonanie manipulacji na obiekcie przed jego zwróceniem.

Więc zamiast tego:

def some_method
  ...
  some_object.serialize
  some_object
end

możemy zapisać dodatkową linię:

def some_method
  ...
  some_object.tap{ |o| o.serialize }
end

W niektórych sytuacjach ta technika może zaoszczędzić więcej niż jedną linię i sprawić, że kod będzie bardziej zwarty.

megas
źródło
24
Byłbym jeszcze bardziej drastyczny:some_object.tap(&:serialize)
amencarini
28

Korzystanie z kranu, tak jak zrobił to blogger, jest po prostu wygodną metodą. W twoim przykładzie mogło to być przesadą, ale w przypadkach, gdy chcesz zrobić kilka rzeczy z użytkownikiem, dotknięcie może zapewne zapewnić bardziej przejrzysty interfejs. Może więc lepiej będzie na poniższym przykładzie:

user = User.new.tap do |u|
  u.build_profile
  u.process_credit_card
  u.ship_out_item
  u.send_email_confirmation
  u.blahblahyougetmypoint
end

Użycie powyższego ułatwia szybkie sprawdzenie, że wszystkie te metody są zgrupowane razem, ponieważ wszystkie odnoszą się do tego samego obiektu (użytkownika w tym przykładzie). Alternatywą byłoby:

user = User.new
user.build_profile
user.process_credit_card
user.ship_out_item
user.send_email_confirmation
user.blahblahyougetmypoint

Ponownie, jest to dyskusyjne - ale można stwierdzić, że druga wersja wygląda trochę bardziej niechlujnie i wymaga trochę bardziej ludzkiego analizowania, aby zobaczyć, że wszystkie metody są wywoływane na tym samym obiekcie.

Rebitzele
źródło
2
To jest tylko dłuższy przykład tego, co OP już zadał w swoim pytaniu. Nadal możesz zrobić to wszystko z user = User.new, user.do_something, user.do_another_thing... czy mógłbyś wyjaśnić, dlaczego można to zrobić?
Matt,
Chociaż przykład jest w zasadzie ten sam, pokazując go w dłuższej formie, można zobaczyć, jak użycie kranu może być bardziej estetyczne w tym przypadku. Dodam zmianę, aby pomóc w zademonstrowaniu.
Rebitzele,
Ja też tego nie widzę. tapZ mojego doświadczenia wynika, że używanie nigdy nie przyniosło żadnych korzyści. Tworzenie i praca ze userzmienną lokalną jest moim zdaniem znacznie czystsza i czytelna.
gylaz
Te dwa nie są równoważne. Jeśli to zrobiłeś, u = user = User.newa następnie użyłeś go udo wywołań konfiguracji, byłoby to bardziej zgodne z pierwszym przykładem.
Gerry
26

Może to być przydatne przy debugowaniu serii ActiveRecordpołączonych zakresów.

User
  .active                      .tap { |users| puts "Users so far: #{users.size}" } 
  .non_admin                   .tap { |users| puts "Users so far: #{users.size}" }
  .at_least_years_old(25)      .tap { |users| puts "Users so far: #{users.size}" }
  .residing_in('USA')

To sprawia, że ​​bardzo łatwo jest debugować w dowolnym punkcie łańcucha bez konieczności przechowywania czegokolwiek w zmiennej lokalnej ani konieczności znacznej zmiany oryginalnego kodu.

I wreszcie, użyj go jako szybkiego i dyskretnego sposobu debugowania bez zakłócania normalnego wykonywania kodu :

def rockwell_retro_encabulate
  provide_inverse_reactive_current
  synchronize_cardinal_graham_meters
  @result.tap(&method(:puts))
  # Will debug `@result` just before returning it.
end
Gupta
źródło
14

Wizualizuj swój przykład w funkcji

def make_user(name)
  user = User.new
  user.username = name
  user.save!
end

Takie podejście wiąże się z dużym ryzykiem związanym z konserwacją, w zasadzie z niejawną wartością zwrotu .

W tym kodzie zależy od save!zwrócenia zapisanego użytkownika. Ale jeśli użyjesz innej kaczki (lub twoja obecna ewoluuje), możesz otrzymać inne rzeczy, takie jak raport o stanie ukończenia. W związku z tym zmiany w kaczce mogą spowodować uszkodzenie kodu, co by się nie zdarzyło, gdyby wartość zwracana była podawana zwykłymuser dotknięciem lub użyj.

Widziałem takie wypadki dość często, szczególnie w przypadku funkcji, w których wartość zwracana zwykle nie jest używana, z wyjątkiem jednego ciemnego, błędnego rogu.

Niejawna wartość zwracana jest zwykle jedną z tych rzeczy, w których nowicjusze mają tendencję do przerywania rzeczy, dodając nowy kod po ostatniej linii, nie zauważając efektu. Nie widzą, co naprawdę oznacza powyższy kod:

def make_user(name)
  user = User.new
  user.username = name
  return user.save!       # notice something different now?
end
SystematicFrank
źródło
1
Nie ma absolutnie żadnej różnicy między twoimi dwoma przykładami. Czy chciałeś wrócić user?
Bryan Ash
1
To był jego punkt widzenia: przykłady są dokładnie takie same, jeden jest po prostu wyraźny co do zwrotu. User.new.tap{ |u| u.username = name; u.save! }
Chodziło
14

Jeśli chcesz zwrócić użytkownika po ustawieniu nazwy użytkownika, musisz to zrobić

user = User.new
user.username = 'foobar'
user

Z tapwas może uratować tę niezręczną powrót

User.new.tap do |user|
  user.username = 'foobar'
end
montrealmike
źródło
1
To jest Object#tapdla mnie najczęstszy przypadek użycia .
Lyndsy Simon
1
Cóż, zapisałeś zero wierszy kodu, a teraz, patrząc na koniec metody, aby sprawdzić, co zwraca, muszę przeskanować ponownie, aby zobaczyć, że blok jest blokiem #tap. Nie jestem pewien, czy to jest jakakolwiek wygrana.
Irongaze.com
być może, ale może to łatwo być 1 liniowiec user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder
11

Skutkuje to mniejszym bałaganem w kodzie, ponieważ zakres zmiennej jest ograniczony tylko do części, w której jest naprawdę potrzebna. Ponadto wcięcie w bloku sprawia, że ​​kod jest bardziej czytelny dzięki przechowywaniu razem odpowiedniego kodu.

Opis tapmówi :

Poddaje się blokowi, a następnie zwraca siebie. Głównym celem tej metody jest „wykorzystanie” łańcucha metod w celu wykonania operacji na wynikach pośrednich w łańcuchu.

Jeśli przeszukamy kod źródłowy railsów pod kątem tapużycia , możemy znaleźć kilka interesujących zastosowań. Poniżej znajduje się kilka pozycji (lista nie jest wyczerpująca), które dadzą nam kilka pomysłów, jak z nich korzystać:

  1. Dołącz element do tablicy na podstawie określonych warunków

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Inicjowanie tablicy i zwracanie jej

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Jako cukier syntaktyczny, aby uczynić kod bardziej czytelnym - w poniższym przykładzie można powiedzieć, że użycie zmiennych hashi serversprawia, że ​​intencja kodu jest jaśniejsza.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Inicjuj / wywołuj metody na nowo utworzonych obiektach.

    Rails::Server.new.tap do |server|
       require APP_PATH
       Dir.chdir(Rails.application.root)
       server.start
    end

    Poniżej przykład z pliku testowego

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Działanie na wyniku yieldwywołania bez konieczności używania zmiennej tymczasowej.

    yield.tap do |rendered_partial|
      collection_cache.write(key, rendered_partial, cache_options)
    end
Wand Maker
źródło
9

Odmiana odpowiedzi @ sawa:

Jak już wspomniano, używanie tap pomaga zorientować się w przeznaczeniu kodu (niekoniecznie czyniąc go bardziej zwartym).

Następujące dwie funkcje są równie długie, ale w pierwszej musisz przeczytać do końca, aby dowiedzieć się, dlaczego na początku zainicjowałem pusty hash.

def tapping1
  # setting up a hash
  h = {}
  # working on it
  h[:one] = 1
  h[:two] = 2
  # returning the hash
  h
end

Z drugiej strony, od samego początku wiesz, że inicjowany skrót będzie wyjściem bloku (aw tym przypadku wartością zwracaną przez funkcję).

def tapping2
  # a hash will be returned at the end of this block;
  # all work will occur inside
  Hash.new.tap do |h|
    h[:one] = 1
    h[:two] = 2
  end
end
Giuseppe
źródło
to zastosowanie tapdaje bardziej przekonujący argument. Zgadzam się z innymi, że kiedy widzisz user = User.new, zamiar jest już jasny. Anonimowa struktura danych może być jednak wykorzystana do wszystkiego, a tapmetoda przynajmniej jasno pokazuje, że to struktura danych jest głównym elementem metody.
volx757
Nie jestem pewien, czy ten przykład jest lepszy, a benchmarking w porównaniu z def tapping1; {one: 1, two: 2}; endprogramami .tap
używanymi
9

To pomocnik w tworzeniu łańcuchów połączeń. Przekazuje swój obiekt do danego bloku i po jego zakończeniu zwraca obiekt:

an_object.tap do |o|
  # do stuff with an_object, which is in o #
end  ===> an_object

Zaletą jest to, że tap zawsze zwraca obiekt, do którego jest wywoływany, nawet jeśli blok zwraca inny wynik. W ten sposób można wstawić blok kranu w środek istniejącego potoku metody bez przerywania przepływu.

Pushp Raj Saurabh
źródło
8

Powiedziałbym, że nie ma żadnej korzyści z używania tap. Jedyna potencjalna korzyść, jak wskazuje @sawa jest, cytuję: „Czytelnik nie musiałby czytać zawartości bloku, aby wiedzieć, że utworzono instancję użytkownika”. Jednak w tym momencie można wysunąć argument, że jeśli robisz nieprostą logikę tworzenia rekordów, twój zamiar byłby lepiej przekazany poprzez wyodrębnienie tej logiki do własnej metody.

Uważam, że tapjest to niepotrzebne obciążenie dla czytelności kodu i można by to zrobić bez lub zastąpić lepszą techniką, taką jak Extract Method .

Chociaż tapjest to wygodna metoda, to także osobiste preferencje. Dać tapszansę. Następnie napisz kod bez użycia tap, zobacz, czy podoba ci się jeden sposób.

gylaz
źródło
4

Mogłoby istnieć wiele zastosowań i miejsc, z których moglibyśmy skorzystać tap. Jak dotąd znalazłem tylko następujące 2 zastosowaniatap .

1) Głównym celem tej metody jest skorzystanie z łańcucha metod w celu wykonania operacji na wynikach pośrednich w łańcuchu. to znaczy

(1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
    tap    { |x| puts "array: #{x.inspect}" }.
    select { |x| x%2 == 0 }.
    tap    { |x| puts "evens: #{x.inspect}" }.
    map    { |x| x*x }.
    tap    { |x| puts "squares: #{x.inspect}" }

2) Czy kiedykolwiek zdarzyło Ci się wywołać metodę na jakimś obiekcie, a wartość zwracana nie była tym, czego chciałeś? Może chciałeś dodać dowolną wartość do zestawu parametrów przechowywanych w hashu. Aktualizujesz go za pomocą Hash. [] , Ale otrzymujesz back bar zamiast hash params, więc musisz go jawnie zwrócić. to znaczy

def update_params(params)
  params[:foo] = 'bar'
  params
end

Aby przezwyciężyć tę sytuację, tapw grę wchodzi metoda. Po prostu wywołaj to na obiekcie, a następnie przekaż stuknij blok z kodem, który chciałeś uruchomić. Obiekt zostanie przekazany do bloku, a następnie zostanie zwrócony. to znaczy

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

Istnieją dziesiątki innych przypadków użycia, spróbuj znaleźć je sam :)

Źródło:
1) Dock Object API API
2) pięć metod-ruby-powinieneś-używać

Aamir
źródło
3

Masz rację: użycie tap w twoim przykładzie jest trochę bezcelowe i prawdopodobnie mniej czyste niż twoje alternatywy.

Jak zauważa Rebitzele, tap jest to tylko wygodna metoda, często używana do tworzenia krótszych odniesień do bieżącego obiektu.

Jednym z dobrych przypadków użycia tapjest debugowanie: możesz zmodyfikować obiekt, wydrukować bieżący stan, a następnie kontynuować modyfikowanie obiektu w tym samym bloku. Zobacz na przykład: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

Czasami lubię używać tapmetod wewnętrznych, aby warunkowo powrócić wcześniej, podczas gdy w przeciwnym razie zwracam bieżący obiekt.

Jacob Brown
źródło
Jest to również aplikacja wspomniana w dokumentach: ruby-doc.org/core-2.1.3/Object.html#method-i-tap
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3

Istnieje narzędzie o nazwie flog, które mierzy, jak trudno jest odczytać metodę. „Im wyższy wynik, tym większy problem z kodem”.

def with_tap
  user = User.new.tap do |u|
    u.username = "foobar"
    u.save!
  end
end

def without_tap
  user = User.new
  user.username = "foobar"
  user.save!
end

def using_create
  user = User.create! username: "foobar"
end

i zgodnie z wynikiem bata metoda tapjest najtrudniejsza do odczytania (i zgadzam się z tym)

 4.5: main#with_tap                    temp.rb:1-4
 2.4:   assignment
 1.3:   save!
 1.3:   new
 1.1:   branch
 1.1:   tap

 3.1: main#without_tap                 temp.rb:8-11
 2.2:   assignment
 1.1:   new
 1.1:   save!

 1.6: main#using_create                temp.rb:14-16
 1.1:   assignment
 1.1:   create!
Evmorov
źródło
1

Możesz uczynić swoje kody bardziej modułowymi za pomocą dotknięcia i uzyskać lepsze zarządzanie zmiennymi lokalnymi. Na przykład w poniższym kodzie nie musisz przypisywać zmiennej lokalnej do nowo utworzonego obiektu w zakresie metody. Zwróć uwagę, że zmienna bloku u jest objęta zakresem w obrębie bloku. W rzeczywistości jest to jedna z piękności kodu ruby.

def a_method
  ...
  name = "foobar"
  ...
  return User.new.tap do |u|
    u.username = name
    u.save!
  end
end
user3936126
źródło
1

W railsach możemy tapjawnie dodać parametry do białej listy:

def client_params
    params.require(:client).permit(:name).tap do |whitelist|
        whitelist[:name] = params[:client][:name]
    end
end
Ashan Priyadarshana
źródło
1

Podam inny przykład, którego użyłem. Mam metodę user_params, która zwraca parametry potrzebne do zapisania dla użytkownika (to jest projekt Rails)

def user_params
  params.require(:user).permit(
    :first_name,
    :last_name,
    :email,
    :address_attributes
  )
end

Możesz zobaczyć, że nie zwracam niczego, ale ruby ​​zwraca wynik ostatniej linii.

Po jakimś czasie musiałem warunkowo dodać nowy atrybut. Więc zmieniłem to na coś takiego:

def user_params 
  u_params = params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  )
  u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  u_params
end

Tutaj możemy użyć tap, aby usunąć lokalną zmienną i usunąć powrót:

def user_params 
  params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  ).tap do |u_params|
    u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  end
end
Rubyprince
źródło
1

W świecie, w którym wzorce programowania funkcjonalnego stają się najlepszą praktyką ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), można zobaczyć tap, jako mapna pojedynczej wartości, rzeczywiście , aby zmodyfikować dane w łańcuchu transformacji.

transformed_array = array.map(&:first_transformation).map(&:second_transformation)

transformed_value = item.tap(&:first_transformation).tap(&:second_transformation)

Nie trzeba itemtutaj wielokrotnie deklarować .

Augustin Riedinger
źródło
0

Jaka jest różnica?

Różnica w zakresie czytelności kodu jest czysto stylistyczna.

Kod Przejdź przez:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

Kluczowe punkty:

  • Zwróć uwagę, jak plik u zmienna jest teraz używana jako parametr bloku?
  • Po wykonaniu bloku plik user zmienna powinna teraz wskazywać na użytkownika (z nazwą użytkownika: „foobar” i który również jest zapisany).
  • Jest po prostu przyjemny i łatwiejszy do odczytania.

Dokumentacja API

Oto łatwa do odczytania wersja kodu źródłowego:

class Object
  def tap
    yield self
    self
  end
end

Aby uzyskać więcej informacji, zobacz te linki:

https://apidock.com/ruby/Object/tap

http://ruby-doc.org/core-2.2.3/Object.html#method-i-tap

BKSpurgeon
źródło