Any fool can make things bigger, more complex, and more violent. It takes a touch of genius and a lot of courage to move in the opposite direction. Albert Einstein

o Ruby

Nieinwazyjny monkey-patching

8 komentarzy | Kategorie: Ruby, Techblog, Tips & tricks | trackback
Tagi:

Ostatnimi czasy cicho w polskiej blogosferze programistów Rubiego. Mój blog też nie jest wyjątkiem. Zdaje się czas jest sporym problemem, bo o czym pisać to jest... No nieważne :). Dzisiaj krótko, ale że kiedyś obiecałem sobie pisać nie tylko długo i treściwie...

monkey-patching wcieleniem zła?

Monkey-patching to technika, w której pozwalamy sobie na wprowadzanie zmiany w cudzym kodzie, na przykład podmianę istniejącej metody, dodanie nowej. W społeczności Rubiego ta technika jest stosunkowo popularna, z kolei Pythonistas jej nie lubią. Tym razem nie będę zagłębiać się w szczegóły i rozstrzygał czy ta technika jest dobra, zła, niebezpieczna (być może kiedyś). Chciałbym pokazać prosty sposób na nieinwazyjne ulepszanie istniejących klas/metod.

W moim kodzie chciałem użyć metody Array#index. Metoda zwraca index w tablicy podanego obiektu lub nil jeśli elementu nie znaleziono.

a = [3, 7, 2, 10]
puts a.index(7) # => 1

Jednak jak się okazało metoda ta nie obsługuje bloków, zatem poniższy kod nie zadziała.

a = [4, 8, 10, 7, 1]
puts a.index {|e| e % 2 == 1 } # szukamy indeksu pierwszej nieparzystej liczby

monkey-patching na ratunek

W prosty sposób możemy tą dziurę załatać. Należy jednak dobrze przemyśleć strategię. Najważniejsze jest to, żeby nie zepsuć aktualnej wersji.

class Array
  alias_method :__old_index__, :index

  def index(obj = nil)
    if obj.nil?
      self.each_with_index do |e, i|
        return i if yield(e)
      end
      return nil
    else
      return __old_index__(obj)
    end
  end
end

a = [4, 8, 10, 7, 1]
puts a.index {|e| e % 2 == 1 } # szukamy indeksu pierwszej nieparzystej liczby

Myślę, że kod jest prosty w zrozumieniu, jednakże dla tych mniej wprawnych programistów Rubiego pozwolę sobie na mały opis. Poprzez alias_method tworzymy alias do metody index, tak by móc do niej się potem odwołać. Następnie nadpisujemy metodę index, zauważ, że parametr jest opcjonalny (obj = nil). Jest to przygotowanie do wersji z blokiem. W ciele metody sprawdzamy czy parametr został podany. Jeśli nie (obj.nil? == true) to wykona się kod obsługujący blok, w przeciwnym wypadku odpalamy starą wersję metody index poprzez jej alias.

W Rubym 1.8.7 (już niedługo oficjalne wydanie) i 1.9 poprawiono wiele metod w tym i Array#index by obsługiwały blok. Można zatem dodać kod sprawdzający obsługę bloku i nie dodawać tej metody w takim wypadku.

class Array
  begin
    [1].index {|e| e == 1}
  rescue ArgumentError
    puts "(monkey-)patching Array#index (RUBY_VERSION = #{RUBY_VERSION})"
    alias_method :__old_index__, :index

    def index(obj = nil)
      if obj.nil?
        self.each_with_index do |e, i|
          return i if yield(e)
        end
        return nil
      else
        return __old_index__(obj)
      end
    end
  end

end

a = [4, 8, 10, 7, 1]
puts a.index {|e| e % 2 == 1 } # szukamy indeksu pierwszej nieparzystej liczby

Jeśli spodobał Ci się wpis to może umieścisz ten blog w swoim czytniku RSS?

Komentarze

1. avatar icon Seban napisał(a) 27 Maj 2008 o godz. 19:50:

Masz rację! Ja również bardzo sobie cenię otwarte klasy w Rubim.
Na goldenline.pl na grupie języków skryptowych była (cały czas jest) dyskusja o tym

2. avatar icon Radarek napisał(a) 27 Maj 2008 o godz. 20:08:

@Seban, mógłbyś zapodać linkiem?

3. avatar icon Sebastian Nowak napisał(a) 27 Maj 2008 o godz. 20:09:

Mówisz, masz: http://www.goldenline.pl/forum/jezyki-skryptowe/325194

4. avatar icon comboy napisał(a) 27 Maj 2008 o godz. 21:18:

(Komentarz zmodyfikowany 16.10.2008 o 17:27)

mozna tez z finda skorzystać:

  def index(obj=nil,&block)
      return __old_index__(find(&block)) if block_given?
      return __old_index__(obj)
  end
5. avatar icon RazorJack napisał(a) 29 Maj 2008 o godz. 13:41:

Kodując w Railsach pewien system e-commerce, zacząłem mieć nagle bardzo dziwne błędy związane ze stringami. Okazało się, że pewna (nie pamiętam która – od razu wyleciała z hukiem) biblioteka od PDF-ów sobie monkey-pacznęła klasę String. Konsekwencje były opłakane, ale dało rade szybko naprawić.

Istnieją inne języki niż Ruby, które umożliwiają monkey-patching, ale społeczności niektórych z nich uważają taki zabieg za poniżający, za ostateczność.

Może i monkey-patching umożliwia sprytne i szybkie naprawienie/dodanie jakiegoś ficzera, ale moim zdaniem nie powinno się tak entuzjastycznie reklamować tej możliwości w Rubim. Programiści bywają bardzo nieodpowiedzialni, żonglując fajerwerkami Rubiego.

6. avatar icon Radarek napisał(a) 29 Maj 2008 o godz. 13:49:

Jak najbardziej się z Tobą @RazorJack zgadzam. To tylko utwierdza mnie w przekonaniu, że efektywne i świadome korzystanie z możliwości Rubiego wymaga dosyć dogłębnego poznania go. Osobiście nie uznaję monkey-patchingu jako leku na wszelkie zło, a nawet denerwuje mnie jeśli widzę w cudzych bibliotekach jeśli klasy wbudowane są przy byle okazji modyfikowane (a można daną funkcjonalność umieścić w swoich klasach). Jednak umiarkowane stosowanie we własnych końcowych aplikacjach (tak jak pokazałem we wpisie) wydaje się ok :).

7. avatar icon Drogomir napisał(a) 31 Maj 2008 o godz. 22:55:

Możesz napisać o alias_method_chain w railsach :) Tego jest od groma w kodzie railsów i w sumie się przydaje często. Sam miałem napisać, ale cholera czasu mało ostatnio…

8. avatar icon Adam Byrtek napisał(a) 02 Cze 2008 o godz. 14:09:

Symbol :__old_index__ sugeruje pythonowe inspiracje :)

Dodaj coś od siebie

Możesz korzystać ze składni Textile.

Pola oznaczone * są wymagane.

Proszę o dodawanie komentarzy związanych z tematem postu, sprawy osobiste proszę załatwiać przez maila bądź gg.

Zastrzegam sobie prawo do moderacji komentarzy (edycja, usuwanie).