Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Ruby
Code

Антипаттерны: модели Rails

by
Length:LongLanguages:

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Анти-что? Вероятно, это звучит намного сложнее, чем есть. За последние пару десятилетий программисты смогли определить полезный выбор шаблонов «дизайна», которые часто встречались во всех их кодовых решениях. Решая подобные проблемы, они смогли «классифицировать» решения, которые избавляли их от постоянного изобретения колеса. Важно отметить, что эти шаблоны следует рассматривать скорее как открытия, чем изобретения группы продвинутых разработчиков.

Если это для вас совсем не так, и вы видите себя в большей степени на стороне новичка Ruby/Rails, то это точно написано для вас. Я думаю, что лучше всего, если вы посмотрите на это как на очень глубокую тему, постижение которой не произойдет за одну ночь. Тем не менее, я твердо верю, что погружение в нее принесет новичкам огромную пользу.

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

Не стоит недооценивать последствия этих плохих решений - они будут вас мучить, несмотря ни на что.

Темы

  • Жирные модели
  • Отсутствует комплект для тестирования
  • Подглядывающие модели
  • Закон Деметры
  • Спагетти SQL

Жирные модели

Я уверен, что вы слышали о «Толстые модели, тонкие контроллеры», когда вы впервые начали с Rails. Хорошо, теперь забудьте об этом! Разумеется, бизнес-логику нужно хранить в модельном слое, но вы не должны чувствовать себя склонным к тому, чтобы все это происходило бессмысленно, чтобы избежать пересечения линий на территории контроллера.

Вот новая цель, к которой вы должны стремиться: «Тощие модели, тощие контроллеры». Вы могли бы спросить: «Ну и как мы должны организовать код для достижения этого? Ведь это игра с нулевой суммой?» Хорошо! Название игры - композиция, а Ruby хорошо оборудован, чтобы дать вам множество вариантов, чтобы избежать ожирения модели.

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

Вопрос в том, как вы намереваетесь справиться с такой сложностью. Active Record определенно дает вам много веревки, чтобы повеситься, делая вашу жизнь невероятно легкой. Это заманчивый подход к дизайну вашего слоя модели, просто следуя пути наивысшего удобства. Тем не менее, будущая архитектура требует гораздо большего внимания, чем культивирование огромных классов и добавление всего в объекты Active Record.

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

Мы можем сделать лучше! Если вы считаете, что сложность не имеет большого значения - в конце концов, вы особенный, умный и продуманный! Сложность - самый известный серийный убийца проекта, а не ваш дружественный «Темный защитник».

«Тонкие модели» очень ценятся среди продвинутых парней в сфере разработки (возможно, гораздо больше профессий, чем кода и дизайна), и мы должны максимально стремиться к простоте! Или, по крайней мере, к тому, что является справедливым компромиссом, если сложность трудно искоренить.

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

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

one man band

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

У нас есть класс Spectre  который имеет слишком много обязанностей и поэтому излишне разрос в размерах. Помимо этих методов, я думаю, что легко представить, что такой образец уже собрал много других вещей, также представленных тремя маленькими точками. Spectre уже на пути к становлению God классом. (Шансы довольно низкие, чтобы разумно сформулировать такое предложение снова в ближайшее время!)

Spectre превращает различные вражеские агенты, убивает делегатов 007, ребрирует членов кабинета Spectre’s  когда они терпят неудачу, а также распечатывает операционные задания. Явный случай микроменеджмента и, безусловно, нарушение «принципа единой ответственности». Приватные методы также быстро накапливаются.

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

Я думаю, что самая важная часть, на которую вы должны обратить внимание, - это то, как мы использовали простой класс Ruby, например Interrogator, для обработки агентов из разных агентств. Реальные примеры могут представлять собой конвертер, который, скажем, преобразует HTML-документ в pdf и наоборот. Если вам не нужны полные функциональные возможности классов Active Record, зачем их использовать, если простой трюк Ruby также может сделать тоже самое? Уже немного меньше веревки, чтобы повеситься.

Класс Specter переносит неприятную логику агентов в класс Interrogator и просто делегирует ему эту задачу. У него теперь есть единственная ответственность за пытки и промывание мозгов захваченными агентами.

Все идет нормально. Но почему мы создали отдельные классы для каждого агента? Просто. Вместо прямого извлечения различных методов, таких как turn_mi6_agent, до Interrogator, мы предоставили им новое место в своем собственном классе.

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

Поскольку все эти агенты являются объектами Active Record, мы создали общий, EnemyAgent, который имеет общее представление о том, что означает агент, и мы инкапсулируем этот бит для всех агентов в одном месте. Мы используем наследование, предоставляя методы turn различных агентов с помощью super, и поэтому мы получаем доступ к делу по «промыванию мозгов» и пыткам без дублирования. Отдельные обязанности и отсутствие дублирования являются хорошей отправной точкой для продолжения.

Другие классы Active Record берут на себя различные обязанности, о которых Spectre не нужно заботиться. «Номер один» обычно делает гриль неудачных членов кабинета Spectre, поэтому почему бы не позволить специализированному объекту справиться с электрическим током? С другой стороны, неспособность членов Спектра умереть самим, когда их поджаривают на электрическом стуле с помощью NumOne . Operation cамо собой подсказывает, что больше нет надобности хранить этот код в самом Spectre.

И последнее, но не менее важное: убийство Джеймса Бонда обычно предпринимает агент в поле, поэтому kill_james_bond теперь является методом SpectreAgent. Разумеется, Голдфингер справился бы с этим по-другому, конечно же, с этой лазерной штучкой.

Как вы можете ясно видеть, теперь у нас есть десять классов, а раньше был только один. Разве это не слишком много? Это может быть, конечно. Это проблема, с которой вам придется сражаться большую часть времени, когда вы разделяете такие обязанности. Вы можете определенно переусердствовать. Но взгляд на это под другим углом может помочь:

  • Разделили ли мы ответственности? Абсолютно!
  • У нас есть легкие, тощие классы, которые лучше подходят для решения своих собственных обязанностей? Определенно!
  • Рисуем ли мы теперь более четкую картину того, кто участвует, и отвечает за определенные действия? Я надеюсь, что это так!
  • Легче ли переварить то, что делает каждый класс? Наверняка!
  • Уменьшили ли мы количество приватных методов? Ага!
  • Означает ли это лучшее качество объектно-ориентированного программирования? Поскольку мы использовали композицию и обращались к наследованию только там, где это необходимо для создания этих объектов, то определенно это так!
  • Чувствуется себя чище? Да!
  • Мы лучше вооружены, чтобы была возможность легко изменить наш код, не создавая беспорядка? Конечно!
  • Стоило ли оно того? Как вы думаете?

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

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

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

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

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

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

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

Подглядывающие модели

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

Закон Деметры

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

Закон Деметры - про такой запах кода, который вы всегда можете атаковать, не беспокоясь о возможных недостатках.

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

Это общий способ объяснить это, и все это сводится к использованию не более одной точки для вызовов ваших методов. Кстати, совершенно нормально использовать больше точек или вызовов методов, когда вы имеете дело с одним объектом. Что-то вроде @weapons.find_by_name('Poison dart').formula вполне нормально. Формула просто прекрасна. Иногда объекты могут накапливать несколько точек. Тем не менее, хорошая идея заключается в их инкапсулировании специальными методами.

Нарушения закона Деметры 

Давайте рассмотрим пару плохих примеров из вышеперечисленных классов:

Чтобы узнать об этом, вот еще несколько вымышленных:

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

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

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

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

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

Мы можем сделать лучше. Там, где это возможно, давайте делегируем вызовы методов непосредственно их объектам и стараемся максимально сократить методы обёртки. Rails знает, что нам нужно, и предоставляет нам удобный метод класса delegate, чтобы сообщить друзьям нашего объекта, какие методы нам нужны.

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

Как вы можете видеть, мы могли немного упростить работу с помощью делегирования методов. Мы полностью избавились от Operation#spectre_member_name и Operation#spectre_member_number, а SpectreAgent больше не нужно вызывать number у spectre_member - number передается обратно прямо в его «исходный» класс SpectreMember.

Сперва все это немного запутывает, как это работает? Вы указываете делегату, какой :method_name он должен делегировать :to какому :class_name (несколько имен методов тоже нормально).  Часть prefix: true является необязательной.

В нашем случае он предварял имя класса класса в snake-cased формате получающего класса перед именем метода и дал нам возможность вызвать operator.spectre_member_name вместо потенциально неоднозначного operation.name если бы мы не использовали опцию префикса. Это очень хорошо работает с ассоциациями belongs_to и has_one.

Однако на стороне has_many, музыка остановится, и у вас возникнут проблемы. Эти ассоциации предоставляют вам прокси коллекции, который будет бросать вам имена или ошибки NoMethodErrors, когда вы делегируете методы этим «коллекциям».

Спагетти SQL

Чтобы завершить эту главу об антипаттернах модели в Rails, я хотел бы потратить немного времени на то, чего следует избегать при использовании SQL. Ассоциации Active Record предоставляют варианты, которые значительно упрощают вашу жизнь, когда вы знаете, чего вам следует избегать. Методы поиска - это целая тема сами по себе, и мы не будем их охватывать в полной мере, но я хотел упомянуть несколько общих техник, которые помогут вам.

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

Выглядит безвредно, нет? Мы просто ищем кучу агентов, у которых есть лицензия на убийство для нашей страницы ops. Подумайте еще раз. Почему OperationsController должен копаться во внутренности Agent? Кроме того, это действительно лучшее, что мы можем сделать, чтобы инкапсулировать поиск Agent?

Если вы думаете, что можете добавить метод класса, такой как Agent.find_licence_to_kill_agents, который инкапсулирует логику поиска, вы определенно делаете шаг в правильном направлении, но этого еще не достаточно.

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

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

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

Это намного лучше. В случае, если синтаксис областей не нов для вас, это просто (неубедительные) lambdas - не обаятельно смотреть на них таким образом, между прочим, они являются надлежащим способом вызова областей с Rails 4. Agent теперь сам отвечает за управление собственными параметрами поиска, а ассоциации могут просто зависеть от того, что им нужно найти.

Такой подход позволяет получать запросы как единичные вызовы SQL. Мне лично нравится использовать возможности scope. Области также очень удобны для цепочки внутри хорошо известных методов поиска - таким образом они увеличивают возможность повторного использования кода и проводят его в соответствие с принципом DRY. Допустим, у нас есть что-то более активное:

Теперь мы можем использовать все эти области для пользовательской сборки более сложных запросов.

Конечно, это работает, но я хотел бы предложить вам сделать еще один шаг вперед.

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

Если вы беспокоитесь о нарушении Закона Деметры, вам будет приятно услышать это. Поскольку мы не добавляем точки, входя в связанную модель, но связывая их только с их собственным объектом, мы не совершаем никаких нарушений закона Деметры.

Последние мысли

С точки зрения новичка, я думаю, вы много узнали о лучшей обработке моделей Rails и о том, как их моделировать.

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

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

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.