Я пытался найти альтернативы использованию глобальной переменной в каком-то устаревшем коде. Но этот вопрос не о технических альтернативах, я в основном обеспокоен терминологией .
Очевидное решение - передать параметр в функцию вместо использования глобального. В этой унаследованной кодовой базе это означало бы, что я должен изменить все функции в длинной цепочке вызовов между точкой, в которой будет в конечном итоге использоваться значение, и функцией, которая сначала получает параметр.
higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)
где newParam
в моем примере ранее была глобальная переменная, но вместо этого это могло быть ранее жестко закодированное значение. Дело в том, что теперь значение newParam получено в higherlevel()
и должно «путешествовать» вплоть до level3()
.
Мне было интересно, есть ли имя (имена) для такого рода ситуации / паттерна, где вам нужно добавить параметр во многие функции, которые просто «передают» значение без изменений.
Надеюсь, использование правильной терминологии позволит мне найти больше ресурсов о решениях для редизайна и описать эту ситуацию коллегам.
Ответы:
Сами данные называются «данные бродяги» . Это «запах кода», указывающий на то, что один фрагмент кода взаимодействует с другим фрагментом кода на расстоянии через посредников.
Рефакторинг для удаления глобальных переменных труден, и бродячие данные являются одним из способов сделать это, и часто самым дешевым способом. У него есть свои расходы.
источник
Я не думаю, что это само по себе является анти-паттерном. Я думаю, проблема в том, что вы думаете о функциях как о цепочке, тогда как на самом деле вы должны думать о каждой из них как о независимом черном ящике ( ПРИМЕЧАНИЕ : рекурсивные методы являются заметным исключением из этого совета).
Например, скажем, мне нужно вычислить количество дней между двумя календарными датами, поэтому я создаю функцию:
Для этого я создаю новую функцию:
Тогда моя первая функция становится просто:
В этом нет ничего против паттерна. Параметры метода daysBetween передаются другому методу, и на него никогда не ссылаются в методе, но они все еще необходимы для того, чтобы этот метод делал то, что ему нужно.
Я бы порекомендовал посмотреть на каждую функцию и начать с пары вопросов:
Если вы смотрите на беспорядочный код без единой цели, связанной с методом, вам следует начать с этого. Это может быть утомительно. Начните с самых простых вещей, чтобы вытащить и перейти в отдельный метод и повторять, пока у вас не получится что-то связное.
Если у вас просто слишком много параметров, рассмотрите рефакторинг Method to Object .
источник
BobDalgleish уже отметил, что этот (анти) паттерн называется « бродяга данных ».
По моему опыту, наиболее частой причиной избыточных данных является наличие множества связанных переменных состояния, которые действительно следует инкапсулировать в объект или структуру данных. Иногда может даже потребоваться вложить несколько объектов для правильной организации данных.
Для простого примера рассмотрим игру , которая имеет настраиваемый плеер характер, со свойствами , как
playerName
,playerEyeColor
и так далее. Конечно, у игрока также есть физическая позиция на игровой карте и различные другие свойства, такие как, скажем, текущий и максимальный уровень здоровья и так далее.На первой итерации такой игры вполне может быть разумным сделать все эти свойства глобальными переменными - в конце концов, есть только один игрок, и почти все в игре так или иначе связано с игроком. Таким образом, ваше глобальное состояние может содержать переменные, такие как:
Но в какой-то момент вы можете обнаружить, что вам нужно изменить этот дизайн, возможно, потому что вы хотите добавить многопользовательский режим в игру. В качестве первой попытки вы можете попытаться сделать все эти переменные локальными и передать их функциям, которые в них нуждаются. Однако вы можете обнаружить, что конкретное действие в вашей игре может включать цепочку вызовов функций, например:
... и
interactWithShopkeeper()
функция заставляет лавочника обращаться к игроку по имени, так что теперь вам внезапно необходимо передать все эти функции вplayerName
виде бродягих данных . И, конечно же, если владелец магазина считает, что голубоглазые игроки наивны и будут взимать с них более высокие цены, вам придется пройти через всю цепочку функций и так далее.playerEyeColor
В данном случае правильным решением, конечно, является определение объекта игрока, который включает в себя имя, цвет глаз, положение, здоровье и любые другие свойства персонажа игрока. Таким образом, вам нужно только передать этот единственный объект всем функциям, которые так или иначе связаны с игроком.
Кроме того, некоторые из вышеперечисленных функций могут быть естественным образом превращены в методы этого объекта игрока, что автоматически даст им доступ к свойствам игрока. В некотором смысле, это просто синтаксический сахар, поскольку вызов метода объекта эффективно передает экземпляр объекта как скрытый параметр в метод, так или иначе, но это делает код более ясным и более естественным при правильном использовании.
Конечно, типичная игра имеет гораздо более «глобальное» состояние, чем просто игрок; например, у вас почти наверняка будет какая-то карта, на которой происходит игра, и список неигровых персонажей, перемещающихся по карте, и, возможно, предметы, размещенные на ней, и так далее. Вы также можете передавать все это как объекты-бродяги, но это опять-таки загромождает аргументы вашего метода.
Вместо этого решение состоит в том, чтобы объекты хранили ссылки на любые другие объекты, с которыми они имеют постоянные или временные отношения. Так, например, объект игрока (и, возможно, любые объекты NPC тоже), вероятно, должен хранить ссылку на объект «игровой мир», который будет иметь ссылку на текущий уровень / карту, так что методу, подобному методу
player.moveTo(x, y)
, не нужно явно указывать карту в качестве параметра.Точно так же, если бы наш персонаж игрока имел, скажем, собаку, которая следовала за ним, мы естественным образом сгруппировали бы все переменные состояния, описывающие собаку, в один объект, и дали бы объекту игрока ссылку на собаку (так, чтобы игрок мог скажем, назовите собаку по имени) и наоборот (чтобы собака знала, где находится игрок). И, конечно, мы, вероятно, хотели бы сделать объекты player и dog подклассами более общего объекта «актера», чтобы мы могли повторно использовать один и тот же код, скажем, для перемещения по карте.
Ps. Несмотря на то, что я использовал игру в качестве примера, есть и другие виды программ, в которых возникают такие проблемы. Однако, по моему опыту, основная проблема всегда одна и та же: у вас есть куча отдельных переменных (локальных или глобальных), которые действительно хотят объединить в один или несколько взаимосвязанных объектов. Независимо от того, «внедряют» ли ваши данные в ваши функции «глобальные» настройки параметров или кэшированные запросы к базе данных или векторы состояния в численном моделировании, решение неизменно состоит в том, чтобы идентифицировать естественный контекст , к которому относятся данные, и преобразовать его в объект. (или что-либо, что является самым близким эквивалентом на выбранном вами языке).
источник
foo.method(bar, baz)
иmethod(foo, bar, baz)
есть другие причины (в том числе полиморфизм, инкапсуляция, локальность и т. Д.), Чтобы предпочесть первое.Я не знаю конкретного имени для этого, но я думаю, что стоит упомянуть, что проблема, которую вы описываете, это просто проблема поиска лучшего компромисса для области действия такого параметра:
как глобальная переменная, область действия слишком велика, когда программа достигает определенного размера
в качестве чисто локального параметра область действия может быть слишком маленькой, когда это приводит к множеству повторяющихся списков параметров в цепочках вызовов
так что в качестве компромисса вы часто можете сделать такой параметр переменной-членом в одном или нескольких классах, и это то, что я бы назвал правильным дизайном класса .
источник
Я полагаю, что шаблон, который вы описываете, является именно инъекцией зависимости . Некоторые комментаторы утверждают, что это шаблон , а не анти-шаблон , и я склонен согласиться.
Я также согласен с ответом @ JimmyJames, в котором он утверждает, что хорошая практика программирования - рассматривать каждую функцию как черный ящик, который принимает все свои входные данные как явные параметры. То есть, если вы пишете функцию, которая делает бутерброд с арахисовым маслом и желе, вы можете написать это как
но было бы лучше применить инъекцию зависимостей и написать так:
Теперь у вас есть функция, которая четко документирует все свои зависимости в сигнатуре функции, что очень удобно для удобства чтения. В конце концов, это правда, что для
make_sandwich
вас нужен доступ кRefrigerator
; таким образом, старая сигнатура функции была в основном неискренней, поскольку не принимала холодильник за входные данные.В качестве бонуса, если вы правильно делаете иерархию классов, избегаете нарезки и т. Д., Вы можете даже выполнить модульное тестирование
make_sandwich
функции, передаваяMockRefrigerator
! (Вам может понадобиться выполнить юнит-тестирование таким образом, потому что ваша среда юнит-тестирования может не иметь доступа ни к одному из нихPhysicalRefrigerator
.)Я понимаю, что не все способы использования зависимостей требуют подключения параметров с одинаковыми именами на несколько уровней вниз по стеку вызовов, поэтому я не отвечаю точно на вопрос, который вы задали ... но если вы ищете дополнительную информацию по этому вопросу, «Внедрение зависимости», безусловно, является подходящим ключевым словом для вас.
источник
Refrigerator
вIngredientSource
или даже обобщить понятие «сэндвич» вtemplate<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&)
; это называется «универсальное программирование», и оно достаточно мощное, но, безусловно, оно гораздо более загадочно, чем ОП действительно хочет попасть прямо сейчас. Не стесняйтесь, чтобы открыть новый вопрос о надлежащем уровне абстракции для сэндвич-программ. ;)make_sandwich()
.Это в значительной степени определение связывания в учебнике: один модуль имеет зависимость, которая глубоко влияет на другой, и это создает волновой эффект при изменении. Другие комментарии и ответы верны, так как это улучшение по сравнению с глобальным, потому что теперь связывание стало более явным и легче для программиста, чем для подрывной деятельности. Это не значит, что это не должно быть исправлено. Вы должны иметь возможность рефакторинга, чтобы удалить или уменьшить муфту, хотя, если она была там некоторое время, это может быть болезненным.
источник
level3()
нужноnewParam
, это наверняка, но каким-то образом разные части кода должны взаимодействовать друг с другом. Я не обязательно назвал бы параметр функции плохой связью, если эта функция использует параметр. Я думаю, что проблемным аспектом цепочки является дополнительная муфта, введенная дляlevel1()
иlevel2()
не имеющая смысла,newParam
кроме как для передачи ее. Хороший ответ, +1 за сцепление.Хотя этот ответ не дает прямого ответа на ваш вопрос, я чувствую, что было бы упущением пропустить его, не упоминая, как его улучшить (поскольку, как вы говорите, это может быть анти-паттерном). Я надеюсь, что вы и другие читатели сможете извлечь пользу из этого дополнительного комментария о том, как избежать «бродячих данных» (как Боб Далглиш так услужливо назвал их для нас).
Я согласен с ответами, которые предлагают сделать что-то большее ОО, чтобы избежать этой проблемы. Тем не менее, другой способ также помочь значительно сократить эту передачу аргументов, не переходя к « просто передать класс, в котором вы передавали много аргументов! », - это рефакторинг, чтобы некоторые этапы вашего процесса происходили на более высоком уровне, а не на более низком. один. Например, вот некоторые перед кодом:
Обратите внимание, что это становится еще хуже, чем больше вещей, которые нужно сделать в
ReportStuff
. Возможно, вам придется передать экземпляр Reporter, который вы хотите использовать. И все виды зависимостей, которые должны быть переданы, функционируют как вложенные функции.Я предлагаю перенести все это на более высокий уровень, где знание шагов требует жизни в одном методе, а не распределяться по цепочке вызовов методов. Конечно, в реальном коде это будет сложнее, но это дает вам представление:
Обратите внимание, что большая разница заключается в том, что вам не нужно передавать зависимости через длинную цепочку. Даже если вы сглаживаете не только один уровень, а несколько уровней, если эти уровни также достигают некоторого «сглаживания», так что процесс рассматривается как последовательность шагов на этом уровне, вы добились улучшения.
Хотя это все еще процедурно и еще ничего не превращено в объект, это хороший шаг к решению, какой инкапсуляции вы можете достичь, превратив что-то в класс. Вызовы методов с глубокими связями в сценарии before скрывают детали того, что происходит на самом деле, и могут сильно затруднить понимание кода. В то время как вы можете переусердствовать с этим и в конечном итоге сообщить высокоуровневому коду о вещах, которые он не должен, или создать метод, который делает слишком много вещей, нарушая, таким образом, принцип единственной ответственности, в целом я обнаружил, что сглаживание вещей немного помогает в ясности и внесении постепенных изменений в сторону лучшего кода.
Обратите внимание, что пока вы делаете все это, вы должны рассмотреть возможность тестирования. Прикованные вызовов методы на самом деле сделать модульное тестирование сложнее , потому что вы не имеете хорошую точку входа и точку выхода в сборке среза , который вы хотите проверить. Обратите внимание, что с этим выравниванием, так как ваши методы больше не принимают так много зависимостей, их проще тестировать, не требуя столько же имитаций!
Недавно я попытался добавить модульные тесты в класс (который я не писал), который занимал что-то вроде 17 зависимостей, и все они должны были быть подвергнуты насмешке! Я еще не все понял, но я разделил класс на три класса, каждый из которых имел дело с одним из существительных, к которым он обращался, и получил список зависимостей до 12 для худшего и около 8 для лучший.
Тестируемость заставит вас писать лучший код. Вы должны писать модульные тесты, потому что вы обнаружите, что это заставляет вас думать о вашем коде по-другому, и вы будете писать лучший код с самого начала, независимо от того, как мало ошибок вы могли иметь до написания модульных тестов.
источник
Вы буквально не нарушаете Закон Деметры, но ваша проблема в чем-то похожа на эту. Поскольку цель вашего вопроса - найти ресурсы, я предлагаю вам прочитать о Законе Деметры и посмотреть, насколько этот совет применим к вашей ситуации.
источник
Существуют случаи, когда лучше всего (с точки зрения эффективности, удобства обслуживания и простоты реализации) иметь определенные переменные как глобальные, а не накладные расходы на постоянную передачу всего (например, у вас есть около 15 переменных, которые должны сохраняться). Поэтому имеет смысл найти язык программирования, который лучше поддерживает область видимости (как частные статические переменные C ++), чтобы смягчить потенциальный беспорядок (пространства имен и несанкционированного доступа). Конечно, это просто общеизвестно.
Но подход, изложенный в OP, очень полезен, если кто-то занимается функциональным программированием.
источник
Здесь вообще нет анти-паттерна, потому что вызывающий абонент не знает обо всех этих уровнях ниже и ему все равно.
Кто-то вызывает более высокий уровень (params) и ожидает, что более высокий уровень выполнит свою работу. То, что более высокий уровень делает с params, не относится к числу вызывающих. Более высокий уровень решает проблему наилучшим образом, в этом случае путем передачи параметров на уровень 1 (параметры). Это абсолютно нормально.
Вы видите цепочку вызовов - но цепочки вызовов нет. Вверху есть функция, которая делает свою работу наилучшим образом. И есть другие функции. Каждая функция может быть заменена в любое время.
источник