Устранение циклических зависимостей Clojure

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

  • Основная проблема заключается в том, что я получаю ошибку «Нет такой переменной: пространство имен / имя функции» в одном из файлов.
  • Я попытался «объявить» функцию, но потом она пожаловалась: «Не могу сослаться на квалифицированную переменную, которая не существует»
  • Я мог бы, конечно, реорганизовать всю кодовую базу, но это кажется непрактичным делать каждый раз, когда у вас есть зависимость, которую нужно разрешить ... и это может стать очень уродливым для определенных сетей с круговыми зависимостями
  • Я мог бы выделить кучу интерфейсов / протоколов / деклараций в отдельный файл и иметь все, что связано с этим .... но похоже, что это закончится беспорядком и испортит текущую красивую модульную структуру, которую у меня есть, с сгруппированными связанными функциями вместе

Есть предположения? Как лучше всего справиться с такой циклической зависимостью в Clojure?


person mikera    schedule 21.06.2010    source источник


Ответы (5)


Я помню ряд обсуждений пространств имен в Clojure - в списке рассылки и в других местах - и я должен сказать вам, что консенсус (и, AFAICT, текущая ориентация дизайна Clojure) заключается в том, что круговые зависимости - это призыв дизайна к рефакторинг. Временами могут быть возможны обходные пути, но они уродливы, возможно, проблематичны для производительности (если вы делаете вещи излишне «динамическими»), не гарантируете, что они будут работать вечно и т. Д.

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

Подводя итог, я отдаю свой голос рефакторингу.

person Michał Marczyk    schedule 21.06.2010
comment
Спасибо Михалу за понимание и полезную информацию! Я все еще не уверен, что всегда избегать циклических зависимостей - лучший вариант для структурирования проекта. Посмотрим на группу Clojure и посмотрим, сможет ли это убедить меня в обратном :-) - person mikera; 22.06.2010
comment
Небольшое обновление - размещение протоколов в их собственном пространстве имен сработало хорошо и решило большинство проблем, я обычно заканчиваю добавлением (: use [protocol]) к большинству других объявлений ns, и все просто работает. Единственное, что мне все еще кажется некрасивым для работы, - это когда вы объявляете класс (например, deftype), на который хотите сослаться, прежде чем он будет объявлен (например, как подсказка типа в определении протокола !!) - person mikera; 06.07.2010
comment
Спасибо за обновление, рад это слышать! Я думаю, что намекание на функции протокола / интерфейса с именами фактических реализующих классов может быть не очень хорошей идеей (на самом деле у меня сложилось впечатление, что методы протокола еще не могут быть намекны, но методы интерфейса могут, и аргументом является то же самое): вместо этого укажите имя интерфейса. Если вы имеете дело с классом, созданным deftype, все его методы в любом случае будут методами Object / interface / protocol. Единственный раз, когда я бы использовал подсказки, указывающие на классы, - это когда это необходимо для взаимодействия. - person Michał Marczyk; 07.07.2010
comment
Тем не менее, из любопытства, как можно обойтись без класса, когда он нужен для подсказки ...? - person Michał Marczyk; 07.07.2010

У меня была аналогичная проблема с некоторым графическим кодом, в итоге я сделал следующее:

(defn- frame [args]
  ((resolve 'project.gui/frame) args))

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

person Hamza Yerlikaya    schedule 21.06.2010
comment
Это очень быстро становится уродливым. Я бы предложил провести рефакторинг пространств имен, если это вообще возможно. - person celwell; 13.10.2015

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

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

Решение в этом случае и в большинстве случаев состоит в том, чтобы переместить код, не имеющий зависимостей, в отдельные (util) пространства имен, где к ним можно будет свободно делиться. Я еще не сталкивался со случаем, когда проблема не могла быть решена с помощью этой техники. Это делает поддержание полных, функциональных бизнес-объектов практически невозможным, но, похоже, это единственный вариант. Clojure предстоит пройти долгий путь, прежде чем он станет зрелым языком, способным точно моделировать реальный мир, и до тех пор нелогичное разделение кода - единственный способ устранить эти зависимости.

Если Aa () зависит от Ba (), а Bb () полагается на Ab (), единственное решение - переместить Ba () в Ca () и / или Ab () в Cb (), даже если C технически не существует в реальный мир.

person Ralph Ritoch    schedule 10.04.2014
comment
Тело и сердце не составлены и не созданы для того, чтобы их можно было составить. Пространства имен должны быть. Вы не получите возможности компоновки, просто моделируя реальный мир. - person Leon Grapenthin; 10.04.2014
comment
Пространства имен существуют с единственной целью - иметь возможность повторно использовать одни и те же имена в разных контекстах без конфликтов. Моделируя реальный мир, вы получаете интуитивно понятный и удобный в обслуживании дизайн. Я не собираюсь оспаривать составность сердец или тел, но есть много случаев, которые показывают, что они действительно могут быть составлены. - person Ralph Ritoch; 10.04.2014
comment
Если вы говорите о пространствах имен строго в смысле избежания конфликта имен, вы должны знать, что никаких ограничений зависимостей не накладывается. Вы можете создавать как символы в пространстве имен, так и ключевые слова. Зависимости идут с require. Существует порядок загрузки библиотек: LIB1 требует LIB2, поэтому LIB2 будет загружен как часть LIB1. Вы знаете, что происходит, когда LIB2 требует LIB1? - Конечно. Решением было бы проигнорировать это и просто подождать, чтобы увидеть, что происходит во время выполнения. Хики прокомментировал, почему он решил не ‹news.ycombinator.com/item?id=2467809 - person Leon Grapenthin; 10.04.2014
comment
lgrapenthin, я читал комментарии Хикки, и хотя он ясно заявляет о некоторых преимуществах шепелявого стиля, он не делает ничего, кроме как извиняться. Объявления для сторонних пространств имен (которые приводят к ошибкам, если они вызываются до определения) и более мягкие правила загрузки (например, soft-require, где говорится о необходимости функции, но не запускает загрузку файла) решают все проблемы, о которых плакал Хикки о. У Хики чистое отсутствие опыта. - person Ralph Ritoch; 10.04.2014
comment
Хики заявляет в ссылке, что вы должны учитывать компромисс между полезностью и сложностью. Вы это сделали? - person Leon Grapenthin; 10.04.2014
comment
lgrapenthin, я не думаю, что сложность настолько велика. Я, вероятно, мог бы сделать это сейчас в чистом закрытии, определив функции, которые просто генерируют ошибки и позволяя их переопределить в правильном файле, и комментируя обратные ссылки. Однако такой хак вряд ли будет компилироваться в java. Чтобы получить лучшее из обоих миров, необходимо некоторое количество препроцессорного управления. - person Ralph Ritoch; 10.04.2014
comment
Я бы случайно не стал сомневаться в мудрости Рича Хики. Разве диалекты Лиспа не предназначены для организации логики и данных в деревья? Вот что такое связанный список. Наличие циклической зависимости не укладывается в дерево - это граф. Чтобы разрешить циклическое соединение, вы должны добавить родительский узел с зависимостями под ним. Таким образом, если два пространства имен должны что-то совместно использовать, то, что совместно используется, должно находиться в его собственном пространстве имен. - person Tyler; 09.02.2017
comment
Тайлер, твое мнение очень политическое, но идея лиспа - это слияние данных и кода, оно обычно использовалось для ИИ и оптимизировано для возможности перекодирования самого себя. Как видите, это слабо переведено на Clojure, который не является LISP, поскольку не соответствует стандартам LISP. Clojure - это отдельный язык. - person Ralph Ritoch; 09.02.2017
comment
Рефакторинг - прекрасное предложение в Clojure. Однако я давно использовал неглубокие циклические зависимости в других языках. Я склонен сталкиваться с их желанием при использовании инфраструктуры компонентов, поскольку компонент часто используется для предоставления состояния во время запуска, но компонент полагается на пространство имен, которое также использует это состояние. Разделение пространства имен здесь - простое решение. - person ctpenrose; 04.08.2017
comment
Я не уверен, почему люди так быстро судят о вас как о некомпетентном кодировщике, потому что у вас есть циклическая зависимость, которую нелегко разрешить. На самом деле повторный фактор часто может решить проблему в 90% случаев, но из каждого правила есть исключения. Я уверен, что где-то есть несколько реальных примеров этого. - person Joel M; 30.05.2020

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

person stopthe    schedule 08.12.2011

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

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; example/a.cljc

(ns example.a
  (:require [example.b :as b]))

(defn foo []
  (println "foo"))

#?(

   :clj
   (alter-var-root #'b/foo (constantly foo))                ; <- in clojure do this

   :cljs
   (set! b/foo foo)                                         ; <- in clojurescript do this

   )

(defn barfoo []
  (b/bar)
  (foo))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; example/b.cljc

(ns example.b)

;; Avoid circular dependency.  This gets set by example.a
(defonce foo nil)

(defn bar []
  (println "bar"))

(defn foobar []
  (foo)
  (bar))

Я научился этому трюку из кода Дэна Холмсанда в Reagent.

person bonkydog    schedule 10.09.2017