Как стать автором
Обновить

Как не нужно писать на руби

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров5.8K

В последнее время мне пришлось прочитать довольно много кода претендентов на позицию Senior Ruby Developer, и теперь, вытащив из глаз затычки, предотвращающие вытекание, я решил поделиться теми типовыми ошибками, которые соискатель делать не имеет права в принципе. Все они напрямую скопированы из разных решений тестового задания. Задание простое, требует немного денежной арифметики, немного простой логики, немного умения спроектировать и организовать малюсенький проектик.

Итак, встречайте: пять самых частых ляпов, зажигающих перед соискателем красный свет.

Оговорюсь, что я никогда не зверствую в кодревью и требую исправить только ошибки. Советую подправить — совсем уж корявый код, и только после апрува. Захочет коллега — исправит перед мерджем, нет — нет.

№5 Class variables

Пример:

class Foo
  @@env = :prod
  def env; @@env end
end

В последнее время этот тип ошибки уходит в прошлое, вместе с использованием attr_accessor и прочих глобальных прелестей. Тем не менее, иногда хочется определить константу на уровне класса, и, вроде бы, переменная класса для того и была придумана. Чтобы увидеть, в чем тут проблема, надо четко различать классы и инстансы, что немного усложнено тем, что класс — сам по себе тоже инстанс (своего айгенкласса, который синглтон, хм).

Так вот, код выше определяет переменную класса на уровне айгенкласса Foo. Всего у нас есть четыре способа объявить переменную с префиксом @: переменные класса или инстанса в разных местах:

  • на уровне класса (в теле определения модуля или класса)

  • на уровне инстанса (внутри метода)

Довольно бойлерплатный кусок кода ниже показывает различия:

class Foo
  @@env_class = :foo_env_class
  @env_class_instance = :foo_env_class_instance

  def initialize
    @@env_instance_class = :foo_env_instance_class
    @env_instance_instance = :foo_env_instance_instance
  end

  def get
    {
      env_class: @@env_class,
      env_class_instance: @env_class_instance,
      env_instance_class: @@env_instance_class,
      env_instance_instance: @env_instance_instance
    }
  end
end

class FooChild < Foo
  @@env_class = :foo_child_env_class
  @env_class_instance = :foo_child_env_class_instance

  def initialize
    @@env_instance_class = :foo_child_env_instance_class
    @env_instance_instance = :foo_child_env_instance_instance
  end
end

Если теперь сходить в консоль и глянуть, что там получилось, то результат будет вот таким:

[1] ▸ Foo.new.get
#=> {
#    :env_class=>:foo_child_env_class,
#    :env_class_instance=>nil,
#    :env_instance_class=>:foo_env_instance_class,
#    :env_instance_instance=>:foo_env_instance_instance}
[2] ▸ FooChild.new.get
#=> {
#    :env_class=>:foo_child_env_class,
#    :env_class_instance=>nil,
#    :env_instance_class=>:foo_child_env_instance_class,
#    :env_instance_instance=>:foo_child_env_instance_instance}

Из примечательного тут вот что: определенные на уровне модуля переменные с одной собакой (module instance variables) — из кода методов недоступны, их увидят методы класса, определенные внутри self << class или через def self.get. Но главное, переменная класса оказалась в родителе перетерта из наследника. Это ожидаемое поведение, переменные класса так работают. Поэтому не нужно их использовать примерно никогда. Для этих целей как раз и существует переменная инстанса класса, которая пуста в коде выше; ей просто нужен правильный геттер.

№4 Класс вместо Struct (часто вместо простого Hash)

Я знаю, что руби — это настоящий ООП, там даже каждое булево значение — инстанс настоящего класса (это не совсем так, на самом деле, но разговор об этом выходит за рамки данной заметки). И тем не менее, не нужно городить классы там, где вам нужны просто структурированные данные. Ходят слухи, что из введения краткой записи рекордов вместо многословного традиционного объявления классов с геттерами и сеттерами — однажды вырос целый язык. В руби вы не сможете защитить ваши данные, если злой коллега захочет их перезаписать, не нужно себя тешить модификаторами private и прочими attr_reader.

Поэтому если нужна структура данных — объявите ее как структуру данных. А еще лучше — вообще просто как Hash. Даже, если вам завтра потребуется какая-то аггрегация, — тут все еще руби.

[1] ▸ employee = {name: 'John', lastname: 'Smith'}.extend(
  Module.new { def full; [self[:name], self[:lastname]].join(?,) end}
)
[2] ▸ employee.full
#=> "John,Smith"

Ладно, шучу, так делать не нужно :) Но и злоупотреблять классами там, где достаточно простого хэша, или структуры — тоже.

№3 Hash.default_proc

Это любимая ахиллесова пята всех без исключения рельсовиков (стр. 7), ||:

class User
  def initialize(**input)
    @raw = input
  end
  
  def name
    @raw[:name] || "N/A" 
  end
end

Не делайте так. Определите Hash#default_proc на внутреннем хэше и спокойно отдавайте его наружу (замороженным, если боитесь измен).

№2 Вычисления с плавающей точкой

Текст уже получается длинноват, поэтому по этому пункту выскажусь кратко: никогда, ни при каких обстоятельствах, не используйте флоаты там, где нельзя вернуть «примерно то же самое, что было передано». Деньги, интервалы в днях, да вообще почти все. Используйте вместо них Rational. Или любую реализацию даблов, если прям принципиальна скорость.

№1 Итерация вместо маппинга или редьюса

И, наконец, венчает наш хит-парад — самый противный код, который вообще только можно представить.

sum = 0
[1, 2, 3, 4].each do |i|
  sum += i
end
sum

Здесь ужасно просто всё. Самое удивительное, что я встречаю подобное насилие над простым проходом по массиву чуть ли не в каждом втором тестовом.

  • объявление переменной вне скоупа вычислений

  • изменение переменной внешнего скоупа изнутри итератора

  • совершенно убийственная многословность ради ничего

  • введение в заблуждение читателя: тут нет итерации как таковой, тут reduce

Вот как этот код должен выглядеть:

[1, 2, 3, 4].reduce(0, &:+)
# или, если вы совсем новичок, то
[1, 2, 3, 4].reduce(0) do |acc, i|
  acc + i
end

На сегодня, пожалуй, все. А какие ваши любимые ошибки начинающего разработчика в руби?

Теги:
Хабы:
Всего голосов 8: ↑3 и ↓5-1
Комментарии119

Публикации

Истории

Работа

Ruby on Rails
5 вакансий
Программист Ruby
6 вакансий

Ближайшие события