Как внештатный разработчик, я часто работаю с разработчиками из разных областей программирования и с разным уровнем квалификации. Со временем я научился рассказывать программисту об уровне Ruby, глядя на код, который они написали. Мало того, я могу также сделать безопасную ставку на фон кодирования программиста, основанный на определенных шаблонах в коде, которые дают тонкие подсказки относительно предыдущего опыта программирования кодера. Есть определенные методы кодирования, которые обычно используют Ruby Rookies. Давайте назовем эти анти-паттерны. Этот пост перечисляет наиболее распространенные из этих анти-паттернов.
Объединять, а не интерполировать
Ruby — один из немногих языков, которые позволяют вам выполнять интерполяцию строк непосредственно в строку без использования каких-либо специальных методов. К сожалению, многие новички в Ruby предпочитают объединять строки:
puts “error description” + e.cause.to_s + “happened on: ” + Time.now.to_s
Когда вместо этого они могли написать:
puts “error description #{e.cause} happened on: #{Time.now}”
Мало того, что это проще для глаз, но и метод #to_s
автоматически вызывается для интерполированных объектов. Неспособность явно вызвать #to_s
при конкатенации приведет к #to_s
. Таким образом, интерполяция приводит к более короткому, красивому и безопасному коду.
Кроме того, если вы хотите похвастаться перед своими собравшимися друзьями, почему бы не использовать форматированную интерполяцию Ruby?
pets = %w{dog cat rabbit} puts "My first pet is a %s, my second one a %s and my third is a %s" % pets => My first pet is a dog, my second one a cat and my third is a rabbit
Правду говорят
Многие люди, приходящие в Ruby с других языков, не понимают, насколько проста в действительности модель истинности Ruby. Таким образом, они пишут код так:
if an_object != nil && an_object.method? == true #do something end
В Ruby только значения false
и nil
оцениваются как false (фактически они оба являются объектами). Все остальное оценивается как правда. Таким образом, наше условное становится намного проще:
if an_object && an_object.method? #do something end
Пока мы говорим о булевой логике, вот еще один анти-паттерн, с которым я часто сталкиваюсь.
if !an_object.active? #do something end
Нет ничего плохого в отрицательных утверждениях как таковых, кроме одной вещи: читабельность. Этот восклицательный знак слишком легко пропустить, давая читателю противоположное представление о том, что делает код. К счастью, Ruby предлагает немного хорошего синтаксического сахара в форме оператора
unless an_object.active? #do something end
Это хорошее, решительное утверждение, которое трудно пропустить.
AndNotOr
Многие Ruby Rookies настолько обрадованы английским эквивалентом логических операторов, что постоянно используют их, заменяя символические. Однако они не всегда понимают, что [!, &&, || ]
[!, &&, || ]
не совпадают с [not, and, or]
. Разница в приоритете.
puts "hello" && "world" => world
В этом случае оператор &&
вычисляется перед puts
, эффективно сокращая выражение до puts "world"
Если вместо этого мы используем оператор and
, выражение оценивается как (puts "hello") and ("world")
puts "hello" and "world" => hello
Заметьте, что символические операторы более «липкие», чем их аналоги на естественном языке. Это может привести к незначительным ошибкам:
arr = [1,2,3] !arr[0].is_a?(String) && arr.length > 3 => false not arr[0].is_a?(String) && arr.length > 3 => true
Как правило, используйте только операторы естественного языка для управления потоком выполнения, придерживаясь при этом символических операторов для логических операций.
Не бойся жнеца
Ruby не очень заботится о том, к какому классу относится объект, он заботится о том, что может сделать объект. Если я хочу заказать пиццу, я хочу, чтобы пиццерия предоставила мне способ заказать пиццу. Пиццерия может называть себя «Pizza Parlour», «The Pizza Emporium», или как там угодно, мне действительно все равно, какое у нее название. Я просто хочу, чтобы он поддерживал метод order_pizza
. Многие разработчики со статически типизированным фоном сталкиваются с трудностями, соглашаясь с этим понятием.
Среднестатистический программист со статической типизацией, который только начал программировать на Ruby, думает так. Вот пример:
def my_method(arg) #hold on, as there are no type declarations in Ruby, arg could be anything at all #better be defensive about this if arg.is_a? MyClass #that will ensure I'm dealing with the right type # oh, but in Ruby one can delete methods from objects at run-time # better protect against that too if arg.respond_to? :my_method arg.my_method # now I can safely call my method else # panic! end end #that's nice, solid code. Well done me. end
На самом деле это просто раздутый, избыточный код. То, что что-то может произойти, не означает, что это произойдет. Если вы ожидаете, что ваш объект будет вести себя определенным образом, скорее всего, так оно и будет. Если случается непредвиденное, большинство языков программирования, как правило, имеют дело с непредвиденным, обеспечивая обработку исключений, и Ruby делает именно это. Возникнет исключение, если объект не поддерживает метод.
Так что, если вы хотите быть осторожным, просто поймайте исключение:
def m(arg) begin arg.my_method rescue => e # handle exception here end end
Это достаточно оборонительно, не выходя за борт. Более того, более опытный разработчик Ruby знает, что само объявление метода может действовать как начальный блок, поэтому код можно сделать еще проще.
def m(arg) arg.my_method rescue => e # handle exception here end
Это безопасный код без раздувания. Рубин не блестящий?
Гнездо Ифс
Слишком много if
и elsif
делают наш код уродливым и трудным для чтения, особенно если они вложенные. Для if..else
выражения if..else
часто используются в качестве молотка для взлома любого вида ореха. Есть много способов избежать дублирования и беспорядка нескольких условных выражений. Очень распространенным решением является просто рефакторинг хотя бы одного из условных операторов в качестве отдельного метода. Тем не менее, один из моих любимых приемов — использование хэшей в качестве объектов бизнес-правил. Проще говоря, мы можем абстрагировать некоторую нашу условную логику в объекте Hash, тем самым делая наш код проще для глаз и пригодным для повторного использования.
Учти это:
if @document.save if current_user.role == "admin" redirect_to admin_path elsif current_user.role == "reviewer" redirect_to reviewer_path elsif current_user.role == "collaborator" redirect_to collaborator else redirect_to user_path end else render :new end
Мы можем использовать объект Logic Hash и троичный оператор, чтобы сделать этот код более кратким и понятным:
redirection_path = Hash.new{|hash,key| hash[key] = user_path} redirection_path[:admin] = admin_path redirection_path[:reviewer] = reviewer_path redirection_path[:collaborator] = collaborator_path @document.save ? redirect_to redirection_path[current_user.role.to_sym] : (render :new)
Мы сократили 5 возможных путей выполнения до 2. Но, подождите минуту … мы можем воспользоваться возможностями метапрограммирования Руби, чтобы поднять это на следующий уровень краткости.
redirection_path = Hash.new{|hash,key| hash[key] = ( %w(reviewer admin collaborator).include?(key.to_s) ? instance_eval("#{key}_path") : instance_eval('user_path') )} @document.save ? redirect_to redirection_path[current_user.role.to_sym] : (render :new)
Хорошо, я признаю, что мы немного дрейфуем с территории новичка прямо сейчас. Дело в том, что Ruby дает возможность выбирать краткие и недвусмысленные выражения над вложенными условными выражениями, так что просто продолжайте и используйте их.
Список непонимания
Функциональные аспекты Ruby, реализованные блоками, крайне недооцениваются Ruby Rookies, особенно когда речь идет о построении списков на основе других списков. Скажем, нам нужно извлечь четные числа из списка и умножить их на 3. Тогда нам нужно только сохранить числа ниже определенного порога. Многие новички будут использовать этот подход:
arr =[1,2,3,4,5,6,7,8,9,10] new_arr = [] arr.each do |x| if x % 2 == 0 new_arr << x * 3 if x * 3 < 20 end end
Что отлично работает, но не совсем по сравнению с выразительностью:
arr.select{|x| x % 2 == 0 }.map{|x| x * 3}.reject{|x| x > 19}
Используя блоки, мы эффективно передаем функции в функции. Многие методы Ruby используют это преимущество, чтобы получить некоторый краткий, мощный, но очень читаемый код. Ruby Rookie может написать некоторый код, который условно загружает ряд задач Rake, например:
load "project/tasks/annotations.rake" load "project/tasks/dev.rake" load "project/tasks/framework.rake" load "project/tasks/initializers.rake" load "project/tasks/log.rake" load "project/tasks/middleware.rake" load "project/tasks/misc.rake" load "project/tasks/restart.rake" load "project/tasks/routes.rake" load "project/tasks/tmp.rake" load "project/tasks/statistics.rake" if Rake.application.current_scope.empty?
Теперь посмотрим, как Rails добивается того же самого за один, элегантный шаг:
%w( annotations dev framework initializers log middleware misc restart routes tmp ).tap { |arr| arr << 'statistics' if Rake.application.current_scope.empty? }.each do |task| load "rails/tasks/#{task}.rake" end
Использование метода самореферентного крана и Enumerator (каждого) для создания чистого и удобного в обслуживании кода. Прекрасный!
Что дальше?
Эта статья посвящена кодированию анти-паттернов. В следующей статье я расскажу об анти-шаблонах проектирования , именно так Ruby Rookies структурируют свой код не слишком умным способом.