Пакеты, системы, модули, библиотеки — КАКОГО?
По моим наблюдениям, минимум раз в неделю в списке c.l.l или другом Lisp-списке «новички» путаются в том, что связано с пакетами. Говорят о «загрузке» пакета, «требовании» (requiring) пакета, удивляются тому, что после загрузки системы нужно пользоваться маркерами пакетов и т.д. Меня это раздражает, думаю также, что это может быть одной из причин, почему начинающие считают, что использование библиотек в Lisp сложнее, чем есть на самом деле.
Обычно я прекращаю попытки написать полезное объяснение, и, естественно, это объяснение очень простое. Я создал эту страницу, чтобы в следующий раз просто отправить сюда, вместо того, чтобы снова и снова объяснять одно и то же.
Прежде всего следует иметь ясную голову. Термин «пакет» сильно перегружен. В дистрибутивах Linux вроде Debian или Gentoo есть «пакеты», «пакеты» есть в языках программирования Java, Perl или Python. Вполне вероятно, что вы пришли в Lisp с предвзятым мнением относительно того, что такое «пакет» или чем он должен быть.
Пакеты
Пакетом в Common Lisp называется полноправный элемент языка, семантика которого четко определена стандартом. Более того, из всех обсуждаемых на этой странице терминов, этот — единственный, имеющий (в контексте Common Lisp) однозначное определение. Пакеты — это, строго говоря, контейнеры для символов. Можно сказать, что они нужны для помощи в организации отдельных пространств имен в ваших программах.
В Common Lisp есть функции и макросы для создания, изменения, исследования и удаления пакетов. Очень хорошее введение в пакеты (и символы) можно найти в главе 21 великолепной книги Practical Common Lisp Питера Сайбела. Определение термина находится в главе 11 (онлайн-версии) стандарта ANSI Common Lisp specification.
В общем, про пакеты это всё. Говоря технически, вы не загружаете пакеты. Вы можете загрузить (с помощь LOAD ) код, который в свою очередь создаст пакет, и это существенное различие.
Кроме того, если ваш Lisp жалуется, что не может найти какой-то пакет, это означает, что пакета как Lisp-объекта нет в образе (т.е. FIND-PACKAGE возвращает NIL ), потому что его еще никто не создал. Это не означает, что Lisp-машина поискала в файловой системе и ничего не нашла. (Частая причина такой неудачи состоит в том, что события происходят в неправильном порядке. Об этом ниже.)
Системы
Системы, в отличие от пакетов, даже не упоминаются в стандарте. Тем не менее, опытные Lisp-программисты знают этот термин, поскольку им потребуютется знать и применять какой-то инструмент определения систем. Наиболее заметный сегодня — ASDF (используется большинством Lisp-библиотек с открытым исходным кодом); другой известный инструмент определения систем, гораздо старше ASDF — MK:DEFSYSTEM. Некоторые разработчики также поставляют свои инструменты определения систем вместе с дистрибутивами, см. например, Common Defsystem для LispWorks.
В этом ключе система, строго говоря, это набор кода плюс инструкция по его обработке, например, зависимости от других систем, что следует загрузить/скомпилировать в первую очередь и т.д. Другими словами, инструмент определения систем по своему назначению похож на make или Ant.
Кроме того, инструмент определения систем обычно может намного больше — Common Defsystem может, например, интегрировать файлы библиотек типов COM, ASDF полностью расширяем и использовался, среди прочего, для компиляции файлов на C. Он также часто используется для определения тестовых наборов описываемой системы.
Хотя ASDF и весьма популярен, он не вездесущ. Он идет предустановленным со многими Lisp-системами вроде SBCL, OpenMCL или AllegroCL, вероятнее всего, что он загрузится и в других Lisp-системах, но этот факт не делает его частью Common Lisp. Это набор кода без явной спецификации и с разными версиями, которые бывают несовместимы между собой.
Поди пойми…
Модули
Например, в LispWorks можно использовать
для загрузки парсера, способного читать определения на C, но это не сработает на OpenMCL. Также можно вызвать
для загрузки ASDF на OpenMCL, но не в LispWorks.
Библиотеки
Скорее всего вы не найдете четкого определения, что такое библиотека. Большинство людей думают об этом как о коллекции кода, предназначенного для выполнения одной или нескольких определенных задач и распространяемого как единое целое, обычно в виде сжатого архива, который можно откуда-то скачать. На самом деле, это неясное определение является, думаю, наиболее подходящим при разговоре о программах, написанных на Lisp. Большинство Lisp-библиотек сегодня включают в себя определение (ASDF) системы, но это вовсе не обязательно. Возможно, в зависимости от способа получения, это будет модуль в вашей Lisp-системе, но и это тоже не обязательно. Кроме того, библиотека обычно определяет один или несколько пакетов, а может и не определять ни одного.
И, по соглашению, а может из-за недостатка фантазии, может сложится и часто складывается ситуация, когда библиотека «Ку» идет с определением системы «Ку», которую можно загрузить как модуль «Ку». После загрузки кода получите новый пакет, называемый «Ку». Четыре разных сущности с одинаковым именем! Я допускаю, что это сбивает с толку, но надеюсь, что несколько предыдущих абзацев помогли слегка прояснить ситуацию.
Но у меня все еще ничего не работает!
Часто люди жалуются, что они не могут скомпилировать файл, содержащий код вроде этого:
Почему так? Почему я могу загрузить этот файл, но не могу скомпилировать его? И почему я могу скомпилировать его после загрузки? Не странно ли?
Нет, не странно. Компилятор читает первую форму (которая является инструкцией скомпилировать — если необходимо — и загрузить систему CL-PPCRE, но не выполнить ее. В конце концов, компилятор заинтересован лишь в компиляции кода. После выполнения первой формы он переходит ко второй форме, к определению функции. Здесь возможно сообщение об ошибке, так как Lisp-сканер, пытающийся читать эту форму, обнаружит последовательность символов «cl-ppcre:scan», которая должна обозначать внешний символ из пакета CL-PPCRE, но самого пакета CL-PPCRE еще нет. В процессе загрузки системы CL-PPCRE, кроме всего прочего, создается пакет CL-PPCRE, но этого еще не произошло. Читайте главу 3 CLHS.
Можно воспользоваться EVAL-WHEN для указания компилятору загрузить CL-PPCRE перед чтением второй формы. Следует, однако, найти другой способ организации своего кода. Первая форма — это просто обявление того, что ваш код зависит от системы CL-PPCRE. Такое не должно находиться в том же файле, что и Lisp-код. Напишите определение системы для вашей программы и поместите зависимости туда.
Модуль 2. Урок 4. Пакеты в Java. — Введение в Java
Что такое пакеты?
Пакеты, по сути, являются файловой и логической структурой связей классов в мире java. Очень схоже с файловой системой компьютера. На уровне файловой системы пакеты это и есть папки, в которых лежат другие папки (подпакеты) и классы. Но пакеты не всегда описывают напрямую всю структуру проекта. На практике проект включает в себя различные ресурсы, а структура папок, которую мы назначаем как имена пакетов для наших классов — может быть лишь небольшой частью целого проекта. Ведь, кроме основного кода в пакетах, у нас должны быть еще и тесты, библиотеки или даже другие языки программирования в проекте в целом.
Как аналогию, можно привести пример с адресами.
Что такое пакет для класса
Для класса его пакет — это его местоположение в проекте, относительно других классов. Благодаря разделению классов на несколько пакетов — мы организовываем структуру программы.
Сильно забегая вперед скажу, что такое разделение нужно не только для красоты, но и для ограничения доступа к некоторым членам класса. Например:
Как создать класс внутри пакета?
Рассмотрим этот процесс поэтапно.
Сначала пишем имя любимого редактора в терминале, потом существующий путь, а потом имя будущего текстового файла. Пример для редактора nano :
Или с помощью sublime:
Если команда subl не найдена и Вы уверены в том, что sublime установлен — проверьте, в переменных среды окружения, наличие прописанного пути к папке, в которой лежит subl.
Можно и обычным блокнотом создать нужный файл в нужной директории. Создать сам файл можно и любым другим удобным для вас способом.
Файл создан. Но класса в нем нет. Создадим класс:
Теперь у нас есть и файл в нужной папке, и класс в файле. Но сам класс ничего не знает про то, что он принадлежит некому пакету. Исправим это:
Не забываем сохранять файл!
Ключевые моменты верного создания класса внутри пакета:
Корень пакетов
А как же задать корневой каталог (папку) как основу пакетов, в котором уже и происходит ветвление этих всех под-пакетов (под-папок)? Почему, например, папка io является корнем для классов проекта, а папка src — не является пакетом, да и вообще не входит в пакетную структуру проекта?
Пример содержимого класса OneMoreClass.java :
Пример содержимого класса SomeView.java :
Имена пакетов
Например, package com.MySuperLongPackageName.view — плохая практика именования пакетов.
Совокупность имен под-пакетов делает проект уникальным, не похожим на миллионы других. Даже если, в каждом проекте в мире, 100% будет класс Main.java — то они маловероятно пересекутся и помешают друг-другу. Но даже если пересекутся — это решаемо, рассмотрим это в дальнейших примерах.
Применение пакетов
Адреса (пакеты) классам, в нашем проекте, мы уже выдали. Они нужны для доступа классов друг к другу.
Как уже было сказано выше — пакет можно сравнить с адресом. Рассмотрим это на примере ветвления папок(пакетов).
Но прописывание таких длинных путей ( io.hexlet.xo.view.SomeView ) — просто не удобно. Поэтому, к нам на выручку, приходит импортирование.
Импортирование пакетов
Перепишем предыдущий пример с импортированием любых классов из пакета io.hexlet.xo.view :
Импортирование по умолчанию
Обратите внимание на строку System.out.println(«Some very important message!»); в классе SomeView :
Статическое импортирование
Как компилировать классы в пакетах
Как запускать классы в пакетах
Именно с него начинается работа самостоятельной java-программы. В остальных классах одной программы — этот метод не нужен.
А для запуска «пакетного» класса на исполнение требуется иной подход.
Полезные ссылки:
Что такое модуль в Python? А что значит пакет?
Написание модулей
Модуль draw может выглядеть примерно так:
Импорт объектов модуля в текущее пространство имен
Преимущества использования этой записи в том, что функции внутри текущего модуля проще использовать, поскольку вам не нужно указывать, из какого модуля поступает функция. Однако ни в одном пространстве имен не может быть двух объектов с одинаковым именем, поэтому команда import может заменить существующий объект в пространстве имен.
Импорт всех объектов из модуля
Мы также можем использовать команду import * для импорта всех объектов из определенного модуля, например:
Это может быть немного рискованно, так как изменения в модуле могут повлиять на модуль, который его импортирует, но это более короткий вариант и также не требует указания объектов, которые вы хотите импортировать из модуля.
Пользовательское имя импорта
Мы также можем загружать модули под любым именем. Это полезно, когда мы хотим импортировать модуль условно, чтобы использовать то же имя в остальной части кода.
Например, если у вас есть два draw модуля с немного разными именами, вы можете сделать следующее:
Инициализация модуля
Расширение пути загрузки модуля
Есть несколько способов сказать интерпретатору Python, где искать модули, кроме используемых по умолчанию, которыми является локальный каталог и встроенные модули. Вы можете использовать переменную окружения PYTHONPATH чтобы указать дополнительные каталоги для поиска модулей, например так:
Это добавит каталог foo в список путей для поиска модулей.
Изучение встроенных модулей
Проверьте полный список встроенных модулей в стандартной библиотеке Python здесь.
Мы можем посмотреть, какие функции реализованы в каждом модуле, используя функцию dir :
Когда мы находим в модуле функцию, которую хотим использовать, мы можем получить дополнительную информацию о ней, используя функцию help внутри интерпретатора Python:
Написание пакетов
Файл __init__.py также может решить, какие модули пакет экспортирует как API, сохраняя при этом другие модули внутренними, переопределив переменную __all__ например, следующим образом:
Упражнение
Научим основам Python и Data Science на практике
Это не обычный теоритический курс, а онлайн-тренажер, с практикой на примерах рабочих задач, в котором вы можете учиться в любое удобное время 24/7. Вы получите реальный опыт, разрабатывая качественный код и анализируя реальные данные.
Теория программирования: пакетные принципы и метрики
Чтобы применять любые принципы правильно, сначала нужно их понять — то есть осознать, откуда они взялись и для чего нужны. Если применять вслепую всё, что угодно — результат будет хуже, чем если бы мы вообще не использовали эти принципы. Я начну издалека и сначала расскажу про абстракцию.
Что есть абстракция?
Это обобщение существенного и удаление несущественного, так как мир настолько сложен, что запрограммировать удаётся только его существенные части. Если попытаемся запрограммировать всё — мы потонем, поэтому абстракция помогает нашему мозгу «впихнуть невпихуемое», как это умеют делают военные (а программисты — пока нет):
Наши проблемы идут от мозга и от того, как устроена наша память. В отличие от хорошего хранилища долговременной памяти, кратковременная устроена по типу stack:
Еще мы используем Chunking (группировку) всякий раз, когда важно запомнить что-то большое. Например, чтобы запомнить число 88003334434, мы разделим его на группы по типу телефонного номера: 8-800-333-44-34. Для нашего мозга получится 5 объектов, которые запомнить легче, чем пытаться удержать число целиком или отдельно каждую его часть.
В коде мы используем слои, чтобы облегчить эту задачу. Однако пять таких слоев в голове удержать становится сложно, а больше пяти — уже проблема. Для лучшего представления это можно сравнить с неправильной супер-лазаньей, в которой слишком много слоёв и — они все слиплись. И это гораздо хуже, чем спагетти — потому что спагетти мы можем отрефакторить и что-то нормальное в результате получить. А чтобы получить что-то нормальное из такой лазаньи, её надо сначала растерзать и превратить в понятные и очевидные спагетти, а потом заново собирать лазанью, только уже правильную.
Поэтому, чтобы совладать с неподъёмно сложными системами, изобретают архитектуру, используя абстракцию. Это не цель, а инструмент, и одновременно — необходимое зло, так как все эти классы, супер-паттерны и прочие шутки не от нашей хорошей жизни.
Но как построить абстракцию, не сделав хуже?
Существует два понятия: cohesion (связность) и coupling (связанность). Они относятся в первую очередь к классам, но в целом и ко всем остальным сущностям. Разницу мало кто видит, так как звучат они почти одинаково.
И, хотя оба означают связь, coupling понимают в негативном ключе. Один объект завязан на другой в плохом смысле, если, ломая один из объектов, ломается всё остальное по цепочке. Cohesion несёт в себе позитивную ноту. Это группировка, в которой то, что близко по смыслу — лежит в одном месте и взаимодействует примерно с теми же местами, опять же близкими по смыслу.
Для того, чтобы понять, coupling у вас или cohesion, существуют проверочные правила. Их сформулировал инженер и специалист в области информатики Роберт Мартин еще в 2000 году, и это — принципы SOLID:
Что есть пакет?
Пакет — это группа единиц кода. Причем, пакеты – это не обязательно пакеты Maven или Composer, или npm. Это программные модули, — то, что вы выделяете в namespaces или иным способом группируете.
Обычно имеются в виду классы, но это могут быть и библиотеки, и модули (не те, которые в фреймфворках, а которые изначально описывали в объектно-ориентированном программировании, то есть группы относящихся друг к другу классов в отдельном namespace). И даже микросервисы можно назвать пакетами — если это настоящие микросервисы, а не те макро-, на которые частенько распиливают монолиты.
И того, кто эти пакеты пилит, обычно волнуют два вопроса: Как правильно формировать пакеты и как работать с зависимостями пакетов?
Как их правильно формировать?
Собственно, cohesion и coupling, как основополагающие принципы, отлично работают и для пакетов. Но сработает ли SOLID для пакетов?
Да, но не совсем. Оказалось, что существуют ещё 6 принципов от того же Роберта Мартина, сформулированные в том же году. Часть из них относится к cohesion, и это о том, как дизайнить код: REP, CCP, CRP. Другая часть — это coupling (то есть использование пакетов): ADP, SDP, SAP — и это о том, как сделать так, чтобы один пакет не завязался на другой и чтобы всё нормально работало:
1 принцип – REP (Reuse-Release Equivalency Principle)
На сегодняшний день этот принцип выглядит до смешного очевидным, но не забываем, что сформулирован он в 2000 году, когда не было таких замечательных штук, как Maven, Composer и прочих, а пакетные релизы были не частыми.
Принцип гласит: «The granule of reuse is the granule of release. Only components that are released through a tracking system can effectively be reused. This granule is the package. — что переиспользуем, то и релизим. Эффективно переиспользовать можно только компоненты, релизнутые через системы версионирования. Такие компоненты называются пакетами». То есть упаковывайте то, что переиспользуется в отдельные пакеты и релизьте это через любимый пакетный менеджер, версионируя по SemVer.
2 принцип – CCP (Common Closure Principle)
«Classes that change together are packaged together — изменение в пакете должно затрагивать весь пакет». Этот принцип очень похож на SOLID-ский OCP. Классы, которые изменяются по одной и той же причине, должны упаковываться в один пакет. Что логично.
Нормальный пример: адаптеры. Библиотека, допустим, кеш. Если мы запихиваем в один пакет тучу адаптеров: для файлов, memcached, Redis, то при попытке изменить один какой-то адаптер мы нарушаем два принципа. Во-первых, принцип REP (начинаем релизить один из адаптеров, а релизить приходится все). А во-вторых — принцип CCP. Это когда классы для адаптера под Redis изменяются, а все остальные адаптеры в пакете —нет.
3 принцип – CRP (Common Reuse Principle)
«Classes that are used together are packaged together — Пакеты должны быть сфокусированными. Использоваться должно всё». То есть классы, которые используются вместе — упаковываем вместе. Проверочное правило здесь такое: смотрим, используется ли в нашем софте всё из того пакета, который к нему подключен. Если используется чуть-чуть, значит, скорее всего, пакет спроектирован неверно.
Эти три принципа дают понимание, как пакеты дизайнить. И казалось бы, нормально делай — нормально будет. Однако реальность сурова, и я сейчас объясню — почему. Вспомним треугольник от Артемия Лебедева, который вершины «быстро», «дёшево» и «качественно» обозначил несколько другими словами. Такой же треугольник нарисовали и для пакетных принципов в Институте Макса Планка:
Получается, эти принципы конфликтуют, и в зависимости от того, какие стороны треугольника мы выбираем, вылезают соответствующие косяки:
Теперь переходим к принципам использования.
4 принцип – ADP (Acyclic Dependencies Principle)
«The dependency graph of packages must have no cycles — Если есть циклические зависимости, то проблема вызывает лавину». Если есть циклы, то есть зависимость пакета зависит от самого пакета прямо или косвенно, то косяк в одном пакете вызывает лавину во всех остальных пакетах, и ломается абсолютно всё. К тому же, такие пакеты очень тяжело релизить.
Поэтому надо проверять, есть ли циклы. Для этого необходимо строить направленный граф зависимостей и смотреть на него. Руками это делать не очень удобно, поэтому для PHP есть библиотека clue/graph-composer, которой скармливаешь пакет, и она строит гигантский граф со всеми зависимостями. Смотреть на это, конечно, невозможно, поэтому надо зайти в PR#45, зачекаутить его и выбрать возможность исключать зависимости, которые не интересны. Допустим, если вы пишите фреймворк, то вам скорее всего интересны зависимости на свои пакеты, а чужие — не так сильно, ведь свои косяки поправить можем, чужие — тяжелее. И получается вот такой граф:
Если мы видим — как здесь — что циклических зависимостей нет, то всё отлично. Если есть, надо исправлять. Чем меньше зависимостей, тем проще.
Как разорвать цикл?
5 принцип – SDP (Stable Dependencies Principle)
Это принцип стабильных зависимостей: «Depend in the direction of stability — Не получится строить стабильное на нестабильном». Нестабильность считается так:
Если на нас завязалось очень много всего — скорее всего, мы стабильны. Если же мы завязались на много всего, то, очевидно, мы не очень стабильны. Как повысить стабильность? Следующим принципом.
6 принцип – SAP (Stable Abstraction Principle)
Принцип стабильных абстракций гласит «A package abstractness should increase with stability — Стабильные пакеты абстрактны / Гибкие конкретны». То есть абстрактность должна возрастать со стабильностью. Стабильность здесь — то, как часто нам приходится менять части пакета: классы, интерфейсы, или что-либо ещё. Абстрактные пакеты должны быть стабильны, чтобы безболезненно на них завязываться. В примере с тем же кэшем пакет с интерфейсом будем сверхстабильным, потому что менять интерфейс, про который мы договорились и хорошо над ним подумали — скорее всего, не придётся. Если мы, конечно, абстрагируем не СУБД.
А вот гибкие пакеты, наоборот, конкретны — они должны быть нестабильны, чтобы мы их легко меняли. Все наши приложения, наши конечные пакеты — это на самом деле дико нестабильная штуковина, которая зависит от тучи всего и ломается чаще, чем пакеты, от которых она зависит.
Можно ли измерить абстрактность?
Конечно. Абстрактность — это число абстрактных классов и интерфейсов в пакете, деленное на общее число классов и интерфейсов в этом самом пакете:

Еще есть такой полезный показатель, как D-метрика, в которой по вертикали — нестабильность, а по горизонтали — абстрактность. По двум зонам — вверху справа и внизу слева — мы можем понять:
Линия посередине называется главной линией, и если классы и интерфейсы попадают на неё или выстраиваются вдоль — это тот случай, когда всё отлично. По сути, D-метрика — это дистанция от главной линии, поэтому 0 в этом случае — это хорошо, а 1 — плохо. Но, как правило, ни то, ни другое не случается — значения плавают в диапазоне от 0 до 0,9-0,7. Считается метрика так:

Для PHP есть 2 инструмента для того, чтобы посмотреть метрику своих пакетов:
Как и SOLID, все эти дополнительные принципы и метрики — не догма, но могут быть весьма полезными.
Резюме
Конечно, есть области, в которых можно сознательно игнорировать все эти принципы, отдавая себе отчёт в том, что за этим стоит и где вы можете получить проблемы.
Данные принципы же позволяют не скатываться в монолит или в left-pad из npm. С left-pad была в свое время история — его создали для добавления символа в конце строки, так как в JavaScript есть традиция дробить пакеты вообще в пыль. А потом на этот пакет завязалось практически всё — вплоть до пакетных менеджеров и самых крутых фреймворков. В какой-то момент автор обиделся на всех и выпилил left-pad из системы — после чего, как вы понимаете, сломалось всё. Рассмотренные принципы, в том числе, позволяют уменьшить вероятность такого сценария.
Единственная конференция по PHP в России PHP Russia 2021 пройдет в Москве 28 июня. Первые доклады уже приняты в программу!
Купить билеты можно тут.
Хотите получить материалы с предыдущей конференции? Подписывайтесь на нашу рассылку.







