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?
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?
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.
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ć.