Jeśli nie monkey patching, to co?

Monkey patching to dobra praktyka?
A może trzymać się zasady, że nie modyfikuje się klas których się nie stworzyło?
Jestem ciekawy opinii doświadczonych programistów.

Powiedzmy że mamy metodę push_uniq - dodającą element do tablicy jedynie wtedy gdy go tam jeszcze nie ma i zwracającą indeks tegoż elementu:

[code]class Array

def push_uniq e
if include? e
index e
else
push e
size-1
end
end

end[/code]
Jak przerobić ten kawałek kodu na równie eleganckie rozwiązanie bez monkey patchingu?

W tym wypadku lepiej skorzystać z wbudowanych w rubego bibliotek. W rubym masz klasę Set, która odwzorowuje zbiór, w którym każda wartość jest unikalna.

[code]require ‘set’

set = Set.new
set.add(1)
set.add(1)
set.size // 1[/code]

Set odpada, bo zbiór jest uporządkowany(metoda ma zwracać index dodanego elementu).
Zresztą nie chodzi mi tu o ten jeden konkretny przypadek. Raczej o całą klasę podobnych sytuacji.
Inny przykład:

class Array def tail [1..-1] end end
Chcę dodać taki “syntactic sugar” do swojego programu bez monkey patching. Jak to zrobić aby było elegancko, zwięźle i bezpiecznie?

[quote=knife]Set odpada, bo zbiór jest uporządkowany(metoda ma zwracać index dodanego elementu).
Zresztą nie chodzi mi tu o ten jeden konkretny przypadek. Raczej o całą klasę podobnych sytuacji.[/quote]
Jeśli chodzi o całą klasę sytuacji to tworzysz nową klasę w Ruby:http://stackoverflow.com/questions/773403/ruby-want-a-set-like-object-which-preserves-order

[code]class UniqueArray < Array

def add e
i = index e
if i
i
else
push e
size-1
end
end
end[/code]
Ok, pomysł z klasą dziedziczącą może i w tym przypadku jest uzasadniony, choć pewnie trzeba napisać dużo kodu, który będzie gwarantował, że nie da się dodać do takiej tablicy elementu, który już się w niej znajduje - a to jest komplikowanie dosyć prostej funkcjonalności.

Nie zmienia to faktu, że jest mnóstwo sytuacji gdzie dana metoda naturalnie pasuje do istniejącej już klasy. Dziedziczenie w wielu sytuacjach jest zbyt rozwlekłe:

[code]class MyArray < Array
def tail
[1…-1]
end
end

MyArray.from_array(array).tail[/code]
Jest jakiś lepszy sposób niż monkey patching i dziedziczenie?

  1. Możesz użyć delegacji:

[code=ruby]class MyClass
extend Forwardable

def_delegators :@array, :[], :size # itd

def initialize(array)
@array = array
end

def my_foo_method
end
end[/code]
http://apidock.com/ruby/Forwardable

  1. Możesz użyć modułu dla klasy:

[code=ruby]module MyMethods
def my_foo_method
end
end

class Array
include MyMethods
end[/code]
Zaletą tego rozwiązania jest możliwość wyśledzenia ingerencji:

Array.ancestors # zwróci MyMethods
  1. Albo rozszerzasz tylko obiekt:

[code=ruby]class MyClass

attr_reader :array

def initialize(array)
@array = array
@array.extend(MyMethods)
end

end[/code]
Również możesz wyśledzić ingerencje:

array.singleton_class.ancestors # zwróci MyMethods

Rozwiązania oparte na tworzeniu nowej klasy - potrzeba konwertowania z jednej klasy na drugą.
Includowanie modułu do klasy - tak samo niebezpieczne jak monkey patching

Najlepszym rozwiązaniem jest chyba coś w stylu:

[code]module ArrayX
def tail
self[1…-1]
end
end

[1,2,3].extend(ArrayX).tail[/code]
Dzięki wszystkim za pomoc.

Tak przy okazji

czyli, że co? DCI znów górą? :wink:

Sprawdź najpierw czy poszukiwanej przez Ciebie metody nie ma w Ruby Facets.
Używanie monkey patchingu ma sens jeśli przyjmujesz, że będziesz używał tego w wielu projektach i że nazwa nie będzie kolidowała z czymś co zrobił ktoś inny.
Jeśli tak nie jest, to lepiej zrobić po prostu metody w kalsie powiedzy ArrayUtils:

class ArrayUtils def self.push_uniq(array,element) if array.include?(element) array.index(element) else array.push(element) array.size-1 end end end a = [1,2,3] ArrayUtils.push_uniq(a,2)
Oczywiście jest to znacznie mniej eleganckie, ale na pewno znacznie bezpieczniejsze.

Moim zdaniem wszystko zależy od konkretnej metody. Ja tail dopisałbym bezpośrednio do Array, tak jak np. second czy last, ale dla uniq zrobiłbym już nową klasę, UniqueArray. Trochę więcej pracy, ale dużo bardziej przyszłościowe podejście.

UPDATE:

A sama zamiana Array na UniqueArray wcale nie musi być kosztowna w przypadku podejścia z delgacją.

Przy kodzie, który podał Sławosz UniqueArray byłaby wrapperem na Array (mozesz dodać Enumerable, żeby była to pełniejsza implementacja), więc jedyne co trzeba zrobić, żeby taka tablica stała się unikalna, to UniqueArray.new(array). Kłopot z dodaniem push_uniq jest taki, że nie masz pewności czy tablica, którą dostajesz ma unikalne elementy, więc w teorii ten kod jest trudniejszy do utrzymania.

Oczywiście wszystko też zależy od sytuacji, ale to chyba jest najbardziej elastyczne podejście.

UPDATE2:

Trochę zaspany jestem, w przypadku tworzenia wrappera najładniej byłoby tablicę sklonować, żeby później nie było niemiłych niespodzianek. Chociaż to też zależy od tego jak ktoś by chciał tego używać.

Ciekawostka z biblioteki standardowej:

[quote=“set.rb”]# This library provides the Set class, which deals with a collection

of unordered values with no duplicates. It is a hybrid of Array’s

intuitive inter-operation facilities and Hash’s fast lookup. If you

need to keep values ordered, use the SortedSet class.[/quote]

Tyle, że implementacja tego SortedSeta to jakaś mocna kaszana.

Ogólnie starałbym się zaimplementować tę unikalną tablicę podobnie do Set-a.

Rzeczywiście nie ma potrzeby dodawania metody push_uniq do Array.
Po przemyśleniu sprawy ostatecznie zaimplementuje to w ten sposób:

[code]class OrderedSet

def initialize
@size= 0
@store = {}
end

def push e
if index = @store[e]
index
else
@size = @size+1
@store[e] = @size-1
end
end

def to_a
@store.keys
end

end[/code]

Hash jest ordered od 1.9, afair, więc na 1.8 to nie będzie działać, #to_a odda posortowaną tablicę.

W grę wchodzi coś takiego:

def to_a @store.each_pair.sort_by{ |k, v| v }.map(&:first) end