«Почему мы объявляем более одного« класса »в одном файле .go?»

«Это очень интересный вопрос», - подумал я. Пока я пытался найти ответ на этот очень интригующий вопрос, в моей голове возникла фраза - Пакеты Go.

Я использую Go около четырех лет. В этот раз я использовал его для разных целей - для создания скриптов, HTTP-приложений и многих других. Это не самый простой язык для освоения по сравнению с другими языками, такими как JavaScript и Ruby (и да, мне тоже нравятся оба этих языка), но вы можете делать действительно отличные / потрясающие / сумасшедшие вещи в Go. Один из примеров, образ докера ~ 5 МБ для запуска сервера HTTP API, при этом на таких языках, как Ruby, образ докера может стать действительно огромным (например, 500 МБ и выше), если установлено много зависимостей.

Тем не менее, у меня были сомнения по поводу некоторых стандартов структурирования кода языка. Я с энтузиазмом обсуждал с коллегами именование переменных для аббревиатур - это отличалось от нормы для всех других языков - например, simpleHTTPServerID вместо simpleHttpServerId. Тем не менее, преимущества полного использования аббревиатур все еще были в некотором смысле разумными.

Вернемся к сегодняшнему дню и времени, к моему удивлению, когда мне задали тот же вопрос в заголовке. Чтобы уточнить, мне задали этот вопрос кто-то, кто был новичком в Go, и что сравнивалось здесь было объявление пользовательского типа в Go, в отличие от классов для таких языков, как Ruby.

Учитывая, что конструкции с обеих сторон похожи, можно было бы связать настраиваемые типы в Go с классами на других языках. Было логично, что структуры кода для одного класса для каждого файла на других языках по праву должны применяться и к Go.

За исключением того, что этого не произошло.

По соглашению, особенно при рассмотрении собственных библиотек Go и репозиториев с открытым исходным кодом, наличие нескольких типов, определенных в одном файле «.go», на самом деле считалось «нормальным».

Так что же дает? Я решил погрузиться глубже, чтобы узнать.

Изучение других языков

Прежде чем мы поговорим о Go и о причинах этого вопроса, я подумал, что было бы неплохо сначала изучить другие языки и пересмотреть наше понимание здесь: Зачем разделять классы по разным файлам?

Джава

Первая подсказка, которую я нашел для Java, пришла из документа об организации файлов от Oracle:

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

Пока что документация предполагает наличие некоторых ограничений для одного общедоступного класса Java для каждого файла. Тем не менее, есть способы обойти это:

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

Рубин

Для Ruby нет подробного объяснения ограничений в этом вопросе. Во всяком случае, нет никаких ограничений для объявления нескольких классов для каждого файла .rb.

Однако, если вы используете популярный фреймворк Ruby, Ruby on Rails, вначале вы получите другое сообщение - разделение и присвоение имен вашим файлам Ruby приветствуется для получения преимуществ автозагрузки при использовании фреймворка.

Хотя vanilla Ruby не имеет никаких ограничений для классов, подобных Java, руководство по стилю также предлагает те же рекомендации, что и в документации Rails выше.

Следовательно, в отличие от Java, использование нескольких внешних классов в файле Ruby не ограничено. Однако использование одного класса для каждого файла лучше всего подходит для фреймворков, а также рекомендуется.

Общий

Помимо Java и Ruby, я также кратко рассмотрел JavaScript (и TypeScript). В нем было то же предложение, что и в Ruby, как лучшая практика из настраиваемых правил eslint, стандарта, который вносится и согласовывается сообществом в целом.

Приведенные выше наблюдения для разных языков в основном взяты из документации, которую можно найти в Интернете, но как насчет сообщества разработчиков?

Просмотр страниц и страниц StackOverflow и Reddit по этой теме начал давать мне более ясную картину. Хотя такие языки, как Java, могут вмешаться и обеспечить соблюдение этой практики, рекомендуемая практика использования одного класса для каждого файла для любого языка является интуицией, поддерживаемой большей частью сообщества. И это восходит к одному основному принципу, который мы усвоили в школе: «Высокая сплоченность, слабая связь».

В общем, вот что это означает в контексте классов на этих языках программирования:

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

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

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

Иди сюда и свои условности?

Мы изучили разные языки программирования и поняли, почему существует врожденная интуиция для разделения классов на разные файлы. А что насчет Go?

Во-первых, мы должны подумать, эквивалентны ли пользовательские типы в Go «классам» в других языках? Давайте посмотрим на более раннее изображение:

Есть некоторое сходство между типом Page в Go и классом Page в Ruby, в основном потому, что они имеют схожий набор методов получения и установки.

Однако если мы расширим это, чтобы включить традиционные атрибуты «класса», такие как наследование, вы очень быстро обнаружите, что в Go нет такой вещи, как наследование. Ближайшим решением в Go является встраивание типов в другие типы (иногда даже в несколько встроенных типов одновременно), что является составом, а не наследованием.

Итак ... нет занятий по го? Но, конечно же, было бы полезно разделить пользовательские типы и методы Go в их собственные файлы?

Ответ здесь ... Пойдите пакеты

Пакеты Go

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

Как оказалось, в Go мы объявляем и импортируем целые наборы файлов «.go» через эти «пакеты». Взгляните на образец изображения проекта, который у меня ниже:

Все эти зеленые прямоугольники по сути являются каталогами, которые ссылаются на их собственный пакет. Например, каталог core представляет собой основной пакет, который состоит из всего кода из его четырех файлов, и я бы посмотрел на импорт пакета с repository.com/core вместо repository.com/core/utils.go.

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

  • Пакет должен отражать единую концепцию / функциональность (высокая степень связанности, низкая степень связи)
  • Пакет может содержать несколько типов, если они представляют части концепции или используются внутри концепции. Типы, которые являются частными конструкциями пакета, должны оставаться неэкспортируемыми, если в этом нет необходимости. Экспортируемые типы и методы представляют пакет как различные части его интерфейса.

Действительно отличный пример - посмотреть на собственные библиотеки Go, чтобы понять степень сплоченности и сцепления пакетов - например, SQL.

В приведенном выше примере из библиотеки SQL sql.go, Conn и TxOptions существуют в одном пакете и файле для выполнения транзакции в соединении с базой данных. Контекст может быть достаточно коротким, чтобы упростить задачу поиска ссылок между этими двумя типами. Несмотря на то, что они разделены на разные типы, они используются «согласованно» в одном sql пакете.

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

Так что же дает?

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

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

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

Чао ~