Эта статья, ориентированная на новичков, охватывает еще один круг запахов и рефакторингов, с которыми вы должны ознакомиться в начале своей карьеры. Мы рассмотрим операторы 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 применимы и к нулевым объектам. В конце концов. это просто особый случай заявлений. То же самое касается недостатков:
- Понимание реализации и поведения может стать непростым делом, потому что код разбросан по всему, и нулевые объекты не имеют явного представления об их существовании.
- Добавление нового поведения может нуждаться в синхронизации между нулевыми объектами и их аналогами.
Класс данных
Давайте закроем эту статью чем-то легким. Это запах, потому что это класс, который не имеет никакого поведения, кроме получения и установки своих данных — это в основном не более чем контейнер данных без дополнительных методов, которые что-то делают с этими данными. Это делает их пассивными по своей природе, потому что эти данные хранятся там для использования другими классами. И мы уже знаем, что это не идеальный сценарий, потому что он кричит о зависти . Вообще говоря, мы хотим иметь классы, которые имеют состояние, то есть данные, о которых они заботятся, а также поведение с помощью методов, которые могут воздействовать на эти данные без особых помех или отслеживания других классов.
Вы можете начать рефакторинг таких классов, возможно, извлекая поведение из других классов, которые воздействуют на данные, в ваш класс данных. Делая это, вы можете постепенно привлекать полезное поведение для этих данных и придать им надлежащий вид. Вполне нормально, если эти классы растут со временем Иногда вы можете легко переместить весь метод, а иногда вам нужно сначала извлечь части более крупного метода, а затем извлечь его в класс данных. В случае сомнений, если вы можете уменьшить доступ, который другие классы имеют к вашему классу данных, вам определенно следует пойти на это и перенести это поведение на другое. Ваша цель должна состоять в том, чтобы в какой-то момент отключить методы получения и установки, которые дали другим объектам доступ к этим данным. В результате у вас будет меньше связей между классами, что всегда является победой!
Заключительные мысли
Видите, все было не так сложно! Есть много причудливого жаргона и сложных техник звучания вокруг запахов кода, но, надеюсь, вы поняли, что запахи и их рефакторинг также имеют несколько черт, число которых ограничено. Запахи кода никогда не исчезнут и не станут неактуальными — по крайней мере, до тех пор, пока наши тела не будут улучшены ИИ, которые позволят нам написать качественный код, от которого мы находимся за несколько световых лет.
В течение последних трех статей я представил вам большую часть наиболее важных и наиболее распространенных сценариев запаха кода, с которыми вы столкнетесь в своей карьере объектно-ориентированного программирования. Я думаю, что это было хорошее введение для новичков в этой теме, и я надеюсь, что примеры кода были достаточно многословны, чтобы легко следовать теме.
Разобравшись с этими принципами проектирования качественного кода, вы сразу же обнаружите новые запахи и их рефакторинг. Если вы сделали это до сих пор и чувствуете, что не потеряли общую картину, я думаю, что вы готовы приблизиться к статусу ООП уровня босса.