Начало выяснения того, как организовать Java-проект

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

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

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

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

Как вы решаете, что входит в пакет? Есть ли набор принципов, подобных SOLID, которым нужно следовать?

Конечно, есть, но я не думаю, что у них есть милая аббревиатура.

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

Принцип эквивалентности повторного использования/выпуска

Дядя Боб описывает этот принцип следующим образом:

Гранула повторного использования — это гранула выпуска. Только компоненты, выпущенные через систему отслеживания, могут эффективно использоваться повторно. Эта гранула и есть упаковка.

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

Осторожно, спойлер: копировать и вставлять (почти) всегда неверный ответ.

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

  1. Ваш код более организован
  2. Хотя это зависимость, она четко указана
  3. Вы не несете ответственности за поддержку этого кода (конечно, мы надеемся, что кто-то другой поддерживает его).
  4. Любые обновления/изменения этого кода изолированы от вашей кодовой базы; вы можете отложить включение обновлений в пакет до тех пор, пока у вас не будет времени убедиться, что он по-прежнему будет правильно работать в вашей системе.

Одним из следствий (повторного) использования кода таким образом является невозможность повторного использования единицы кода, которая меньше размера пакета. Когда вы повторно используете даже небольшую часть пакета, вы становитесь клиентом всего выпуска. Таким образом, мы можем считать повторное использование и выпуск эквивалентными, что порождает этот принцип.

Общий принцип повторного использования

Классы в пакете повторно используются вместе. Если вы повторно используете один из классов в пакете, вы повторно используете их все.

Поскольку введение пакета в качестве зависимости означает, что вы зависите от всего пакета, для создателя этого пакета разумно позаботиться о том, чтобы содержащиеся в нем классы чаще всего использовались вместе.

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

Кроме того, новые версии пакета должны быть проверены для работы с вашим приложением, что само по себе является проблемой.

Как потребитель пакета, вас бы не раздражало, если бы вам пришлось иметь дело с внесением новых изменений в классы, которые вы даже не используете?

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

Общий принцип закрытия

Классы в пакете должны быть закрыты вместе для одинаковых изменений. Изменение, влияющее на пакет, влияет на все классы в пакете.

Идея здесь основана на принципе открытого/закрытого ООП, который гласит, что классы должны быть открыты для расширения, но закрыты для модификации.

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

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

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

  1. Принцип ациклических зависимостей — ваш граф зависимостей не должен иметь циклов (т. е. при отслеживании зависимостей системы вы не можете вернуться к предыдущему пакету).
  2. Принцип стабильных зависимостей. Перемещение в значительной степени зависит от пакетов, которые являются стабильными/менее вероятными для изменения. Меньше полагайтесь на пакеты, которые изменчивы/с большей вероятностью изменятся.
  3. Принцип стабильных абстракций. Чем выше абстрактность пакета, тем выше его стабильность. Хорошая абстракция не требует никаких модификаций, чтобы ее можно было расширить.

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

Моя серверная программа Java в настоящее время включает в себя ряд различных обязанностей. Есть класс MyServer (отвечает за открытие сокета, чтение входного потока от клиента и запись в выходной поток), HTTPRequestParser (отвечает за получение строки, прочитанной из клиентский запрос, анализируя его и возвращая в более структурированном/полезном формате), и аналогичные парсеры для заголовков запроса и параметров строки запроса, которые составляют то, что я называю «действиями сервера». Логично сгруппировать их в пакет, поскольку эти классы будут использоваться вместе.

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

Но что происходит, когда вы можете подумать об изменении, которое коснется большинства классов системы? Мы не можем просто положить все в одну упаковку.

Например, многие классы в моей программе имеют как минимум один метод, который либо принимает данные в виде HashMap, либо возвращает хэш-карту — в обоих случаях эта хэш-карта является представлением исходного HTTP-запроса, но с ключами для таких вещей, как «метод », «путь» и «заголовки», чтобы с ними было проще работать, чем с простой строкой.

Что делать, если я хочу изменить этот формат? Что произойдет, если я решу изменить ключи внутри хэш-карты или даже создам класс Request с этими ключами в качестве полей?

Все должно измениться, потому что все зависит от этого формата.

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

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

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

Если идея запроса действительно настолько распространена, что от нее будут зависеть многие пакеты, она должна быть как можно более стабильной. Какой фактор коррелирует со стабильностью? Абстрактность.

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

Кроме того, теперь у меня есть некоторые рекомендации о том, где разместить свои классы.