Статьи

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

файл

Это вторая часть небольшой серии статей о запахах кода и возможных рефакторингах. Целевая аудитория, о которой я говорил, это новички, которые слышали об этой теме и, возможно, хотели немного подождать, прежде чем войти в эти продвинутые воды. В следующей статье рассматриваются «Зависть к особенностям», «Хирургия дробовика» и «Разные изменения».

  • Зависть
  • Дробовик Хирургия
  • Дивергентное изменение

Что вы быстро поймете с запахами кода, так это то, что некоторые из них очень близкие родственники. Даже их рефакторинги иногда связаны — например, Inline Class и Extract Class не так уж отличаются.

Например, с помощью встраивания класса вы извлекаете весь класс, а избавляетесь от исходного. Так что любопытно извлечь класс с небольшим поворотом. Суть, которую я пытаюсь подчеркнуть, заключается в том, что вы не должны чувствовать себя перегруженными количеством запахов и рефакторингов и, конечно же, не должны разочаровываться их умными именами. Такие вещи, как «Дробовик», «Зависть к функциям» и «Разные изменения», могут показаться необычным и пугающим для людей, которые только начали. Может я ошибаюсь, конечно.

Если вы немного погрузитесь в эту тему и поиграете с парой рефакторингов кодовых запахов, вы быстро увидите, что они часто оказываются в одном и том же месте. Многие рефакторинги — это просто разные стратегии, чтобы добраться до точки, где у вас есть краткие, хорошо организованные классы, сфокусированные на небольшом количестве обязанностей. Я думаю, будет справедливо сказать, что если вы сможете достичь этого, большую часть времени вы будете впереди других — не так важно быть впереди других, но такой дизайн классов просто часто отсутствует в коде от людей до того, как они считаются «экспертами».

Так почему бы не начать игру раньше и создать конкретную основу для разработки своего кода. Не верьте тому, что может быть вашим собственным рассказом, что это сложная тема, которую вы должны отложить на некоторое время, пока не будете готовы. Даже если вы новичок, если вы делаете небольшие шаги, вы можете обернуть голову вокруг запахов и их рефакторинга намного раньше, чем вы думаете.

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

Давайте вернемся к примеру из предыдущей статьи. Мы извлекли длинный список параметров для #assign_new_mission в объект параметра через класс Mission . Пока все круто.

М с особой завистью

« `ruby class M def assign_new_mission (mission) print« Миссия # {mission.mission_name} была назначена # {mission.agent_name} с целью # {mission.objective}. », если mission.licence_to_kill печатает« Лицензия убить предоставлено. »иначе печать« Лицензия на убийство не была предоставлена ​​». конец конец конец

Класс миссии attr_reader: имя_миссии,: имя_агента,: цель,: licence_to_kill

инициализация по умолчанию (имя-миссии: имя-миссии: имя-агента, имя-агента: имя-агента, цель: цель, licence_to_kill: licence_to_kill) @mission_name = имя-миссии-имя @agent_name = имя-агента @objective = цель @licence_to_kill = licence_to_kill конец конец

m = M.new

mission = Mission.new (mission_name: «Octopussy», agent_name: «James Bond», цель: «найти ядерное устройство», licence_to_kill: true)

m.assign_new_mission (миссия)

=> «Миссия Octopussy была назначена Джеймсу Бонду с целью найти ядерное устройство. Лицензия на убийство была предоставлена ​​».

« `

Я кратко упомянул, как мы можем еще больше упростить класс M , переместив метод #assign_new_mission в новый класс для объекта параметра. Я не обращал внимания на тот факт, что у M тоже была легко излечимая форма зависти к особенностям . M был слишком любопытен на предмет атрибутов Mission . Иными словами, она задала слишком много «вопросов» об объекте миссии. Это не только плохой случай микроуправления, но и очень распространенный запах кода.

Позвольте мне показать вам, что я имею в виду. В M#assign_new_mission M «завидует» данным в новом объекте параметра и хочет получить к ним доступ повсюду.

  • mission.mission_name
  • mission.agent_name
  • mission.objective
  • mission.licence_to_kill

В дополнение к этому у вас также есть объект параметра Mission который сейчас отвечает только за данные — это еще один запах, класс данных .

В целом, вся эта ситуация говорит вам, что #assign_new_mission хочет быть где-то еще, и M не нужно знать детали того, как назначаются миссии. В конце концов, почему миссия не несет ответственности за назначение новых миссий? Не забудьте всегда соединять вещи, которые также изменяются вместе.

М без особой зависти

« `ruby class M def assign_new_mission (mission) mission.assign end end

Класс миссии attr_reader: имя_миссии,: имя_агента,: цель,: licence_to_kill

инициализация по умолчанию (имя_миссии: имя-миссии: имя-агента, имя-агента: имя-агента, цель: цель, licence_to_kill: licence_to_kill) @mission_name = имя-миссии @agent_name = имя-агента @objective = цель @licence_to_kill = licence_to_kill end

def assign print «Миссия # {имя_миссии} была назначена # {agent_name} с целью # {target}.», если licence_to_kill выдает «Лицензия на убийство предоставлена». иначе выведите «Лицензия на уничтожение не была предоставлено. »конец конец конец

m = M.new mission = Mission.new (название миссии: «Octopussy», имя агента: «Джеймс Бонд», цель: «найти ядерное устройство», licence_to_kill: true) m.assign_new_mission (mission) « `

Как видите, мы немного упростили вещи. Метод значительно уменьшился и передает поведение ответственному объекту. M больше не запрашивает данные о заданиях и, конечно же, не вмешивается в то, как распечатываются задания. Теперь она может сосредоточиться на своей реальной работе, и ее не нужно беспокоить, если какие-либо детали назначения миссии меняются. Больше времени для интеллектуальных игр и охоты на мошенников. Win-выиграть!

Зависть к функциям порождает запутанность — под этим я не подразумеваю хороший вид, тот, который позволяет информации распространяться быстрее, чем свет, — я говорю о той, которая со временем может позволить вашему импульсу развития обернуться все более приближающейся остановкой. Не хорошо! Почему так? Волновые эффекты в вашем коде создадут сопротивление! Изменение в одном месте бабочек через все виды вещей, и вы в конечном итоге, как воздушный змей в урагане. (Хорошо, немного чрезмерно драматично, но я даю себе B + для ссылки на облигации там.)

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

Имя немного глупо, не так ли? Но в то же время это довольно точное описание. Звучит как серьезный бизнес, и это так! К счастью, это не так сложно понять, но, тем не менее, это один из самых неприятных запахов кода. Почему? Потому что это порождает дублирование, как никто другой, и легко упустить из виду все изменения, которые вам нужно внести, чтобы исправить положение. Что происходит во время операции с дробовиком, вы вносите изменения в один класс / файл, и вам также нужно коснуться многих других классов / файлов, которые необходимо обновить. Надеюсь, это не похоже на хорошее время для тебя.

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

Если у вас есть спектр с DRY-кодом на одной стороне, то код, который часто требует операции дробовика, в значительной степени на противоположной стороне. Не ленись и позволь себе войти на эту территорию. Я уверен, что вы бы лучше открыли один файл и применили свои изменения там, и с этим покончено. Это тот вид ленивого, к которому нужно стремиться!

Чтобы избежать этого запаха, вот краткий список симптомов, на которые вы можете обратить внимание:

  • Зависть
  • Тесная связь
  • Длинный список параметров
  • Любая форма дублирования кода

Что мы имеем в виду, когда говорим о связанном коде? Допустим, у нас есть объекты A и B Если они не связаны, то вы можете изменить один из них, не влияя на другой. В противном случае вам чаще всего придется иметь дело и с другим объектом.

Это проблема, и операция с дробовиком также является признаком жесткой связи. Поэтому всегда следите за тем, как легко вы можете изменить свой код. Если это относительно просто, это означает, что ваш уровень связи приемлемо низок. Сказав это, я понимаю, что ваши ожидания были бы нереальными, если вы ожидаете, что сможете избежать связывания все время любой ценой. Это не произойдет! Вы найдете веские причины, чтобы принять решение против этого побуждения — например, заменить условные выражения полиморфизмом . В таком случае стоит немного поработать над связью, операцией дробовика и синхронизацией API объектов, чтобы избавиться от тонны инструкций case через Null Object (подробнее об этом позже).

Чаще всего вы можете применить один из следующих рефакторингов, чтобы залечить раны:

  • Переместить поле
  • Встроенный класс
  • Извлечь класс
  • Метод перемещения

Давайте посмотрим на некоторый код. Этот пример представляет собой фрагмент того, как приложение Spectre обрабатывает платежи между их подрядчиками и злыми клиентами. Я немного упростил платежи, установив стандартные сборы для подрядчиков и клиентов. Поэтому не имеет значения, поручено ли Призраку похитить кошку или вымогать целую страну: плата остается неизменной. То же самое касается того, что они платят своим подрядчикам. В редком случае операция идет на юг и еще один Nr. 2 буквально должен прыгнуть с акулы, Spectre предлагает полный возврат денег, чтобы злые клиенты были счастливы. Spectre использует какой-то проприетарный платежный драгоценный камень, который в основном является заполнителем для любого вида платежного процессора.

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

Пример с запахом операции дробовика :

« `Рубиновый класс EvilClient #…

STANDARD_CHARGE = 10000000 BONUS_CHARGE = 10000000

окончание def accept_new_client PaymentGem.create_client (электронная почта)

def charge_for_initializing_operation evil_client_id = PaymentGem.find_client (электронная почта) .payments_id PaymentGem.charge (evil_client_id, STANDARD_CHARGE) end

def charge_for_successful_operation evil_client_id = PaymentGem.find_client (электронная почта) .payments_id PaymentGem.charge (evil_client_id, BONUS_CHARGE) конец конец

Операция класса #…

REFUND_AMOUNT = 10000000

def refund Transactions_id = PaymentGem.find_transaction (payment_id) PaymentGem.refund (Transactions_id, REFUND_AMOUNT) конец конец

Подрядчик класса №…

STANDARD_PAYOUT = 200000 BONUS_PAYOUT = 1000000

def process_payout spectre_agent_id = PaymentGem.find_contractor (электронная почта) .payments_id if operation.enemy_agent == ‘Джеймс Бонд’ && operation.enemy_agent_status == ‘Убитый в действии’ PaymentGem.transfer_funds (spectre_agent_id, BAGUS_Play_Tence_Pence_fer_Tence_Pence_fer_Tence_Pence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_fer_Tence_Pence_PayToTence_PEG). конец конец конец « `

Когда вы смотрите на этот код, вы должны спросить себя: «Должен ли класс EvilClients действительно беспокоиться о том, как обработчик платежей принимает новых злых клиентов и как они взимают плату за операции?» Конечно, нет! Это хорошая идея распространять различные суммы, чтобы заплатить повсюду? Должны ли детали реализации обработчика платежей отображаться в любом из этих классов? Определенно нет!

Посмотрите на это так. Если вы хотите что-то изменить в способе обработки платежей, зачем вам нужно открывать класс EvilClient ? В других случаях это может быть пользователь или клиент. Если вы думаете об этом, не имеет смысла знакомить их с этим процессом.

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

Пример без запаха дробовика и извлеченного класса :

« `ruby class PaymentHandler STANDARD_CHARGE = 10000000 BONUS_CHARGE = 10000000 REFUND_AMOUNT = 10000000 STANDARD_CONTRACTOR_PAYOUT = 200000 BONUS_CONTRACTOR_PAYOUT = 1000000

def initialize (payment_handler = PaymentGem) @payment_handler = payment_handler end

def accept_new_client (evil_client) @ payment_handler.create_client (evil_client.email) конец

def charge_for_initializing_operation (evil_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (evil_client_id, STANDARD_CHARGE) конец

def charge_for_successful_operation (evil_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (evil_client_id, BONUS_CHARGE) end

def refund (operation) транзакция_id = @ payment_handler.find_transaction (operation.payments_id) @ payment_handler.refund (транзакция_id, REFUND_AMOUNT) конец

def contractor_payout (подрядчик) spectre_agent_id = @ payment_handler.find_contractor (contractor.email) .payments_id if operation.enemy_agent == ‘Джеймс Бонд’ && operation.enemy_agent_status == ‘Убито в действии’ @ payment_handler.transfer_fagund_ON_ON_TOR_ONT_OUT_TOR_ONT_OUT_TOR_TRONID_TRONID_TRONT) payment_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) конец конец конец

класс EvilClient #…

def accept_new_client PaymentHandler.new.accept_new_client (self) end

def charge_for_initializing_operation PaymentHandler.new.charge_for_initializing_operation (self) end

def charge_for_successful_operation (операция) PaymentHandler.new.charge_for_successful_operation (self) end end

Операция класса #…

def refund PaymentHandler.new.refund (self) end end

Подрядчик класса №…

def process_payout PaymentHandler.new.contractor_payout (self) end end « `

То, что мы сделали здесь, это PaymentGem API-интерфейс PaymentGem в наш собственный класс. Теперь у нас есть одно центральное место, где мы применяем наши изменения, если мы решим, например, что SpectrePaymentGem будет работать лучше для нас. Больше не нужно прикасаться к нескольким — к внутренним платежам, не связанным — файлам, если нам нужно адаптироваться к изменениям. В классах, которые имеют дело с платежами, мы просто создаем экземпляр PaymentHandler и делегируем необходимую функциональность. Легко, стабильно, и нет причин менять.

И мы не только содержали все в одном файле. В классе PaymentsHandler есть только одно место, которое нам нужно поменять и сослаться на возможный новый обработчик платежей — при initialize . Это рад в моей книге. Конечно, если новый платежный сервис имеет совершенно другой API, вам нужно настроить тела нескольких методов в PaymentHandler . Это крошечная цена по сравнению с полной операцией на дробовике — это больше похоже на операцию с небольшим осколком в вашем пальце. Хорошая сделка!

Если вы не будете осторожны при написании тестов для такого платежного процессора или любой другой внешней службы, на которую вам следует положиться, вы, возможно, столкнетесь с серьезными головными болями, когда они изменят свой API. Конечно, они «страдают от перемен». И вопрос не в том, будут ли они менять свой API, только когда.

Благодаря нашей инкапсуляции мы в гораздо лучшем положении, чтобы заглушить наши методы для обработчика платежей. Почему? Потому что методы, которые мы используем, являются нашими, и они меняются только тогда, когда мы этого хотим. Это большая победа. Если вы новичок в тестировании, и это не совсем понятно для вас, не беспокойтесь об этом. Не торопитесь; эта тема может быть сложно на первый взгляд. Поскольку это такое преимущество, я просто хотел упомянуть об этом для полноты картины.

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

Если вы не совсем довольны этим занятием и видите возможности для рефакторинга, я приветствую вас и с радостью возьму на себя эту заслугу. Я рекомендую вам вырубить себя! Хорошим началом может быть то, как вы находите payments_id s. Сам класс тоже уже немного переполнен …

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

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

Не поймите меня неправильно: часто переодевание — это не запах. Это полезный симптом, хотя. Еще один очень распространенный и более явный признак состоит в том, что этот объект должен совмещать более одной ответственности. Принцип единой ответственности SRP — отличное руководство для предотвращения запаха этого кода и для написания более стабильного кода в целом. Это может быть сложно следовать, но тем не менее все же стоит молоть.

Давайте посмотрим на этот неприятный пример ниже. Я немного изменил пример операции с дробовиком. Блофельд , глава Spectre, может быть знаком с микроменеджментом, но я сомневаюсь, что ему будет интересна половина вещей, которыми занимается этот класс.

« `ruby class Spectre

STANDARD_CHARGE = 10000000 STANDARD_PAYOUT = 200000

def charge_for_initializing_operation (client) evil_client_id = PaymentGem.find_client (client.email) .payments_id PaymentGem.charge (evil_client_id, STANDARD_CHARGE) конец

def contractor_payout (подрядчик) spectre_agent_id = PaymentGem.find_contractor (contractor.email) .payments_id PaymentGem.transfer_funds (spectre_agent_id, STANDARD_PAYOUT) конец

def assign_new_operation (operation) operation.contractor = ‘Какой-то злой чувак’ operation.objective = ‘Украсть кучу ценных вещей’ operation.deadline = ‘Полночь, 18 ноября’ конец

def print_operation_assignment (operation) print «# {operation.contractor} присваивается # {operation.objective}. Крайний срок выполнения миссии заканчивается на # {operation.deadline}. ”End

def dispose_of_agent (spectre_agent) ставит «Вы разочаровали эту организацию. Вы знаете, как Спектр обрабатывает неудачи. До свидания # {spectre_agent.code_name}! ”Конец конец` «

В классе Spectre слишком много разных вещей, о которых он беспокоится:

  • Назначение новых операций
  • Зарядка за грязную работу
  • Распечатка заданий на миссии
  • Убийство неудачных призрачных агентов
  • Работа с внутренностями PaymentGem
  • Оплата их Спектру агентам / подрядчикам
  • Он также знает о суммах денег для начисления и выплаты

Семь разных обязанностей в одном классе. Не хорошо! Вам нужно изменить способ утилизации агентов? Один вектор для изменения класса Spectre . Вы хотите обрабатывать выплаты по-другому? Еще один вектор. Вы получаете дрейф.

Хотя этот пример далеко не реалистичен, он все же рассказывает о том, как легко излишне накапливать поведение, которое необходимо часто менять в одном месте. Мы можем сделать лучше!

« `ruby class Spectre #…

def dispose_of_agent (spectre_agent) ставит «Вы разочаровали эту организацию. Вы знаете, как Спектр обрабатывает неудачи. До свидания # {spectre_agent.code_name}! ”Конец

класс PaymentHandler STANDARD_CHARGE = 10000000 STANDARD_CONTRACTOR_PAYOUT = 200000

# …

def initialize (payment_handler = PaymentGem) @payment_handler = payment_handler end

def charge_for_initializing_operation (evil_client) evil_client_id = @ payment_handler.find_client (evil_client.email) .payments_id @ payment_handler.charge (evil_client_id, STANDARD_CHARGE) конец

def contractor_payout (подрядчик) spectre_agent_id = @ payment_handler.find_contractor (contractor.email) .payments_id @ payment_handler.transfer_funds (spectre_agent_id, STANDARD_CONTRACTOR_PAYOUT) конец конец конец

класс EvilClient #…

def charge_for_initializing_operation PaymentHandler.new.charge_for_initializing_operation (self) end end

Подрядчик класса №…

def process_payout PaymentHandler.new.contractor_payout (self) end end

Операция класса attr_accessor: подрядчик,: цель,: крайний срок

def initialize (attrs = {}) @contractor = attrs [: подрядчик] @objective = attrs [: цель] @deadline = attrs [: дедлайн] конец

def print_operation_assignment print «# {подрядчик} назначен на {{цель}. Крайний срок миссии заканчивается в # {крайний срок}. ”End end` «

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

Вы хотите обрабатывать платежи по-другому? Теперь вам не нужно трогать класс Spectre . Вам нужно взимать или платить по-другому? Опять же, не нужно открывать файл для Spectre . Распечатка назначений операций теперь является бизнесом операций — там, где она принадлежит. Вот и все. Не слишком сложный, я думаю, но определенно один из наиболее распространенных запахов, с которыми вам следует научиться справляться рано.

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

Конечно, примеры из реальной жизни будут намного сложнее, но если вы поняли механику и закономерности обнаружения запахов, вы наверняка сможете быстро адаптироваться к реалистичным сложностям.