Циклические зависимости и интерфейсы в Голанге

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

Создавая такую ​​же структуру в Go, я, похоже, сталкиваюсь с ошибками циклического импорта, гораздо больше, чем я хочу. Никогда не было проблем с импортом в Python. Мне никогда даже не приходилось использовать псевдонимы импорта. Таким образом, у меня мог быть некоторый циклический импорт, который не был очевиден в питоне. Я действительно нахожу это странным.

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

Текущая структура приложения Python выглядит следующим образом:

/main.py

/settings/routes.py      contains main routes depends on app1/routes.py, app2/routes.py etc
/settings/database.py    function like connect() which opens db session
/settings/constants.py   general constants

/apps/app1/views.py      url handler functions
/apps/app1/models.py     app specific database functions depends on settings/database.py
/apps/app1/routes.py     app specific routes

/apps/app2/views.py      url handler functions
/apps/app2/models.py     app specific database functions depends on settings/database.py
/apps/app2/routes.py     app specific routes

settings/database.py имеет общие функции, такие как connect() который открывает сессию БД. Итак, приложение в пакете приложений вызывает database.connect() и сеанс БД открыт.

То же самое в случае с settings/routes.py у него есть функции, которые позволяют приложениям добавлять свои под-маршруты к основному объекту маршрута.

Пакет настроек больше касается функций, чем данных / констант. Он содержит код, который используется приложениями в пакете приложений, которые в противном случае пришлось бы дублировать во всех приложениях. Так что, если мне нужно изменить класс маршрутизатора, например, мне просто нужно изменить settings/router.py и приложения будут продолжать работать без изменений.

4 ответа

Решение

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

При разработке API-интерфейсов, позволяющих избежать необходимости импорта:

  • Напишите функции конфигурации для подключения пакетов друг к другу во время выполнения, а не во время компиляции. Вместо routes импортируя все пакеты, которые определяют маршруты, он может экспортировать routes.Register, который main (или код в каждом приложении) можно позвонить. В общем, информация о конфигурации, вероятно, вытекает из main или выделенный пакет; Вы не хотите, чтобы это разбросано по всему вашему приложению.

  • Обойти основные типы и interface ценности. Если вы полагаетесь на пакет только для имени типа, возможно, вы можете избежать этого. Может быть, некоторый код, обрабатывающий []Page можно вместо этого использовать []string имен файлов или []int идентификаторов или какой-то более общий интерфейс (sql.Rows) вместо

  • Подумайте о наличии пакетов "схемы" с чистыми типами данных и интерфейсами, поэтому User Отдельно от кода, который может загружать пользователей из базы данных. Он не должен сильно зависеть (может быть, от чего-либо), поэтому вы можете включить его из любого места. Бен Джонсон выступил с молниеносной речью на GopherCon 2016, предлагая это и упорядочивая пакеты по зависимостям.

Об организации кода в пакеты:

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

  • Разбить грейф-пакеты ( utils , tools ) по теме или зависимости. В противном случае вы можете импортировать огромный utils пакет (и принимая на себя все его зависимости) для одной или двух частей функциональности (у которых не было бы так много зависимостей, если бы они были выделены).

  • Подумайте о том, чтобы вставить повторно используемый код "вниз" в пакеты более низкого уровня, не связанные с вашим конкретным вариантом использования. Если у тебя есть package page содержащий как логику для вашей системы управления контентом, так и универсальный код HTML-манипуляции, рассмотрите возможность перемещения HTML-материала "вниз" к package html так что вы можете использовать его, не импортируя несвязанные материалы по управлению контентом.


Здесь я бы изменил порядок вещей, чтобы маршрутизатор не включал маршруты: вместо этого каждый пакет приложения вызывает router.Register() метод. Это то, что веб-инструментарий Gorilla mux пакет делает. Ваш routes, database, а также constants пакеты выглядят как фрагменты низкого уровня, которые должны импортироваться кодом вашего приложения, а не импортировать его.

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

  • Пакеты хороши для отделения независимо используемых битов функциональности с точки зрения вызывающего. Для внутренней организации кода вы можете легко перетасовывать код между исходными файлами в пакете. Начальное пространство имен для символов, которые вы определяете в x/foo.go или же x/bar.go это просто пакет x и это не так сложно разделить / объединить файлы по мере необходимости, особенно с помощью такой утилиты, как goimports,

    Стандартная библиотека net/http составляет около 7 тыс. строк (считая комментарии / пробелы, но не тесты). Внутренне, это разделено на много меньших файлов и типов. Но это один пакет, я думаю, потому что у пользователей не было причин, скажем, просто обрабатывать файлы cookie самостоятельно. С другой стороны, net а также net/url отделены, потому что они используют вне HTTP.

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

  • Настройте API для настройки поведения во время выполнения, чтобы вам не пришлось импортировать его во время компиляции. Так, например, ваш URL-маршрутизатор может выставить Register метод вместо импорта appA, appB и т. д. и читая var Routes с каждого. Вы могли бы сделать myapp/routes пакет, который импортирует router и все ваши взгляды и звонки router.Register, Основная идея заключается в том, что маршрутизатор является универсальным кодом, который не должен импортировать представления вашего приложения.

    Несколько способов собрать API конфигурации:

    • Пропустить поведение приложения через interface с или func s: http могут быть переданы пользовательские реализации Handler (конечно) но также CookieJar или же File, text/template а также html/template может принимать функции, которые будут доступны из шаблонов (в FuncMap).

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

    • Если вам нужно, просто приклейте это на пленку: вам не нужно ограничивать себя сверх элегантными конфигурационными системами, если вы не можете приспособить их к своему приложению: возможно, для некоторых вещей package "myapp/conf" с глобальным var Conf map[string]interface{} Полезно. Но следует помнить о недостатках глобальной конф. Если вы хотите написать повторно используемые библиотеки, они не могут импортировать myapp/conf; им нужно принимать всю информацию, которая им нужна в конструкторах и т. д. Глобальные переменные также рискуют получить жесткую схему, предполагая, что что-то всегда будет иметь одно значение для всего приложения, когда это в конечном итоге не будет; может быть, сегодня у вас есть одна конфигурация базы данных или конфигурация HTTP-сервера или что-то подобное, но однажды у вас ее нет.

Некоторые более конкретные способы перемещения кода или изменения определений для уменьшения проблем зависимости:

  • Отделите фундаментальные задачи от приложений-зависимых. В одном приложении, над которым я работаю на другом языке, есть модуль "Утилиты", который смешивает общие задачи (например, форматирование даты-времени или работа с HTML) со специфическими для приложения материалами (которые зависят от схемы пользователя и т. Д.). Но пользовательский пакет импортирует утилиты, создавая цикл. Если бы я портировал на Go, я бы переместил зависящие от пользователя утилиты "вверх" из модуля утилит, возможно, чтобы жить с пользовательским кодом или даже над ним.

  • Подумайте о том, чтобы разбить пакеты с захватом Немного расшифровав последний пункт: если две части функциональности независимы (то есть все работает, даже если вы перемещаете некоторый код в другой пакет) и не связаны с точки зрения пользователя, они являются кандидатами на разделение на два пакета. Иногда комплектация безвредна, но в других случаях она приводит к дополнительным зависимостям, или менее универсальное имя пакета просто сделает более понятный код. Так что мой utils Выше может быть разбито по теме или зависимости (например, strutil, dbutil, так далее.). Если вы получите множество пакетов таким образом, у нас есть goimports чтобы помочь им управлять.

  • Замените требующие импорта типы объектов в API базовыми типами и interface s. Скажем, две сущности в вашем приложении имеют отношение многие ко многим, например: User с и Group s. Если они живут в разных пакетах (большое "если"), вы не можете иметь оба u.Groups() возвращая []group.Group а также g.Users() возврате []user.User потому что это требует, чтобы пакеты импортировали друг друга.

    Тем не менее, вы можете изменить один или оба из них, скажем, []uint идентификаторов или sql.Rows или какой-то другой interface можно обойтись без import с конкретным типом объекта. В зависимости от вашего варианта использования, такие как User а также Group может быть настолько тесно связаны, что лучше просто положить их в один пакет, но если вы решите, что они должны быть разными, это способ.

Спасибо за подробный вопрос и продолжение.

Более короткий ответ на ваш вопрос (с использованием интерфейса), который не отменяет правильности и полноты других ответов, - это пример:

UserServiceвызывает циклический импорт, откуда его не следует вызыватьAuthorizationService. Это просто для того, чтобы иметь возможность извлекать данные пользователя, поэтому мы можем объявить только желаемую функциональность в отдельном интерфейсе на стороне получателя.UserProvider:

https://github.com/tzvatot/cycloimport-solving-exaple/commit/bc60d7cfcbd4c3b6540bdb4117ab95c3f2987389

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

Возможный частичный, но некрасивый ответ: год боролся с проблемой циклической зависимости импорта. Какое-то время удавалось достаточно развязать, чтобы не было цикла импорта. Мое приложение активно использует плагины. При этом используются библиотеки кодирования / декодирования (json и gob). Для них у меня есть собственные методы marshall и unmarshall, а также их эквиваленты для json.
Чтобы они работали, полное имя типа, включая имя пакета, должно быть идентично в структурах данных, которые передаются кодекам. Создание кодеков должно происходить в пакете. Этот пакет вызывается как из других пакетов, так и из плагинов. Все работает до тех пор, пока пакету кодека не нужно вызывать какой-либо пакет, вызывающий его, или использовать методы или интерфейсы для методов. Чтобы иметь возможность использовать типы из пакета в надстройках, надстройки должны быть скомпилированы с пакетом. Поскольку я не хочу включать основную программу в сборки для подключаемых модулей, что нарушило бы смысл подключаемых модулей, только пакет кодеков включен как в подключаемые модули, так и в основную программу. Все работает до тех пор, пока мне не понадобится позвонить из пакета кодеков в основную программу,после того, как основная программа вызвала пакет кодеков. Это вызовет цикл импорта. Чтобы избавиться от этого, я могу поместить кодек в основную программу вместо его собственного пакета. Но поскольку конкретные типы данных, используемые в методах маршалинга / демаршалинга, должны быть одинаковыми в основной программе и в плагинах, мне нужно будет скомпилировать с основным программным пакетом для каждого из плагинов. Кроме того, поскольку мне нужно, чтобы основная программа вызывала плагины, мне нужны типы интерфейса для плагинов в основной программе. Так и не найдя способа заставить это работать, я подумал о возможном решении: во-первых, отделите кодек от плагина, а не просто пакета. Затем загрузите его как первый плагин из основной программы. Создайте функцию регистрации для обмена интерфейсами с базовыми методами.Все кодировщики и декодеры создаются вызовами этого плагина. Плагин обращается к основной программе через зарегистрированный интерфейс. Основная программа и все плагины используют для этого один и тот же пакет типа интерфейса. Однако типы данных для фактических закодированных данных упоминаются в основной программе с другим именем, но с тем же базовым типом, что и в подключаемых модулях, в противном случае существует тот же цикл импорта. для выполнения этой части требуется небезопасный гипс. Написал небольшую функцию, которая выполняет принудительное приведение, чтобы синтаксис был чистым: (<тип указателя приведения *> Cast (<указатель на структуру или интерфейс для указателя на структуру>).типы данных для фактических закодированных данных указаны в основной программе под другим именем, но с тем же базовым типом, что и в подключаемых модулях, в противном случае существует тот же цикл импорта. для выполнения этой части требуется небезопасный гипс. Написал небольшую функцию, которая выполняет принудительное приведение, чтобы синтаксис был чистым: (<тип указателя приведения *> Cast (<указатель на структуру или интерфейс для указателя на структуру>).типы данных для фактических закодированных данных указаны в основной программе под другим именем, но с тем же базовым типом, что и в подключаемых модулях, в противном случае существует тот же цикл импорта. для выполнения этой части требуется небезопасное приведение. Написал небольшую функцию, которая выполняет принудительное приведение, чтобы синтаксис был чистым: (<тип указателя приведения *> Cast (<указатель на структуру или интерфейс для указателя на структуру>).

Единственная другая проблема для кодеков - убедиться, что при отправке данных в кодировщик они приводятся так, чтобы методы marshall/unmarshall распознавали имена типов данных. Чтобы упростить задачу, можно импортировать как основные типы программ из одного пакета, так и типы плагинов из другого пакета, поскольку они не ссылаются друг на друга.

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

По сути, ваш код тесно связан, и Golang заставляет вас поддерживать низкую связь между пакетами, но с высокой степенью согласованности это нормально.

Golang намного превосходит Python по управлению пакетами. В python вы даже можете динамически импортировать пакеты.

Для больших проектов golang обеспечит поддержку ваших пакетов.

Другие вопросы по тегам