темы
- Берегись
- сопротивление
- Большой класс / Бог класс
- Извлечь класс
- Длинный метод
- Длинный список параметров
Берегись
Следующая небольшая серия статей предназначена для немного опытных разработчиков и начинающих разработчиков Ruby. У меня сложилось впечатление, что запахи кода и их рефакторинг могут быть очень пугающими и пугающими для новичков, особенно если они не в удачном положении, чтобы иметь наставников, которые могут превратить мистические концепции программирования в яркие лампочки.
Я, очевидно, сам ходил в этих туфлях, я вспомнил, что было излишне туманно вдыхать запахи и рефакторинг кода.
С одной стороны, авторы ожидают определенного уровня мастерства и, следовательно, могут не чувствовать себя супер-вынужденными предоставлять читателю тот же объем контекста, который может понадобиться новичку, чтобы быстрее погрузиться в этот мир.
Как следствие, может быть, у новичков, с другой стороны, создается впечатление, что им следует подождать немного дольше, пока они не станут более опытными, чтобы узнать о запахах и рефакторинге. Я не согласен с таким подходом и считаю, что, сделав эту тему более доступной, они смогут лучше разрабатывать программное обеспечение в начале своей карьеры. По крайней мере, я надеюсь, что это помогает обеспечить молодым людям хороший старт.
Так о чем мы говорим, когда люди упоминают о запахах кода? Это всегда проблема в вашем коде? Не обязательно! Можете ли вы избежать их полностью? Я так не думаю! Вы имеете в виду, что запахи кода приводят к поломке кода? Ну, иногда, а иногда нет. Должен ли я быть моим приоритетом, чтобы исправить их сразу? Боюсь, ответ тот же: иногда да, а иногда, конечно, сначала надо жарить большую рыбу. Ты псих? Справедливый вопрос на данный момент!
Прежде чем вы продолжите погружаться в весь этот вонючий бизнес, не забудьте убрать одну вещь из всего этого: не пытайтесь устранить каждый запах, с которым вы сталкиваетесь — это, безусловно, пустая трата вашего времени!
Мне кажется, что запахи кода немного сложно обернуть в красиво маркированную коробку. Существуют всевозможные запахи с различными вариантами их устранения. Кроме того, разные языки программирования и структуры подвержены различным запахам, но среди них определенно много общих «генетических» штаммов. Моя попытка описать запахи кода состоит в том, чтобы сравнить их с медицинскими симптомами, которые говорят вам, что у вас может быть проблема. Они могут указывать на все виды скрытых проблем и иметь широкий спектр решений при диагностике.
К счастью, в целом они не так сложны, как общение с человеческим телом и, конечно, с психикой. Это справедливое сравнение, потому что некоторые из этих симптомов нужно лечить сразу, а некоторые другие дают вам достаточно времени, чтобы найти решение, которое лучше всего подходит для общего благополучия «пациента». Если у вас есть работающий код, и вы столкнетесь с чем-то вонючим, вам придется принять трудное решение, стоит ли находить время, чтобы найти исправление, и если этот рефакторинг повышает стабильность вашего приложения.
При этом, если вы наткнетесь на код, который вы можете улучшить прямо сейчас, это хороший совет, чтобы оставить код немного лучше, чем раньше — даже чуть-чуть лучше со временем существенно увеличивается.
сопротивление
Качество вашего кода становится сомнительным, если включение нового кода становится более трудным — например, решение, куда помещать новый код, является болезненным или сопровождается множеством волновых эффектов в вашей кодовой базе, например. Это называется сопротивлением.
В качестве ориентира для качества кода вы всегда можете измерить его по тому, насколько легко вносить изменения. Если это становится все труднее и труднее, то определенно пришло время провести рефакторинг и более серьезно отнестись к последней части красно-зеленого REFACTOR в будущем.
Большой класс / Бог класс
Давайте начнем с чего-то необычного звучания — «Уроки Бога» — потому что я думаю, что их особенно легко понять начинающим. Бог-классы — это особый случай кодового запаха под названием Большой класс . В этом разделе я расскажу им обоим. Если вы провели немного времени на земле Rails, вы, вероятно, видели их так часто, что они выглядят нормально для вас.
Вы наверняка помните мантру «толстые модели, тощий контроллер»? Ну, на самом деле, тощий хорош для всех этих классов, но я полагаю, что это хороший совет для новичков.
Классы Бога — это объекты, которые привлекают всевозможные знания и поведение, как черная дыра. Ваши обычные подозреваемые чаще всего включают модель User и любую проблему (надеюсь!), Которую пытается решить ваше приложение — по крайней мере, в первую очередь. Приложение todo может накапливаться на модели Todos , приложение для покупок в продуктах , приложение для фотографий на фотографиях — вы получите смещение.
Люди называют их богами, потому что они знают слишком много. У них слишком много связей с другими классами — в основном потому, что кто-то их лениво моделировал. Однако тяжело контролировать божий класс. Они позволяют очень легко брать на себя больше обязанностей, и, как засвидетельствует множество греческих героев, требуется немного навыка, чтобы разделить и победить «богов».
Проблема с ними заключается в том, что их становится все труднее понять, особенно для новых членов команды, труднее измениться, и повторное их использование становится все менее и менее вероятным вариантом, чем больше накоплена серьезность. Ах да, вы правы, ваши тесты также излишне трудны для написания. Короче говоря, нет большого преимущества в том, чтобы иметь большие классы, и особенно классы богов.
Есть несколько общих симптомов / признаков того, что вашему классу требуется героизм / операция:
- Вам нужно прокрутить!
- Тонны частных методов?
- У вашего класса есть семь или более методов?
- Трудно сказать, что на самом деле делает ваш класс — кратко!
- Есть ли у вашего класса много причин для изменения, когда ваш код развивается?
Кроме того, если вы щурились в своем классе и думаете: «А? Фу! »Вы тоже можете быть чем-то заняты. Если все это звучит знакомо, велика вероятность, что вы оказались прекрасным образцом.
« `ruby class CastingInviter EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+.)+[az]]2, rout)\z/
attr_reader: сообщение,: приглашенные,: кастинг
def initialize (attribute = {}) @message = attribute [: message] || » @invitees = attribute [: приглашенные] || » @sender = attribute [: sender] @casting = attribute [: casting] end
определение действительно? valid_message? && valid_invitees? конец Def Delivery если действительный? Invitee_list.each делать | Электронная почта | приглашение = создать_инвитацию (электронная почта) Mailer.invitation_notification (приглашение, @ сообщение) конец еще fail_message = "Ваше сообщение # {@casting} не может быть отправлено. Письма приглашенных или сообщение недействительны" приглашение = создать_инвитацию (@sender) Mailer.invitation_notification (приглашение, error_message) конец конец частный def invalid_invitees @invalid_invitees || = Invitee_list.map do | item | если не item.match (EMAIL_REGEX) вещь конец end.compact конец def Invitee_list @invitee_list || = @ Invitees.gsub (/ \ s + /, '') .split (/ [\ n,;] + /) конец def valid_message? @ Message.present? конец def valid_invitees? invalid_invitees.empty? конец
def create_invitation (электронная почта) Invitation.create (приведение: @casting, отправитель: @sender, пригласить_email: электронная почта, статус: ‘в ожидании’) конец конец « `
Уродливый парень, а? Можете ли вы увидеть, сколько мерзости здесь? Конечно, я положил немного вишни сверху, но рано или поздно вы столкнетесь с таким кодом. Давайте подумаем о том, какие обязанности должен CastingInviter
этот класс CastingInviter
.
- Доставка электронной почты
- Проверка действительных сообщений и адресов электронной почты
- Избавляемся от пустого пространства
- Разделение адресов электронной почты на запятые и точки с запятой
Должно ли все это быть сброшено на класс, который просто хочет доставить вызов с помощью deliver
? Конечно, нет! Если ваш метод приглашения изменится, вы можете столкнуться с какой-нибудь операцией с дробовиком . CastingInviter не нужно знать большинство этих деталей. Это больше ответственность некоторого класса, который специализируется на работе с электронной почтой. В будущем вы также найдете здесь много причин для изменения своего кода.
Извлечь класс
Так как мы должны иметь дело с этим? Зачастую извлечение класса является удобным шаблоном рефакторинга, который будет представлять собой разумное решение таких проблем, как большие извилистые классы, особенно когда рассматриваемый класс имеет дело с несколькими обязанностями.
Частные методы часто являются хорошими кандидатами для начала, а также легко оцениваются. Иногда вам нужно извлечь еще несколько уроков из такого плохого парня — просто не делайте все за один шаг. Как только вы найдете достаточно связного мяса, которое, кажется, принадлежит отдельному специализированному объекту, вы сможете извлечь эту функциональность в новый класс.
Вы создаете новый класс и постепенно перемещаете функциональность по очереди. Переместите каждый метод отдельно и переименуйте их, если видите причину. Затем создайте ссылку на новый класс в исходном и делегируйте необходимые функции. Хорошо, что у вас есть тестовое покрытие (надеюсь!), Которое позволяет вам проверять, все ли по-прежнему работает должным образом на каждом этапе пути. Стремитесь к тому, чтобы иметь возможность повторно использовать извлеченные классы. Проще увидеть, как это делается в действии, поэтому давайте прочитаем некоторый код:
« `ruby класс CastingInviter
attr_reader: сообщение,: приглашенные,: кастинг
def initialize (attribute = {}) @message = attribute [: message] || » @invitees = attribute [: приглашенные] || » @casting = attribute [: casting] @sender = attribute [: sender] end
определение действительно? casting_email_handler.valid? конец
def поставить casting_email_handler.deliver end
частный
def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (сообщение: сообщение, приглашенные: приглашенные, кастинг: кастинг, отправитель: @sender) конец конец « `
« `ruby class CastingEmailHandler EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+.)+[az]]]2, rout)\z/
def initialize (attr = {}) @message = attr [: message] || » @invitees = attr [: приглашенные] || » @casting = attr [: casting] @sender = attr [: sender] end
определение действительно? valid_message? && valid_invitees? конец
def доставить, если действует? Invitee_list.each делать | Электронная почта | приглашение = create_invitation (электронная почта) Mailer.invitation_notification (приглашение, @message) end else fail_message = «Ваше сообщение # {@casting} не может быть отправлено. Электронные письма или сообщения приглашенных являются недействительными ». Приглашение = create_invitation (@sender) Mailer.invitation_notification (приглашение, fail_message) конец конец
частный
def invalid_invitees @invalid_invitees || = Invitee_list.map do | item | если item.match (EMAIL_REGEX) конец элемента end.compact конец
def invite_list @invitee_list || = @ пригласить.gsub (/ \ s + /, ») .split (/ [\ n,;] + /) end
def valid_invitees? invalid_invitees.empty? конец
def valid_message? @ Message.present? конец
def create_invitation (электронная почта) Invitation.create (приведение: @casting, отправитель: @sender, пригласить_email: электронная почта, статус: ‘в ожидании’) конец конец « `
В этом решении вы не только увидите, как это разделение проблем влияет на качество вашего кода, но и намного лучше читается и становится легче переваривать.
Здесь мы делегируем методы новому классу, который специализируется на доставке этих приглашений по электронной почте. У вас есть одно специальное место, которое проверяет, являются ли сообщения и приглашенные действительными и как они должны быть доставлены. CastingInviter
не нужно ничего знать об этих деталях, поэтому мы делегируем эти обязанности новому классу CastingEmailHandler
.
Знание того, как доставлять и проверять действительность этих писем с приглашениями на кастинг, теперь содержится в нашем новом извлеченном классе. У нас есть больше кода сейчас? Вы держите пари! Стоило ли это разделять интересы? Достаточно уверен! Можем ли мы пойти дальше и реорганизовать CastingEmailHandler
еще? Абсолютно! Сбей себя с ног!
В случае, если вам интересно о valid?
метод CastingEmailHandler
и CastingInviter
, этот для RSpec для создания пользовательского сопоставления. Это позволяет мне написать что-то вроде:
ruby expect(casting_inviter).to be_valid
Довольно удобно, я думаю.
Есть больше техник для работы с большими классами / объектами бога, и в ходе этой серии вы узнаете несколько способов реорганизации таких объектов.
Не существует фиксированного рецепта для работы с этими случаями — это всегда зависит, и это решение для каждого отдельного случая, если вам нужно принести большие орудия или, если меньшие, методы постепенного рефакторинга требуют больше всего. Я знаю, немного разочаровывает время от времени. Однако следование принципу единой ответственности (SRP) будет иметь большое значение и является хорошим носом для подражания.
Длинный метод
Использование методов, которые стали немного шире, — одна из самых распространенных вещей, с которыми вы сталкиваетесь как разработчик. В общем, вы хотите сразу узнать, что должен делать метод. Он также должен иметь только один уровень вложенности или один уровень абстракции. Короче говоря, избегайте написания сложных методов.
Я знаю, это звучит сложно, и это часто так. Решение, которое часто встречается, состоит в извлечении частей метода в одну или несколько новых функций. Этот метод рефакторинга называется методом извлечения — он один из самых простых, но, тем не менее, очень эффективных. Как хороший побочный эффект, ваш код становится более читабельным, если вы правильно называете свои методы.
Давайте рассмотрим технические характеристики, в которых эта техника вам очень понадобится. Я помню, как познакомился с методом извлечения при написании таких спецификаций функций, и как удивительно это было, когда лампочка включалась. Поскольку технические характеристики, подобные этим, легко понять, они являются хорошим кандидатом для демонстрации. Кроме того, вы будете сталкиваться с подобными сценариями снова и снова, когда будете писать свои спецификации.
спецификации / особенности / some_feature_spec.rb
« `ruby require ‘rails_helper’
функция «M помечает миссию как выполненную», «выполнить сценарий» успешно, выполнить visit_root_path fill_in «Email» с помощью: «[email protected]» click_button «Отправить», посетить missions_path click_on «Создать миссию», fill_in «Название миссии», с: «Project Moonraker ‘click_button’ Submit ‘
в "li: содержит ('Project Moonraker')" сделать click_on 'Миссия выполнена' конец Ожидается (страница) .to have_css 'ul.missions li.mission-name.completed', текст: 'Project Moonraker' конец конец `` `
Как вы можете легко заметить, в этом сценарии многое происходит. Вы переходите на страницу индекса, входите в систему и создаете миссию для настройки, а затем выполняете упражнение, помечая миссию как завершенную, и, наконец, вы проверяете поведение. Нет ракетостроения, но тоже не чисто и определенно не составлено для повторного использования. Мы можем сделать лучше, чем это:
спецификации / особенности / some_feature_spec.rb
« `ruby require ‘rails_helper’
функция «M помечает миссию как завершенную», «выполнить сценарий» успешно, «выполнить sign_in_as», [email protected], «создать_классифицированную_задание» с именем «Project Moonraker».
mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission конец проекта Project Moonraker
def create_classified_mission_named (mission_name) посещение missions_path click_on «Создать миссию», заполните «Имя миссии», с помощью: mission_name click_button «Submit» end
def mark_mission_as_complete (mission_name) в пределах «li: contains (‘# {mission_name}’)» do click_on «Миссия выполнена» end end
def agent_sees_completed_mission (mission_name) ожидаемый (страница) .to have_css ‘ul.missions li.mission-name.completed’, текст: название_миссии end
def sign_in_as (электронная почта) посетить root_path fill_in ‘Email’, с: email click_button ‘Submit’ end « `
Здесь мы извлекли четыре метода, которые теперь можно легко использовать в других тестах. Надеюсь, ясно, что мы ударили трех зайцев одним выстрелом. Эта функция намного более краткая, она лучше читается и состоит из извлеченных компонентов без дублирования.
Давайте представим, что вы написали все виды подобных сценариев без извлечения этих методов, и вы хотели изменить какую-то реализацию. Теперь вы хотели бы потратить время на рефакторинг ваших тестов и иметь одно центральное место для применения ваших изменений.
Конечно, есть еще лучший способ иметь дело со спецификациями функций, такими как эта, например, Page Objects, но это не наша задача на сегодня. Я думаю, это все, что вам нужно знать о методах извлечения. Вы можете применять этот шаблон рефакторинга везде в вашем коде — не только в спецификациях, конечно. С точки зрения частоты использования, я предполагаю, что это будет ваша техника номер один для улучшения качества вашего кода. Веселиться!
Длинный список параметров
Давайте закроем эту статью примером того, как вы можете уменьшить свои параметры. Это довольно быстро становится утомительным, когда вам приходится кормить ваши методы более чем одним или двумя аргументами. Разве не было бы неплохо добавить один объект? Это именно то, что вы можете сделать, если введете объект параметра .
Все эти параметры не только трудны для написания и поддержания порядка, но также могут привести к дублированию кода — и мы, конечно, хотим избежать этого везде, где это возможно. Что мне особенно нравится в этой технике рефакторинга, так это то, как она влияет на другие методы внутри. Вы часто можете избавиться от большого количества ненужных параметров в пищевой цепочке.
Давайте рассмотрим этот простой пример. M может назначить новую миссию и нуждается в названии миссии, агенте и цели. M также может переключать статус двойного 0 агентов, что означает их лицензию на уничтожение.
« `ruby class M def assign_new_mission (mission_name, agent_name, target, licence_to_kill: nil) print« Миссия # {mission_name} была назначена # {agent_name} с целью # {target}. », если licence_to_kill напечатает« Лицензия убить предоставлено. »иначе печать« Лицензия на убийство не была предоставлена ». конец конец конец
m = M.new m.assign_new_mission («Осьминог», «Джеймс Бонд», «найти ядерное устройство», licence_to_kill: true) # => Миссия «Осьминог» была назначена Джеймсу Бонду с целью найти ядерное устройство. Лицензия на убийство была предоставлена. « `
Когда вы смотрите на это и спрашиваете, что происходит, когда «параметры» миссии усложняются, вы уже что-то поняли. Это болевая точка, которую вы можете решить, только если передадите один объект, содержащий всю необходимую вам информацию. Чаще всего это также помогает вам избежать изменения метода, если объект параметра изменяется по какой-либо причине.
« `ruby class Mission attr_reader: имя_миссии,: имя_агента,: цель,: licence_to_kill
инициализация по умолчанию (имя_миссии: имя-миссии: имя-агента, имя-агента: имя-агента, цель: цель, licence_to_kill: licence_to_kill) @mission_name = имя-миссии @agent_name = имя-агента @objective = цель @licence_to_kill = licence_to_kill end
Def Назначить print "Миссия # {имя_миссии} была назначена # {agent_name} с целью # {target}." если licence_to_kill print "Лицензия на убийство выдана". еще print "Лицензия на убийство не выдана". конец конец конец
класс M def assign_new_mission (mission) mission.assign конец конец
m = M.new mission = Mission.new (название миссии: ‘Octopussy’, имя агента: ‘James Bond’, цель: ‘найти ядерное устройство’, licence_to_kill: true) m.assign_new_mission (mission) # => Миссия Octopussy была назначен Джеймсу Бонду с целью найти ядерное устройство. Лицензия на убийство была предоставлена. « `
Таким образом, мы создали новый объект Mission
, который предназначен исключительно для предоставления M
информации, необходимой для назначения новой миссии, и предоставления #assign_new_mission
с единственным параметром. Не нужно передавать эти надоедливые параметры самостоятельно. Вместо этого вы указываете объекту раскрывать нужную вам информацию внутри самого метода. Кроме того, мы также извлекли некоторое поведение — информацию о том, как печатать — в новый объект Mission
.
Зачем M
знать, как печатать задания для миссий? Новый #assign
также выиграл от извлечения, потеряв некоторый вес, потому что нам не нужно было передавать объект параметра — поэтому не нужно писать такие вещи, как mission.mission_name
, mission.agent_name
и так далее. Теперь мы просто используем наши attr_reader
(s), которые намного чище, чем без извлечения. Вы копаете?
Что еще удобно в этом, так это то, что Mission
может собирать все виды дополнительных методов или состояний, которые хорошо инкапсулированы в одном месте и готовы для вас.
С помощью этой техники вы получите методы, которые будут более краткими, будут лучше читать и избегать повторения одной и той же группы параметров повсюду. Довольно хорошая сделка! Избавление от идентичных групп параметров также является важной стратегией для DRY-кода.
Постарайтесь извлечь больше, чем просто ваши данные. Если вы сможете поместить поведение и в новый класс, у вас появятся более полезные объекты, иначе они тоже быстро начнут пахнуть.
Конечно, большую часть времени вы столкнетесь с более сложными версиями этого — и ваши тесты, безусловно, также должны быть адаптированы одновременно во время таких рефакторингов — но если у вас есть этот простой пример под вашим поясом, вы будете готовы к действию ,
Я собираюсь посмотреть новый Бонд сейчас. Слышал, что это не так хорошо, хотя …
Обновление: Пила Призрак. Мой вердикт: по сравнению со Скайфоллом, которым был MEH imho, Spectre был wawawiwa!