Подтвердить что ты не робот

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

Я давно разработчик 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(), который открывает сеанс db. Таким образом, приложение в пакете приложений вызывает database.connect(), и открывается сеанс db.

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

  • Как правило, разбить пакет, когда каждый кусок может быть полезным сам по себе. Если две части функциональности действительно тесно связаны, вам вообще не нужно разбивать их на пакеты; вы можете организовать несколько файлов или типов вместо этого. Большие пакеты могут быть в порядке; Go 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-сервера или что-то подобное, но когда-нибудь вы этого не сделаете.

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

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

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

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

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

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

Ответ 2

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

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

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