Данная статья является конспектом книги "Чистый Код" Роберта Мартина и моим пониманием того, каким Чистый Код должен быть. Тут нет разделов о тестировании, TDD, о том какая должна быть архитектура и т.д. Здесь все только о том, каким должен быть Чистый Код.
Да, возможно, тема Чистого Кода уже заезженна, но тем не менее еще не все с ним знакомы и, тем более, я не встретил аналогов контента, который содержится в моей статье.
Общее
Нет истинного пути и решения. Есть тот, который лучше всего подходит для решения конкретной задачи.
При решении задачи попытайся воспроизвести абсолютно все кейсы, которые могут затрагивать эту задачу и реализуй задачу с учетом абсолютно всех кейсов.
Также при решении задачи попробуй пойти от обратного. Пойми какие результаты в итоге ты хочешь получить и составь на этом основании алгоритм, по которому будет выполняться задача.
Перед тем, как отправить задачу в релиз — проверь правильно ли она работает. Нет ли в ней ошибок. Это касается даже тех коммитов, которые отправляются в твою ветку. Самый идеальный сценарий — тот, в котором никто не смог найти ошибки в функционале, который ты разрабатывал.
Всегда задумывайся о том как можно сделать твой код проще, чище и читабельнее.
- Какие кейсы могут быть у задачи?
- Все ли я учел?
- Что может пойти не так?
- Что можно объединить?
- Есть ли похожий функционал?
- Что тут лишнее?
- Как сделать проще?
- Как сделать читабельнее?
- Как сделать понятнее?
Чистый Код
Как писать чистый и хороший код? Это похоже на написание книги. Сначала ты делаешь черновик и потом причесываешь его до того состояния, в котором тебе было бы приятно его читать. Всегда помни, что твой код должен рассказывать историю происходящего, чтобы читатель мог ее понять.
Под сущностью понимается — интерфейс, класс, метод, переменная, объект и т.д.
- Чистый код простой, выразительный и направлен на конкретную задачу.
- Чистый код читается легко, как проза. Если это не так, то его стоит рефакторить.
- Чистый код легко изменять. Он не должен быть жестко завязан на куче сущностей. Любую сущность можно легко изменить.
- Чистый код намного лучше проходит ревью. Если ревью проходит с огромным количеством комментариев, то он не чистый и его надо рефакторить.
- Чистый код всегда выглядит так, словно над ним очень долго трудились. Какие бы пути для его улучшения ты не искал, ты все равно придешь к тому, что этот код лучший. Соответственно, чистый код — продуманный до всех мелочей.
- Правило бойскаута: Оставь место стоянки чище, чем оно было до тебя. Это легко перекладывается и на программирование. Видишь грязный код? Сделай его чище, пока решаешь свою задачу. Не стоит увлекаться этим и если грязный код очень грязный, то стоит выделить отдельную задачу и время для его очистки.
- Не бойся делать изменений. Если ты хочешь их сделать, то значит у тебя есть на то причины, а значит ты сделаешь код лучше и чище. Тем более тесты покажут нет ли ошибок в твоем коде (при условии, что они вообще есть).
- Любая сущность должна отвечать за один функционал и только за него. И она должна выполнять его хорошо. Single Responsibility.
- Если сущность отвечает сразу за два и более действий, то её функционал нужно разделять.
- Код должен читаться сверху вниз.
- В хорошей и грамотной архитектуре внесение изменений обходится без значительных затрат и усилий.
- Удаляй мертвый код. Мертвый код это код, который не будет вызван ни при каких условиях или код, который нигде не используется.
Наименования и разделения
- Используй понятные и удобнопроизносимые имена для любых сущностей. Они должны описывать почему эта сущность существует, что она делает и как используется.
- Не бойся тратить время на выбор лучшего и понятного имени. Ты выиграешь в будущем при работе или чтении этого кода.
- Если название сущности не соответствует еe функционалу или по названию не понятно, что сущность делает, то еe надо переименовать в самое понятное название. Если этого сделать невозможно, то значит с еe функционалом что-то не так и еe надо рефакторить.
- Сущность, которая имеет в названии "And", "With" — нарушает Single Responsibility. Функционал такой сущности стоит разделять. Но этим правилом стоит иногда пренебрегать.
- Непонятные тексты, строки стоит выносить в переменные и давать им понятные названия.
- Названия методов должны содержать глагол, который описывает, что этот метод делает и ключевое слово с которым работает данный метод. Если в названии метода нет глагола, то эта сущность не должна быть методом или ему нужно дать правильное название.
- Нужно избегать одинаковых наименований для двух разных целей.
- Если сущность имеет схожее с другой сущностью название, то скорее всего их функционал очень сильно похож и их нужно объединить? Если нет, то их названия нужно менять так, чтобы они не были похожими.
- Если ты мысленно переименовываешь сущность, когда читаешь код, чтобы тебе было понятнее понимать её функционал, то переименуй её в это мысленное название.
- Выбери одно слово для одной концепции. Сложно будет понимать функционал, когда у тебя есть fetch, retrieve и get в названиях. Пусть лучше везде будет get.
- Длинное и понятное имя лучше, чем короткое, но непонятное.
Функции
- Функции должны быть короткими и компактными.
- Функции должны быть очень короткими и очень компактными.
- Приблизительный максимум 20 строк и 150 символов в одной строке, если не влезает, то нужно разделять.
- Функция должна выполнять только одну операцию.
- Она должна выполнять её хорошо и ничего другого она делать не должна.
- Если функция выполняет только те действия, которые находятся на одном уровне абстракции, то функция выполняет одну операцию.
- Чтобы определить выполняет ли функция более одной операции, попробуй извлечь из нее другую функцию, которая не будет являться простой переформулировкой реализации.
- Любые условные операторы с длинными выборами через switch-case, if-else должны разделяться или объединяться без дублирования, возможно на классы с реализациями, а выбор реализации передать базовому классу, фабрике или еще кому-то.
- If, else, while и т.д. должны содержать вызов одной функции. Так будет читабельнее, понятнее и проще.
- Идеальное количество входных аргументов для функции = 0. Если входных аргументов больше трех, то стоит задуматься каким образом лучше от них избавиться, например, создать класс для этих аргументов.
- Чем больше входных аргументов, тем тяжелее понимается функция.
- Функция в которую передается аргумент-флаг, от которого зависит работа функции говорит о том, что функция выполняет более одной операции. Такие функции следует разбить на две и вызывать их уровнем выше.
- Функция, которая изменяет входной аргумент, должна отдавать ссылку на измененный объект, а не просто изменять без возврата.
String transform(String text)
- Если функция, должна изменять входной аргумент, то пусть она изменяет состояние своего объекта-владельца.
- Если входной аргумент функции не должен меняться (и используется дальше в коде), то следует скопировать значение аргумента и внутри функции работать с копией.
- Вместо return null лучше использовать пустой объект —
Collection.empty()
или null-объект -EmptyObject()
. - Всегда старайся использовать нестатические функции. Если это невозможно, то используй статические.
- Если есть код, который должен следовать один за другим, то передавай результаты первой функции во вторую, чтобы кто-нибудь не изменил последовательность вызовов.
- Используй полиморфизм вместо if/else или switch/case или when.
- Избегай отрицательных условий.
Комментарии
- Не используй комментарии, если ты можешь использовать функцию или переменную вместо этого.
- Не комментируй плохой код — перепиши его. Не стоит объяснять, что происходит в плохом коде, лучше сделать его явным и понятным.
- Комментарии можно использовать для передачи какой-то информации, предупреждения о последствиях, но не для объяснения того, как работает код.
- Используй TODO и FIXME в тех случаях, когда нужно пометить, что код нуждается в доработке, но сейчас нет ресурсов на это.
- Используй
//region REGIONNAME //endregion REGIONNAME
, а если используешь, то подумай можно ли разделить region на сущности. - Документируй код, который является сложным, но чистым.
- Не оставляй старый закомментированный код. Ты можешь найти его в истории коммитов, если необходимо.
- Комментарии должны быть краткими и понятными. В комментариях с информацией не должно быть много информации. Все должно быть кратко и по делу.
Форматирование и правила
- Соблюдай codestyle, принятый на проекте.
- Соблюдай правила, принятые в команде.
- При соблюдении форматирования и codestyle код будет читаться проще и лучше. Ведь не зря книгу отдают на редакцию, перед тем, как её издавать.
- Нужно иметь автоматические средства, которые будут форматировать код за тебя.
- Файл с исходным кодом должен быть как газетная статья. Есть заголовок, краткое описание в виде параметров и содержание в виде функций. Если это не так, то стоит изменить форматирование.
- Сущности, связанные друг с другом, должны находиться рядом, например, в одном package, чтобы было проще навигировать по коду.
- Переменные(поля) класса должны находиться вверху класса.
- Переменные методов должны находиться ближе к своему месту использования.
- Функции должны находиться в порядке вызова. Если одна вызывает другую, то вызывающая функция должна находиться над вызываемой. C другой стороны, приватные функции более низкого уровня могут находиться внизу файла и не мешать пониманию кода высокого уровня. Но я предпочитаю первый способ.
Объекты и структуры данных
- Ты должен работать с абстракциями, чтобы реализацию можно было легко изменить.
- Ты должен работать с абстракциями, потому что клиент, использующий функционал, не должен знать о деталях реализации, он должен знать какую реализацию в каком случае использовать.
- Ты должен предоставлять API, с которым стоит работать и скрывать детали реализации, структуру. Так будет проще работать с такими сущностями и добавлять новые виды поведений, функционала и реализаций.
- DTO — Data Transfer Object. Класс, который содержит только данные и никакого функционала. Нужен для того, чтобы передавать какие-то данные. Объект такого класса должен быть неизменяемым.
Классы
- Классы должны быть компактными.
- Классы должны быть еще компактнее.
- Имя класса должно описывать его ответственности. Отсюда можно и вычислить размер класса.
- Функционал класса должен четко соответствовать и вписываться в название класса.
- Разделяй связанность на маленькие классы. Жесткой и обильной связанности не должно быть — это усложняет поддержку и развитие проекта.
- Помни о Single Responsibility. Сущность должна иметь одну и только одну причину для изменения.
- Соблюдай инкапсуляцию. Ослабление инкапсуляции всегда должно быть последней мерой.
- Обычно мы объявляем переменные и вспомогательные функции приватными, но иногда их нужно объявлять protected и иметь возможность обратиться к ней из теста.
- Если группа функций относится к определенному функционалу, то эту группу функций можно и нужно выделить в отдельный класс и использовать его экземпляр.
Обработка ошибок
- Используй Exceptions вместо возвращения кодов ошибок.
- Обработка ошибок — это одна операция. Если в функции есть ключевое слово
try
, то после блоковcatch/finally
ничего другого в функции быть не должно. - Если у тебя есть enum, который перечисляет ошибки, то от него лучше избавиться и вместо него использовать исключения.
- Используй unchecked exceptions, чтобы явно указать на место в котором есть проблемы. Такие ошибки не нужно отлавливать, вместо этого нужно написать код так, чтобы этой ошибки никогда не было.
- Передавай достаточное количество информации вместе с выбросом исключения, чтобы потом пользователи твоего кода могли понять, что же действительно произошло.
- Вместо условных операторов с обработкой ошибок лучше выбрасывать исключения и обрабатывать их.
- Не передавай null куда-либо. Старайся этого максимально избежать.
- Обработка ошибок — это отдельная задача и не относится к основной логике программы.
Границы
- Мы всегда используем какие-либо библиотеки, которые чаще всего дают нам слишком широкий, слишком маленький функционал или конфликтуют с ожидаемым функционалом, что делает код грязнее в его конечном использовании. Избежать этого можно просто применив паттерны типа Decorator, Adapter, Facade или другие.
- Бывают ситуации, когда тебе нужно работать с функционалом, который находится в разработке или пока что не адаптирован для использования в продакшен коде. В этом случае стоит представить чего ты ждешь от библиотеки/этого функционала и написать свой интерфейс или создать сущность с которыми ты будешь работать в своем проекте так, как тебе нужно. Когда библиотека доделается и станет стабильной, ты адаптируешь её под свои готовые структуры и использовать уже готовый функционал.
Послесловие
В данной статье представлены лишь рекомендации к написанию Чистого Кода. Разумеется, ими можно пренебрегать. Вам лишь стоит понять, что у любого вашего решения должны быть аргументы в пользу него.
Комментарии (44)
Oskal174
24.09.2018 21:09Достойная выжимка, ничего нового, но конспект отличный. Можно распечатывать и класть на рабочий стол
tanchuev Автор
24.09.2018 21:20Спасибо) Собственно, с этой целью и создавался данный материал) Чтобы под рукой всегда была краткая памятка того, как стоит писать код)
gangstarcj
24.09.2018 21:50Спасибо вам за такой труд.
Часто не хватает таких конспектов по книгам. Самому составлять очень лень(
Hokum
24.09.2018 21:57Отличный труд! Но, мне кажется, лучше стараться делать функции чистыми и отказываться от модификации передаваемых аргументов. Если хочется изменить входной аргумент — пусть функция вернет модифицированную копию. Это не всегда возможно, но к этому стоит стремиться. Такой код проще читать и проще тестировать.
vanxant
26.09.2018 01:31… а еще он отлично жрет процессорное время и память (реклама производителей железок)
sentyaev
24.09.2018 23:14+1Не забудьте после каждого пункта поставить запятую и дописать: «but it depends...»
berezuev
24.09.2018 23:51Используй TODO и FIXME.
Крайне сомнительная рекомендация. Либо делай сразу, либо заводи задачу там, где она не потеряется. А вот уже ссылку на задачу можно добавить в TODO.Slonopotamych
25.09.2018 10:29Я вот тоже не очень понимаю, зачем приниципиально оставлять TODO в коде, а не создавать задачу в трекере задач. Недавно только чистили код от TODO, которые лежали просто годами.
khim
25.09.2018 13:16А TODO и должно ссылаться на задачу. Например сегодня мы работаем с x86, но в будущем нам, возможно, потребуется поддержать ARM. Заводится задача по поддержке ARM'а и всё TODO ссылаются на неё.
Можно ли сразу поддержать ARM? Можно, но не нужно: полноценная поддержка усложнит разработку в 1.5-2 раза, а если вы оставите «мёртвый код» без тестов и/или пользователей, то обнаружите, что к моменту, когда он понадобится — код «протухнет». TODO «протухают» тоже, конечно, но худшее, что с ними может произойти — они станут непонятными и вы их выкините, а странный код, появившийся ради задачи, от которой, в итоге, отказались будет вам мешать долгие годы — так как без комментариев все будут его видеть, но никто не будет понимать — зачем он там.Slonopotamych
25.09.2018 13:26Без этого вашего пояснения фраза
Используй TODO и FIXME.
вредна. Сегодня с утра один наш разработчик скинул в общий чат скриншот этой статьи с этой фразой. Тогда как у нас в компании всего месяц назад с моей подачи запретили оставлять в коде TODO без указания номера задачи.
Jopezzia
26.09.2018 14:05Ну, можно создать связку, которая переводит TODO сразу в трекер, для этого вроде уже существуют инструменты
tanchuev Автор
25.09.2018 10:33Это призыв к использованию TODO и FIXME в тех случаях, когда нужно пометить, что код нуждается в доработке.
deniss-s
25.09.2018 12:16+1Я часто использую TODO и FIXME, чтобы потом вернуться к месту на которое сейчас не является приоритетным. Проект настроен так, что не собирается если в коде где-то есть эти метки, что помогает не забыть о том, что хотел сделать и не сделал.
unclejocker
25.09.2018 13:48В MS VS комбинация кнопок ctrl + w,t открывает окно со списком всех TODO в проекте. В других IDE наверное тоже есть что то подобное.
moonster
25.09.2018 01:16Чтоб научиться писать хороший код — нужно много писать и читать код, периодически возвращаясь к доработке уже написанного. Без такой практики этот список не поможет, а вот качестве дополнения будет очень полезен.
ttmt
25.09.2018 10:37>>Обработка ошибок — это одна операция. Если в функции есть ключевое слово try, то после >>блоков catch/finally ничего другого в функции быть не должно.
Предполагает вынос блока try в отдельную функцию. Имеет ли смысл выносить в отдельную функцию если внутри блока одна строчкаDanik-ik
26.09.2018 18:50Автор утверждает, что имеет, насколько помню. Я с этим согласен. Есть метод, который работу работает. Есть тот, который ошибки ловит. И второй не разобран на части первым. И вообще это разные задачи, зачем их перемешивать?
Привыкнуть только никак не могу. Среда разработки и язык не располагают к хоть сколько нибудь быстрому и / или удобному рефакторингу. В Джаву хочу, на Идею… И выделять методы, выделять методы...
Xuxicheta
25.09.2018 12:57Идеальное количество входных аргументов для функции = 0. Если входных аргументов больше трех, то стоит задуматься каким образом лучше от них избавиться, например, создать класс для этих аргументов.
По мне так наоборот. Не стоит плодить классы с методами без аргументов, в случае нужды будет сложнее понять какие конкретно данные метод берет, на какие другие методы эти данные повлияют, и сложно будет метод переиспользовать.
Нужно стремиться к чистым функциям. Вот вход, вот выход, все сразу понятно из заголовка. Если забирает сразу много данных — сделай интерфейс.nlinker
25.09.2018 17:23+1
Идеальное — это 1. Что-то вошло — что-то вышло. Прямолинейный поток выполнения, композиция, лёгкость переиспользования. Когда у функции нет аргументов, это значит, что она берёт входную информацию из окружения, а значит имеет побочный эффект. Такую архитектуру нельзя называть «чистой».
Avitale
25.09.2018 13:48Вместо return null лучше использовать пустой объект — Collection.empty() или null-объект — EmptyObject().
Объясните, почему так? До настоящего момента приходилось встречать только вариант с null, чем он хуже?siziyman
25.09.2018 13:57Ну про EmptyObject неоднозначно, а вот пустая коллекция точно реже ведёт к ошибкам (притом не факт, что очевидным), чем null.
Neikist
25.09.2018 14:12Как по мне проще для понимания и использования. В какой то статье (или книге) приводился пример как НЕ надо возвращать значение в стиле:
1) Нет друзей — возвращаем null;
2) Есть один друг — возвращаем собственно объект;
3) Друзей больше одного — возвращаем коллекцию объектов.
Мало того что на null например можно попросту забыть проверить, или ожидать только null или коллекцию не учтя случай с одним объектом, так еще и разное поведение лепить в зависимости от типа возвращаемого значения.
Тогда как при пустой коллекции или коллекции с одним элементом нам бы не пришлось в клиентском коде городить отдельную логику.ookami_kb
25.09.2018 14:47+1Все так. Говоря более формальным языком, пустой список – это константа для монады "список".
khim
25.09.2018 15:45По хорошему к этому манифесту книга должна прилагаться. Ибо каждый пункт имеет области применимости — тупое следование этим правилам ни к чему хорошему не приведёт…
То есть каждое правило применимо, ну, скажем, в 99% случаев, но поскольку у нас тут чуть больше, чем полсотни правил, то в половине мест хотя бы одно из правил стоит нарушить…
Про это недавно отдельная статья была…
ta6aku
25.09.2018 15:49Я понимаю что ООП, и во всех учебниках написано, но чем автору насолили статические методы? Про состояния понятно, но с методами то в чем проблема?
poxvuibr
25.09.2018 16:05В том, что статические методы могут ссылаться на статические поля
В том, что в статический метод нельзя подставить свою реализацию для тестирования
cyberzx23
25.09.2018 16:19+1«Идеальное количество входных аргументов для функции = 0»
Очень спорное утверждение.tanchuev Автор
25.09.2018 18:22В книге очень много спорных утверждений. На самом деле стоит просто отдавать себе отчет почему код должен работать именно таким образом каким он написан. Стоит подумать как его можно улучшить. К тому же это всего лишь рекомендации, которые не обязательно соблюдать. Это не правила, а именно рекомендации)
sentyaev
25.09.2018 18:27Идеальный код — это код который не написан, в нем нет багов и его не нужно тестировать и поддерживать.
unabl4
25.09.2018 17:11Приблизительный максимум 20 строк и 150 символов в одной строке
150 символов — это явный перебор. Большенство редакторов ставят отсечку почти на половине от этой величины. И даже это многовато.
Идеальное количество входных аргументов для функции = 0
Если это foo/bar/baz для hello world — да, а так нет.
Названия методов должны содержать глагол, который описывает, что этот метод делает и ключевое слово с которым работает данный метод. Если в названии метода нет глагола, то эта сущность не должна быть методом или ему нужно дать правильное название.
Не обязательно. Это зависит от ЯП.
Не комментируй плохой код — перепиши его. Не стоит объяснять, что происходит в плохом коде, лучше сделать его явным и понятным.
Это не всегда зависит от пожеланий одного разработчика. Время на рефакторинг далеко не всегда удаётся выбить.
If, else, while и т.д. должны содержать вызов одной функции. Так будет читабельнее, понятнее и проще.
Вообще не обязательно. Очень сомнительное правило.
XopHeT
25.09.2018 17:13Не обязательно. Это зависит от ЯП.
Пример можно?
Я всего на нескольких ЯП пишу. Ни в одном из них нет ограничений на имя метода.
Если это foo/bar/baz для hello world — да, а так нет.
- start() — идеальный метод;
- start(speed) — отличный метод;
- start(speed, direction, path) — похуже. Сходу и не вспомнишь, в какой последовательности параметры передавать;
- start(speed, direction, path, maxBias, onError) — комментарии, думаю, излишни.
unabl4
25.09.2018 17:37Пример можно?
Я всего на нескольких ЯП пишу. Ни в одном из них нет ограничений на имя метода.
Ruby. Вполне характерно писать вообще без глаголов или глагольных префиксов.
И если чистые глаголы это ещё куда не шло (#call, #perform или что-то в этом роде), то глагол + ключевое слово — это скорее code smell, чем норма.
И это не ограничение, а соглашение.
start() — идеальный метод;
start(speed) — отличный метод;
start(speed, direction, path) — похуже. Сходу и не вспомнишь, в какой последовательности параметры передавать;
start(speed, direction, path, maxBias, onError) — комментарии, думаю, излишни.
Если аргументов больше ~3 (это эмпирически) — это уже code smell.
Разумно применять keyword-аргументы, если возникает путаница.
cyberzx23
25.09.2018 18:10+2Например возьмите любую функцию из C math.h. Там нет глаголов, только существительные: sqrt, sin. log, exp, round и
Если следовать правилам чистого кода, то надо было их назвать calc_sqrt, calc_sin, find_exp и т.д. Делает ли это код чище? Ничуть.XopHeT
26.09.2018 15:49Вы говорите о «общепринятых» названиях, которые
- применяются в т.ч. в математике
- приживались годами
и поэтому не вызывают дополнительных вопросов.
А вот на заре моей программистской деятельности SQR и SQRT было сложно запомнить.
Полная запись GetSqareRoot (вполне вероятно) вызывала бы меньше вопросов поскольку не является «набором несвязанных букв» (как я тогда воспринимал) а вполне осмысленным словосочетанием.Neikist
26.09.2018 15:58Да, помню свои первые попытки программировать. Особенно если учесть что английский я знал никак — почти все для меня было магическим сочетанием символов. Тогда это здорово притормозило.
XopHeT
25.09.2018 17:47Ruby. Вполне характерно писать вообще без глаголов или глагольных префиксов.
Я не сталкивался с Ruby.
Например, как геттеры от сеттеров отличаются?
И если чистые глаголы это ещё куда не шло (#call, #perform или что-то в этом роде), то глагол + ключевое слово — это скорее code smell, чем норма.
Из текущего:
Есть запрос (Request), который содержит список параметров (Parameter).
У Request есть метод для добавления параметра в список.
Назови я его add никто не поймет что именно добавляется.
Назови я его addParameter — даже не знакомому с архитектурой человеку будет понятно что именно добавляется в Request.
Будет ли имя addParameter «запахом»?FreeNickname
25.09.2018 21:46Я не сталкивался с Ruby.
Например, как геттеры от сеттеров отличаются?
Немного вклинюсь к вам просто разнообразия ради)
Objective-C
getter: `param`;
setter: `setParam:(value)` (синтаксис неточный)
Т.е. функция-геттер не имеет глагола.
unabl4
26.09.2018 01:45Например, как геттеры от сеттеров отличаются?
# это геттер def first_name @first_name end # это сеттер def first_name=(val) @first_name = val end
Вот и всё различие.
На самом деле таким макаром геттеры/сеттеры не пишут, только если надо переопределить их стандартное поведение.
Дефолтную пару можно сгенерировать с помощью одной строчки
attr_accessor :first_name
Будет ли имя addParameter «запахом»?
Скорее да, чем нет.
Из Вашего примера, скорее всего данный функционал будет имплементирован немного по-другому.
request.params[key]=value
params — это просто словарь, к которому можно обратиться напрямую.
"[]=(key,value)" — это тоже метод.
P.S. Проверил — так примерно в рельсах и сделано. Только там params — это экземпляр класса Parameters, который в свою очередь ведёт себя как словарь.
ianzag
Откуда стойкое обращение на Ты :-? Это вроде не men's health & K
tanchuev Автор
Это лишь способ повествования) Каждый, кто читает что-либо, никогда не обращается к себе на вы)