Monkey patching и Duck typing в Ruby: а проблема ли это самого языка?
Много разговоров и плевков в адреc Ruby из-за механизмов monkey patching и duck typing. Всё дело в том, что язык Ruby настолько мощный и гибкий, что позволяет реализовывать подобные трюки в коде, но недостатки ли это самого языка или всё же тех писателей, которые прибегают к использованию данных механизмов в своём коде?
Про monkey patching вообще уже можно забыть, если вы освоите и будете использовать механизм Refinements. Приведу очень простой пример:
class Test module TestString refine String do def blank? = (self.length == 0 || self =~ /\A\p{Space}+\z/u) ? true : false end end using TestString def self.str_blank?(str) = str.blank? end str = "abc" strb = " " p str.blank? # => undefined method 'blank?' for an instance of String (NoMethodError) p Test::str_blank?(str) # => false p Test::str_blank?(strb) # => true
И всё! Вопрос закрыт - дополнительный метод для класса String
действует только внутри класса Test
, никаких побочных эффектов ни на другие классы и модули, ни, тем более, в глобальном пространстве имён нет.
Duck typing
class A def hello = p "class A hello" def hello_any(who) who.hello end # принимаем только экземпляры класса А или его потомков def hello_only_descendant(who) raise 'not a A' unless who.is_a? A # только А или потомки, все остальные - чужаки who.hello end end class B < A def hello = p "class B hello: descendant" end class A_other def hello = p "class A_other hello" end a = A::new b = B::new other = A_other::new a.hello # => "class A hello" b.hello # => "class B hello: descendant" other.hello # => "class A_other hello" a.hello_any(a) # => "class A hello" a.hello_any(b) # => "class B hello: descendant" a.hello_any(other) # => "class A_other hello" a.hello_only_descendant(a) # => "class A hello" a.hello_only_descendant(b) # => "class B hello: descendant" a.hello_only_descendant(other) # => in 'A#hello_only_descendant': not a A (RuntimeError)
Всё очень даже просто реализуемо - скажем так, отпинываем всех чужаков на входе, и никакая утка тут уже не проскочит. Тут уж, кто какую цель преследует: кто хочет - ищет возможности, кто не хочет - причины. У противников Ruby остаётся всё меньше и меньше причин назвать Ruby плохим или проблемным. Я думаю, пора уже перестать вопить насчёт того, что язык программирования Ruby какой-то не такой. Всё зависит прежде всего от того, какой вы программист. Ruby предоставляет очень мощные возможности для реализации различных алгоритмов, подходов, концепций... как хотите, так и назовите. Ruby - язык программирования с красивым, элегантным, лаконичным и понятным синтаксисом, имеющий в своём арсенале все мощные средства современного языка программирования.
Как по мне, так динамическая типизация очень мощная возможность языка, при условии, что вы и сам хороший программист. В Ruby реализован контроль типов, и он не допускает, например, подобной ереси:
a = 1 b = '123' c = a + b # => in 'Integer#+': String can't be coerced into Integer (TypeError)
Но, в то же время, и не запрещает безоговорочно - есть возможность приведения к ожидаемому (совместимому) типу (принудительного изменения типа) (coerce
). Для продолживших быть несогласными могу вас адресовать, например, к языку программирования Java - там механизм приведения к совместимому типу используется очень широко. В данном случае необходимо реализовать обработку аргумента типа String
для метода +
экземпляра класса Integer
.
Неплохая статья по теме Let’s talk about Type Coercion in Ruby, хотя обсуждений этого механизма Ruby в Интернет достаточно много.
Самый простой вариант приведения типов - использовать явное преобразование типа:
c = a + b.to_i
Возможно и более гибкое поведение, например, проводить какие-то свои преобразования типов при определённых операциях. В нижеследующем примере, наоборот (для наглядности), реализуем приведение к типу String
других типов (Integer
и Float
) для метода +
класса String
.
class Test module TestString refine String do def +(other) case other when String super when Integer self.class.new("#{self}+i#{other.to_s}") when Float self.class.new("#{self}+f#{other.to_s}") else raise other.class.name end end end end using TestString def self.add(num) 'add: ' + num end end p Test::add('123') # => "add: 123" p Test::add(123) # => "add: +i123" p Test::add(123.45) # => "add: +f123.45"
Весь вопрос преимущественно состоит в том - умеете ли вы писать программы, понимаете ли вы программирование или для вас не редкость возмущаться на вами же написанную программу: "делай то, что я хочу, а не то, что я написал!" - и особенности, возможности и мощь языка Ruby тут совершенно ни при чём, как говорится, плохому танцору всегда... что-то мешает. ;-)