Статьи

Основы запаха кода Ruby / Rails 03

файл

Эта статья, ориентированная на новичков, охватывает еще один круг запахов и рефакторингов, с которыми вы должны ознакомиться в начале своей карьеры. Мы рассмотрим операторы case, полиморфизм, нулевые объекты и классы данных.

  • Заявления по делу
  • Полиморфизм
  • Нулевые объекты
  • Класс данных

Этот можно также назвать «запах запаха» или что-то в этом роде. Заявления о делах — это запах, потому что они вызывают дублирование — они часто неэлегатны. Они также могут привести к ненужно большим классам, потому что все эти методы, которые отвечают на различные (и потенциально растущие) сценарии случая, часто оказываются в одном классе — который затем имеет все виды смешанных обязанностей. Это не редкий случай, когда у вас есть много частных методов, которые были бы лучше в собственных классах.

Большая проблема с инструкциями case возникает, если вы хотите расширить их. Затем вы должны изменить этот конкретный метод — возможно, снова и снова. И не только там, потому что часто у них есть двойники, повсюду повсюду, которые теперь нуждаются в обновлении. Отличный способ разведения жуков наверняка. Как вы помните, мы хотим быть открытыми для расширения, но закрытыми для модификации. Здесь модификация неизбежна и просто вопрос времени.

Вы затрудняете себе извлечение и повторное использование кода, плюс это бомба беспорядочного беспорядка. Часто вид кода зависит от таких случаев, которые затем дублируют запах и открывают ворота, широко открытые для раунда дробовика в будущем. Ой! Кроме того, опрос объекта до того, как вы найдете правильный метод для выполнения, является неприятным нарушением принципа « говорите, не спрашивайте ».

Есть хороший метод для обработки необходимости в примерах. Причудливое слово входящее! Полиморфизм Это позволяет создавать один и тот же интерфейс для разных объектов и использовать любой объект, необходимый для разных сценариев. Вы можете просто поменять соответствующий объект, и он адаптируется к вашим потребностям, потому что он имеет те же методы на нем. Их поведение под этими методами различно, но пока объекты реагируют на один и тот же интерфейс, Ruby это не волнует. Например, VegetarianDish.new.order и VeganDish.new.order ведут себя по-разному, но оба реагируют на #order одинаково. Вы просто хотите заказать и не отвечаете на множество вопросов, например, едите ли вы яйца или нет.

Полиморфизм реализуется путем извлечения класса для ветви оператора case и перемещения этой логики в новый метод этого класса. Вы продолжаете делать это для каждой ноги в условном дереве и присваиваете им одинаковое имя метода. Таким образом вы инкапсулируете это поведение в объект, который лучше всего подходит для принятия такого рода решений и у которого нет причин для дальнейших изменений. Видите, таким образом вы можете избежать всех этих мучительных вопросов об объекте — вы просто скажете ему, что он должен делать. Когда возникает необходимость в более условных случаях, вы просто создаете другой класс, который отвечает за эту единственную ответственность под тем же именем метода.

Логика выписки

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Operation
  def price
    case @mission_tpe
      when :counter_intelligence
        @standard_fee + @counter_intelligence_fee
      when :terrorism
        @standard_fee + @terrorism_fee
      when :revenge
        @standard_fee + @revenge_fee
      when :extortion
        @standard_fee + @extortion_fee
    end
  end
end
 
counter_intel_op = Operation.new(mission_type: :counter_intelligence)
counter_intel_op.price
 
terror_op = Operation.new(mission_type: :terrorism)
terror_op.price
 
revenge_op = Operation.new(mission_type: :revenge)
revenge_op.price
 
extortion_op = Operation.new(mission_type: :extortion)
extortion_op.price

В нашем примере у нас есть класс Operation который должен расспросить о его mission_type прежде чем он сможет сообщить вам свою цену. Легко видеть, что этот метод price просто ждет изменения, когда добавляется новый тип операции. Если вы также хотите отобразить это в своем представлении, вам также необходимо применить это изменение. (К вашему сведению, для представлений вы можете использовать полиморфные партиалы в Rails, чтобы избежать взрыва этих операторов case во всем вашем представлении.)

Полиморфные классы

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class CounterIntelligenceOperation
  def price
    @standard_fee + @counter_intelligence_fee
  end
end
 
class TerrorismOperation
  def price
    @standard_fee + @terrorism_fee
  end
end
 
class RevengeOperation
  def price
    @standard_fee + @revenge_fee
  end
end
 
class ExtortionOperation
  def price
    @standard_fee + @extortion_fee
  end
end
 
counter_intel_op = CounterIntelligenceOperation.new
counter_intel_op.price
 
terror_op = CounterIntelligenceOperation.new
terror_op.price
 
revenge_op = CounterIntelligenceOperation.new
revenge_op.price
 
extortion_op = CounterIntelligenceOperation.new
extortion_op.price

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

Я думаю, что логика, стоящая за заявлениями по делу, неизбежна. Сценарии, в которых вам нужно пройти какой-то контрольный список, прежде чем вы найдете объект или поведение, которое выполняет эту работу, слишком распространены. Вопрос просто в том, как с ними обращаться лучше всего. Я за то, чтобы не повторять их, и использование инструментов объектно-ориентированного программирования предлагает мне для разработки дискретных классов, которые могут быть легко заменены через их интерфейс.

Проверка на ноль повсюду — особый вид запаха. Запрос объекта о nil часто является своего рода скрытым оператором case. Обработка nil условно может принимать форму object.nil? , object.present? , object.try , а затем какое-то действие в случае, если nil появится на вашей вечеринке.

Другой, более хитрый вопрос — спросить у объекта о его правдивости, то есть, если он существует, или если он нулевой, и затем предпринять некоторые действия. Выглядит безобидно, но это просто маскировка. Не обманывайте себя: троичные операторы или || операторы тоже попадают в эту категорию, конечно. Иными словами, условные обозначения не только четко идентифицируются как операторы if-else или case . У них есть более тонкие способы разрушить вашу вечеринку. Не спрашивайте объекты об их нулях, но скажите им, если объект для вашего счастливого пути отсутствует, что нулевой объект теперь отвечает за ваши сообщения.

Нулевые объекты — это обычные классы. В них нет ничего особенного — просто «причудливое имя». Вы извлекаете некоторую условную логику, связанную с nil и затем решаете ее полиморфно. Вы управляете этим поведением, управляете потоком своего приложения через эти классы, а также имеете объекты, которые открыты для других подходящих им расширений. Подумайте, как класс Trial ( NullSubscription ) может расти со временем. Это не только более СУХОЙ и ЯДДА-ЯДДА-ЯДДА, но и более информативный и стабильный.

Большим преимуществом использования нулевых объектов является то, что вещи не могут взорваться так легко. Эти объекты отвечают тем же сообщениям, что и эмулируемые объекты — вам не всегда нужно копировать весь API-интерфейс в нулевые объекты — что дает вашему приложению мало причин для сумасшествия. Однако обратите внимание, что нулевые объекты инкапсулируют условную логику, не удаляя ее полностью. Вы просто найдете лучший дом для этого.

Поскольку выполнение большого количества действий, связанных с нулем, в вашем приложении довольно заразно и вредно для вашего приложения, мне нравится думать о нулевых объектах как о «шаблоне нулевого содержания» (пожалуйста, не судитесь со мной!). Почему заразно? Потому что, если вы передадите nil , где-то еще в вашей иерархии, другой метод рано или поздно также будет вынужден спросить, находится ли nil в городе, что затем приведет к другому раунду принятия контрмер для решения такого случая.

Иными словами, с ноль не круто тусоваться, потому что просить его присутствие становится заразным. Запрашивать объекты для nil , скорее всего, всегда является признаком плохого дизайна — без обид и не плохо себя чувствую! — мы все были там. Я не хочу идти волей-неволей о том, насколько недружелюбным может быть ноль, но нужно упомянуть несколько вещей:

  • Nil — участник вечеринки (извините, ноль, надо было сказать).
  • Ноль не помогает, потому что ему не хватает смысла.
  • Нил не реагирует ни на что и нарушает идею « Duck Typing ».
  • Нулевые сообщения об ошибках часто являются болью, чтобы иметь дело с.
  • Нил тебя укусит — рано или поздно.

В целом, сценарий, когда объект чего-то не хватает, возникает очень часто. Часто цитируемым примером является приложение, которое имеет зарегистрированного User и NilUser . Но поскольку пользователей, которых не существует, является глупой концепцией, если этот человек явно просматривает ваше приложение, вероятно, было бы круче иметь Guest который еще не зарегистрировался. Отсутствующая подписка может быть Trial , нулевой заряд — Freebie и так далее.

Называть ваши нулевые объекты иногда просто и понятно, иногда очень сложно. Но старайтесь не называть все нулевые объекты ведущими «Нуль» или «Нет». Ты можешь лучше! Я думаю, что предоставление небольшого количества контекста имеет большое значение. Выберите имя, которое является более конкретным и значимым, то, что отражает реальный вариант использования. Таким образом, вы будете более четко общаться с другими членами команды и, конечно же, со своей будущей личностью.

Есть несколько способов реализовать эту технику. Я покажу вам один, который я считаю простым и удобным для начинающих. Я надеюсь, что это хорошая основа для понимания более сложных подходов. Когда вы постоянно сталкиваетесь с каким-то условием, имеющим дело с nil , вы знаете, что пришло время просто создать новый класс и переместить это поведение. После этого вы дадите исходному классу понять, что этот новый игрок теперь занимается ничем.

В приведенном ниже примере вы можете видеть, что класс Spectre слишком много спрашивает о nil и излишне загромождает код. Он хочет убедиться, что у нас есть evil_operation прежде чем он решит зарядить. Можете ли вы увидеть нарушение «Скажи-не-спроси»?

Другая проблемная часть заключается в том, почему Spectre нужно заботиться о реализации нулевой цены. Метод try также evil_operation спрашивает, есть ли у evil_operation price для обработки evil_operation с помощью оператора or ( || ). evil_operation.present? действительно делает ту же ошибку. Мы можем упростить это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Spectre
  include ActiveModel::Model
  attr_accessor :credit_card, :evil_operation
 
  def charge
    unless evil_operation.nil?
      evil_operation.charge(credit_card)
    end
  end
 
  def has_discount?
    evil_operation.present?
  end
 
  def price
    evil_operation.try(:price) ||
  end
end
 
class EvilOperation
  include ActiveModel::Model
  attr_accessor :discount, :price
 
  def has_discount?
    discount
  end
 
  def charge(credit_card)
    credit_card.charge(price)
  end
end
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class NoOperation
  def charge(creditcard)
    «No evil operation registered»
  end
 
  def has_discount?
    false
  end
 
  def price
    0
  end
end
 
class Spectre
  include ActiveModel::Model
  attr_accessor :credit_card, :evil_operation
 
  def charge
    evil_operation.charge(credit_card)
  end
 
  def has_discount?
    evil_operation.has_discount?
  end
 
  def price
    evil_operation.price
  end
 
  private
 
  def evil_operation
    @evil_operation ||
  end
end
 
class EvilOperation
  include ActiveModel::Model
  attr_accessor :discount, :price
 
  def has_discount?
    discount
  end
 
  def charge(credit_card)
    credit_card.charge(price)
  end
end

Я думаю, этот пример достаточно прост, чтобы сразу увидеть, насколько элегантными могут быть нулевые объекты. В нашем случае у нас есть нулевой класс NoOperation который знает, как обращаться с несуществующей злой операцией с помощью:

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

Мы создали этот новый класс, который имеет дело с небытием, объект, который обрабатывает отсутствие вещей, и поместим его в старый класс, если появится nil . API — это ключ, потому что если он совпадает с исходным классом, вы можете легко заменить нулевой объект на возвращаемые значения, которые нам нужны. Именно это мы и сделали в приватном методе Spectre#evil_operation . Если у нас есть объект evil_operation , нам нужно это использовать; в противном случае мы используем нашего nil хамелеона, который знает, как обращаться с этими сообщениями, поэтому evil_operation никогда больше не вернет nil. Утка печатать в лучшем виде.

Наша проблемная условная логика обернута в одном месте, и наш нулевой объект отвечает за поведение, которое ищет Spectre . DRY! Мы как бы восстановили тот же интерфейс из исходного объекта, который не мог обработать nil. Помните, ноль плохо работает при получении сообщений — никого нет дома, всегда! Отныне он просто говорит объектам, что делать, не спрашивая их сначала о «разрешении». EvilOperation то, что вообще не нужно было трогать класс EvilOperation .

И последнее, но не менее важное: я избавился от проверки наличия злой операции в Spectre#has_discount? , Не нужно убедиться, что операция существует, чтобы получить скидку. В результате нулевого объекта класс Spectre намного тоньше и не разделяет обязанности других классов.

Хорошая рекомендация — не проверять объекты, если они склонны что-то делать. Командуйте ими, как сержант. Как это часто бывает, в реальной жизни это, вероятно, не круто, но это хороший совет для объектно-ориентированного программирования. Теперь дай мне 20!

В общем, все преимущества использования полиморфизма вместо операторов case применимы и к нулевым объектам. В конце концов. это просто особый случай заявлений. То же самое касается недостатков:

  • Понимание реализации и поведения может стать непростым делом, потому что код разбросан по всему, и нулевые объекты не имеют явного представления об их существовании.
  • Добавление нового поведения может нуждаться в синхронизации между нулевыми объектами и их аналогами.

Давайте закроем эту статью чем-то легким. Это запах, потому что это класс, который не имеет никакого поведения, кроме получения и установки своих данных — это в основном не более чем контейнер данных без дополнительных методов, которые что-то делают с этими данными. Это делает их пассивными по своей природе, потому что эти данные хранятся там для использования другими классами. И мы уже знаем, что это не идеальный сценарий, потому что он кричит о зависти . Вообще говоря, мы хотим иметь классы, которые имеют состояние, то есть данные, о которых они заботятся, а также поведение с помощью методов, которые могут воздействовать на эти данные без особых помех или отслеживания других классов.

Вы можете начать рефакторинг таких классов, возможно, извлекая поведение из других классов, которые воздействуют на данные, в ваш класс данных. Делая это, вы можете постепенно привлекать полезное поведение для этих данных и придать им надлежащий вид. Вполне нормально, если эти классы растут со временем Иногда вы можете легко переместить весь метод, а иногда вам нужно сначала извлечь части более крупного метода, а затем извлечь его в класс данных. В случае сомнений, если вы можете уменьшить доступ, который другие классы имеют к вашему классу данных, вам определенно следует пойти на это и перенести это поведение на другое. Ваша цель должна состоять в том, чтобы в какой-то момент отключить методы получения и установки, которые дали другим объектам доступ к этим данным. В результате у вас будет меньше связей между классами, что всегда является победой!

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

В течение последних трех статей я представил вам большую часть наиболее важных и наиболее распространенных сценариев запаха кода, с которыми вы столкнетесь в своей карьере объектно-ориентированного программирования. Я думаю, что это было хорошее введение для новичков в этой теме, и я надеюсь, что примеры кода были достаточно многословны, чтобы легко следовать теме.

Разобравшись с этими принципами проектирования качественного кода, вы сразу же обнаружите новые запахи и их рефакторинг. Если вы сделали это до сих пор и чувствуете, что не потеряли общую картину, я думаю, что вы готовы приблизиться к статусу ООП уровня босса.