Организация кода по функционалу, а не по слоям |
- Statistics
- Participants
- Translate into Russian
- Translation result
- Translated in draft, editing and proof-reading required.
Первый вопрос при разработке приложения: - "Как разделить код по пакетам?". Для обычного приложения существует 2 способа ответа на данный вопрос:
Разделение по функционалу
При разделении кода по функционалу названия пакетов отражают набор функций приложения. Все классы, связанные с конкретной функцией находятся в одном пакете (папке). Как результат код разбит на модули и сильно сцеплен внутри пакетов, с минимальными связями между пакетами. Классы, тесно взаимодействующие между собой, находятся рядом. Они не распространяются по пакетам всего приложения. К тому же в некоторых случаях, удаление функционала можно свести к одному действию - удалению папки. (Удаление может быть хорошим тестом правильного разделения на модули: классы хорошо разделены на модули, если их можно удалить без дополнительных изменений).
При разделении кода по функционалу, названия пакетов соответствуют основным, высоко-уровневым составляющим заданной предметной области. Например, аптекарское приложение может иметь следующие пакеты:
- com.app.doctor
- com.app.drug
- com.app.patient
- com.app.presription
- com.app.report
- com.app.security
- com.app.webmaster
- com.app.util
- и т.п.
В пределях каждого пакета все или наибольшее количество классов связано со специфичной функцией. Например, пакет com.app.doctor может содержать следующие классы:
- DoctorAction.java - обработчик или контроллер
- Doctor.java - модель
- DoctorDAO.java - класс для доступа к данным
- скрипты БД
- файлы UI (например, jsp-файлы в случае web-приложения)
Важно что пакеты могут содержать не только Java-классы, но также и другие файлы. На самом деле, для разделения пакетов по функционалу так и должно быть, все объекты, связанные с заданной функцией - от пользовательского интерфейса, до Java-классов и скриптов базы данных - могут находится в одной папке.
В некоторых случаях, пакет не должен использоваться другими пакетами приложения. В таком случае, функционал данного пакета может быть легко удален. Если внутри пакета используются функции другого пакета, тогда удалить пакет будет сложнее чем за одну операцию удаления.
Т.е. идея разделения кода по функционалу предполагает что один пакет не может использовать объекты, принадлежащие другому пакету. Предпочтительно, область видимости по-умолчанию определять, чтобы объекты были доступны только внутри пакета (package-private), и увеличивать область видимости объектов до public только для тех объектов для которых это необходимо.
Разбивка по слоям
Другой вариант - это разбивка кода приложения по слоям. При разбивке по слоям, пакеты отражают различные "уровни" приложения, такие как:
- com.app.action
- com.app.model
- com.app.dao
- com.app.util
В данном случае, реализация каждой функции приложения располагается в нескольких папках, которые можно описать как "категория реализации". Каждый каталог содержит объекты, которые обычно не взаимодействуют между собой. В итоге получается низкая сцепленность и модульность с высоким уровнем связанностью между пакетами. Как результат, редактирование фунционала приложения приводит к редактированию файлов в разных папках. К тому же, удаление функционала почти никогда не может быть произведена за одну операцию.
Рекомендация: Используйте разделение по функционалу
Для типового приложения, разделение по функционалу выглядит предпочтительнее:
Более высокая модульность приложения
Как отмечалось выше, только разделение по функционалу обладает высокой сцепленностью, высокой модульностью и низкой степенью связи между пакетам.
Простая навигация по коду
Разработчикам, сопровождающим код, требуется намного меньше действий для поиска объектов, так как все объекты, которые требуются, обычно располагаются в одном папке. Некоторые инструменты, поддерживающие разделение по слоям, используют специальные соглашения о наименовании пакетов, чтобы облегчить навигацию по коду. Однако, при разделении кода по функционалу нет необходимости для подобных соглашений в первую очередь благодаря отсутствию самой необходимости навигации между папками.
Высокий уровень абстракции
Поддерживание высокого уровня абстракции - это один из основных принципов хорошего программирования. Это позволяет легче рассматривать любую задачу и отделять общие функции от деталей реализации. В следствии наличия высокоуровневой абстракции, приложение становится более читаемым (само-документированным): в целом приложение взаимодействует с рядом пакетов, а основной функционал связан с названием пакетов. Основная ошибка при разделении кода по слоям в том, что, наоборот, детали реализации размещаются перед высокоуровневой абстракцией.
Разделение и функционала и слоев
Разделение по функционалу также поддерживает идею разделения по слоям, но это разделение достигается за счет использования различных классов. Разделение по слоям, с другой стороны, достигает этого используя как и различные классы так и различные пакеты, что не выглядит необходимым и целесообразным.
Сокрытие реализации
Уменьшение области видимости объектов является ещё одним правилом эффективной разработки. Здесь, разделение по функционалу позволяет некоторым классам уменьшить область видимости с общего до видимости только классам пакета. Это существенное изменение, которое уменьшит "волновой эффект" (ripple effects). Разделение по слоям, наоборот, не может использовать видимость package-private, и требует объявлять все классы как public. Это принципиальная ошибка, так как это не позволяет вам уменьшить "волновой эффект" с помощью сокрытия реализации.
Улучшение стиля
В случае разделение кода по функционалу, количество классов одного пакета ограничено количеством объктов, связанных с данной функцией. Если пакет делается слишком большим, то он может быть просто разбит на 2 или более пакета. Разбивка по слоям, наоборот, не разделима. По мере увеличения размера приложения, количество пакетов остается примерно таким же, в то время как количество классов в каждом пакете постоянно увеличивается.
Если вам ещё требуются доказательства, рассмотрим следующие вопросы.
Структура папок основоположна для вашего кода
"Как любой проектировщик хочу сказать, что это (структура папок) первый шаг в процессе проектирования который влияет на многое. Первые несколько действий, которые создают основу и в результате проходят через все" - Кристофер Александер.
(Кристофер Александер - архитектор. Не работая программистом, он повлиял на мысли многих людей, занимающихся программированием. Его раняя книга "A Pattern Language" стала первым стимулом для развития шаблонов проектирования. Он много размышлял о том как создавать прекрасные вещи и это в должной мере отразилось в повсеместно принятых в программировании конструкциях).
В интервью для радио CBC Александер рассказал следующую историю: "Я работал с одним из моих студентов. У него были сложности с одним важным проектом. Он просто не знал как взяться за дело. Итак я сел вместе с ним и сказал: Слушай, начни с выяснения что из этого является наиболее важным. Пойми это. Потрать на это время. Не торопись. Подумай об этом в течении какого-то времени. Когда ты почувствуешь что нашел, когда в твоей голове не будет сомнения что это действительно наиболее важная вещь, тогда ставь её во главу и начинай делать её. Когда ты сделаешь её, спроси себя - можно ли сделать её ещё лучше. Отбрось ерунду, просто сделай это сперва, если ты можешь сделать это лучше или нет. Когда это будет сделано, а ты почувствуешь что больше никак не можешь улучшить это, тогда ищи следующую наиболее важную вещь".
Что является первым штрихом в приложении, которое создает его в итоговом виде? Это структура папок. Структура папок - это самая первая вещь с которой встречаются программисты, когда просматривают исходный код. Все остальное вытекает из этого и зависит от этого. Это безусловно одна из наиболее важных составляющих вашего исходного кода.
Рассмотрим различные реакции программистов, сталкивающихся с разным подходом для разделения кода. При раздении кода по функционалу, размышления разработчика могут быть вроде следующих:
"Понятно. Это список всех высокоуровневых функций приложений сразу. Хорошо."
"Посмотрим. Я не уверен где это находится... О, это здесь. А остальное я могу найти тоже прям здесь, все в одном месте. Превосходно."
При разделении кода по слоям, наоборот, мысли программиста могут быть больше похоже на следующие:
"Эти названия папок ничего не говорят мне. Как много функций в данном приложении? Без понятия. Они точно такие же как и остальные. Нет различий. Отлично. Опять тоже самое..."
"Хм. Я не уверен где это находится... Я думаю части этого находятся по всему приложению по всем папкам. У меня действительно есть все что требуется? Я думаю это станет понятно позже."
"Удивительно если соглашение о наименовании ещё используется. Если нет, Я могу найти это совершенно в другой папке."
"Вау, взгляните на размер этой папки... Ничего себе!"
Разбивка по слоям не эффективна и в других областях
По аналогии, видно что разбивка по слоям приводит к плохим результатам. Например, представим автомобиль. На высоком уровне, "реализация" автомобиля делится следующим образом (разбивка по функционалу):
- безопасность;
- двигатель;
- управление;
- система топлива;
- и т.п.
Теперь представим автомобиль реализация которого разделена на низкоуровневые элементы (разбивка по слоям):
- электроника;
- механика;
- гидравлика
В этом случае проблему коробки передач, например, вы можете исправлять во всех трех компонентах. Это означает переход от одной части к другой - совершенно другой части автомобиля. Несмотря на то, что в этих разных частях, вы бы видели элементы совершенно не относящиеся к проблеме которую вы пытаетесь решить. Они просто мешают и постоянно отвлекают от действительной задачи. Было бы проще если бы это было одно место с именно тем что вам требуется и ничем иным?
Во втором примере, рассмотрим крупный аппарат разделенный на различные подразделения (разделение по функционалу):
- отдел по работе с клиентами;
- операционный отдел;
- бухгалтерия;
- отдел по работе с персоналом;
- экспедиция
Если бы использовалось разделение по слоям, то основные подразделения были бы вроде следующих:
- руководство;
- администрация;
- служащие
А теперь представьте этот аппарат разделенный согласно этим трем категориям. Каждый руководитель находится, к примеру, со всеми остальными руководителями, а не с сотрудниками с которыми он работает. Было бы это эффективно? Нет.
Так почему при разработке приложения должно быть по другому? По видимому разделение по слоям просто плохая традиция, которую давно пора отменить.
