Как стать автором
Обновить

Хватит организовывать код по типу файлов

Время на прочтение 4 мин
Количество просмотров 40K
Всего голосов 63: ↑54 и ↓9 +45
Комментарии 115

Комментарии 115

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

За все годы работы программистом я успел поработать над множеством проектов (в основном JavaScript). Файловая структура некоторых из них была разделена per feature, как рекомендуется в статье, другие же имели per layer структуру. И я заметил одну интересную деталь: в per-layer проектах код мог быть хорошим или плохим, но в per-feature проектах он всегда был отвратительно грязным. Почему же так получалось?

Ответ очень прост. У per feature подхода существует одна неустранимая проблема. Возьмем к примеру, фид в котором имеются посты и комменты.

Per-feature структура будет выглядеть так:
/comment/api.js
/comment/component.js
/comment/state.js
/post/api.js
/post/component.js
/post/state.js

Per layer будет выглядеть так:
/api/comment.js
/api/post.js
/component/comment.js
/component/post.js
/state/comment.js
/state/post.js


Вроде бы подходы равноценны. Но так кажется только до того момента как нам нужно добавить некоторую фичу, в которой разделение на домен не столь очевидно. Например, нам нужно добавить отображение самого популярного коммента в компоненте поста. В случае с per layer подходом у нас нет проблем, мы импортируем api из папки api, а state из папки state. А в случае с per feature подходом мы импортируем api и state из папки comment. После пары таких итераций проект per feature проект становится полон перекрестных зависимостей, а вот per layer проект остается чистым и простым.

Если честно, у вас даже на примере в вакууме что-то не складывается.


А в случае с per feature подходом мы импортируем api и state из папки comment.

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


После пары таких итераций проект per feature проект становится полон перекрестных зависимостей

Что плохого в перекрестных зависимостях? Перекрестные — не циклические.

Ваш пример очень интересен, только не пойму в чем разница, например в
per layer:

import{ fetchMostPopularComment } from '@/api/comment';
import { dispatchSetMostPopularComment } from '@/state/comment';

per feature:

import { fetchMostPopularComment } from '@/comment/api';
import { dispatchSetMostPopularComment } from '@/comment/state';

вроде разница не очень заметная (а она и не должна быть заметной в том коде, что я привел). Можете пояснить по подробнее пожалуйста.

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

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

Для ясности приведу еще примеры проблем с per-feature подходом.

Пример 1

Вы хотите написать некоторую обертку вокруг всех ваших API-запросов, обрабатывающую ошибки и предоставляющую какие-то удобные методы.

При per-feature подходе у вас получается что-то вроде:
/comment/api.js
/comment/component.js
/post/api.js
/post/component.js
/util/api.js


При per-layer подходе у вас получается что-то вроде:
/api.comment.js
/api/post.js
/api/util.js
/components/comment.js
/components/post.js


В per-layer структуре получается, что все связанное с API лежит в одном месте, и программист знает где что искать, а в per-feature структуре работа с API тонким слоем размазано по всему проекту.

Пример 2

У вас есть как и прежде посты и комменты, но вы захотели добавить новую фичу — удаление. Удалить можно и пост, и комментарий. Для этого достаточно отправить запрос вроде DELETE post/1 или DELETE comment/1.

Конечно же, вы можете написать одинаковые методы в /comment/api.js и /post.api.js. Но вы же не хотите плодить лишний код (в реальном проекте у вас же не две, а двадцать-тридцать сущностей) и сделать единый метод, который принимает deleteEntity(entityName, id) и вызывает DELETE-запрос с нужными параметрами.

При per-layer подходе все просто, вы создаете файл common.api и пишите этот метод туда:
```
/api/comment.js
/api/common.js
/api/post.js
/components/comment.js
/components/post.js
```

А что вы будете делать при per-feature подходе? Я вам скажу, что. Вы создадите папку common в руте и добавите туда файл api.js. И у вас получится что-то вроде:
/comment/api.js
/comment/components.js
/common/api.js
/post/api.js
/post/component.js


И дальше при любой такой проблеме вы будете пихать в папку common всякую всячину и она заполнится мусором и хламом. Там будет все: какие-то api-методы, работа со стейтом, утилиты и черт знает что еще.

Пример 3
А теперь давайте подумаем как в наш проект добавить два сервиса — один для работы с пуш-нотификациями, а другой скажем для работы с логгером.

В per-layer подходе все как всегда просто:
/api/comment.js
/api/post.js
/components/comment.js
/components/post.js
/services/pushNotifications.js
/services/logging.js


А вот что вы будете делать при per-feature подходе? Сложите все в папку common? Вряд ли. Скорее всего вы также создадите папку services. И у вас получится
/comment/api.js
/comment/component.js
/post/api.js
/post/component.js
/services/pushNotifications.js
/services/logging.js


То есть вы опять же придете к per-layer разделению, только вместо него у вас будет уродливая смесь обоих подходов.

Послесловие
Все это я обсуждаю в скажем так идеальном варианте развития событий и не сильно вложенной файловой структуре. Обычно, особенно если над проектом работает не один человек, а несколько, при использовании per-feature подхода я вижу что-то вроде:
import {doSomething} from '../../Editor/EditorSettingsScreen/Header/api.js'

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

Я Ваши примеры организовал так

/api/post/create.js
/api/post/delete.js
/api/comment/create.js
/api/comment/delete.js
/api/common.js
/component/post/create.js
/component/post/delete.js
/component/comment/create.js
/component/comment/delete.js
/service/notify/push.js
/service/logging.js

Апи бывает абстрактное, работающее со всеми сущностями, тогда это "отдельная фича", но там не будет никаких "post/delete". Либо оно специфично для каждой сущности и соответственно кладётся рядом с ними.

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

Вы так говорить будто "собственный интерфейс взаимодействия" имеет какую-то особую ценность в отрыве от того с чем собственно идёт взаимодействие.

Апи не важно с чем он взаимодействует. У него есть абстрактные объекты и их методы. Как это сделано под капотом не важно.

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

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

А это ещё одна ошибка. Интерфейсы создаются на использующей их стороне. Как ни крути - не надо класть все рядом с тем что будет использовано.

Возьмём такой пример. Апи имеет один и тот же интерфейс. Меняется только апи версия. По Вашей логике будут созданы 2 разных апи рядом с каждым компонентом с которым этот компонент работает.

В данном случае используемое и использующий находятся рядом.


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

Всё, как обычно, зависит от задачи. Для бэка, возможно, будет удобнее per layer.
Для web-фронта же ещё надо думать про оптимальную доставку кода. Мне кажется, что per feature проще паковать и разрешать зависимости — можно паковать всю фичу в один чанк.
Для per layer сложнее построить дерево зависимостей и разбить файлы по чанкам.

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


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


В своих проектах на Python/Flask у меня есть 2 основных папки: modules, views. Первая предоставляет API для работы с модулями, там есть папка с комментами, а в ней методы create_comment, delete_comment, list_comments. В папке posts — get_post, delete_post и т.д. Но все "запутанную" работу делают файлы в корневой папке views, которые так же разбиты на логический группы (при этом структура папок внутри modules и views может не совпадать, например "переводы" — это модуль, но REST для него нету). И вот при таком подходе в своей функции на получение самого популярного коммента в компоненте поста, вам надо будет сделать два вызова:


@app.get('/posts')
def most_popular():
    post = get_post()
    popular_comment = get_comment(
         model_type=CommentType.post, model_id=post.id, popular=True
    )
    return jsonify({'post': post, 'comment': popular_comment})

Структура папок:


/modules/
..../posts/
......../models.py
......../exc.py
......../admin.py
......../migrations
..../comments/
......../models.py
......../exc.py
......../admin.py
......../migrations
/views/
..../feed/
......../handlers.py
......../routes.py
......../serializers.py


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

Полностью согласен! Логично разделить код по смысловым «модулям». А там, если их малое количество, можно и не городить «стековый подход».

По хорошему — IDE должно уметь показывать код и так и так, а не заставлять программиста ползать по структурам папок и пакетов.

А ещё лучше было бы, если бы яп определял пакеты не по расположению файлов в папках.

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

Тоже думал в эту сторону.

Вообще файлы вроде бы не нужны, немного устаревший конструкт в разрезе хранения классов.

Есть некоторые сложности вроде того что удаление класса из одной иерархии не удаляет из другой, но это конечно решается.

+1
Вроде тэги давно придумали, можно всё гибко идентифицировать, но нет, живём везде до сих пор с папками.

Наверное потому-что это замена одних проблем другими

Например?
  • Как просто определить уникальность файла/сущности, если она не привязана к дереву? Для обращения из другого места.
  • Обратная совместимость с существующими ЯП, компиляторами, другими инструментами.
  • Сложно окинуть взглядом весь проект, с древовидной структурой это проще
  • Необходим новый способ контроля версий или обертка поверх существующего.

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

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

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

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

  • А как в каталогах у вас уникальность определяется? Один и тот же файл может лежать в разных папках. Скорее наоборот тут файл будет всегда один, а на него можно навешивать разные тэги.
  • Обернуть дерево тэгов в слой, который поймут легаси-приложения
  • Наоборот можно строить произвольные деревья тэгов под нужную задачу при этом не перемещая файлы/папки
  • см. выше

ну и никто не запрещает не заменять папки тэгами, а просто добавить возможность указывать тэги и работать с ними
Так скоро — глядишь! — и Smalltalk изобретут :D

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

Кстати уже сейчас в некоторых Java проектах есть как минимум 2 независимых иерархии: одна по package-s, и вторая по файловой системе. Т.е. не все файлы из одного package обязательно лежат в одном месте, а могут быть разнесены из каких-либо соображений в разные места.
Если не путаю такое, например, применяется в Android местами.
Хорошо было бы. Графовая файловая (хотя слово «файл» здесь уже не так применимо) система является одновременно очень хорошей и быстрой графовой БД.

Upd. Вроде даже есть tagfs (Tagsistant)
Да же простые теги без иерархии было бы хорошо. Иерархию уже после можно изобретать исходя из того как получились простые теги.
+1
По сути, теговая система это будет во многом похожа на систему закладок. Только такие закладки должны ставиться одна на множество мест, и назначаться такой закладке имя. И плюс они должны уметь применяться к не текстовым объектам.

После в эксплорере закладок раскрывается плюсик такой закладки, и видится список где она применена — файл и строка. Щелкаем — позиционируемся.

Применение такой штуки к строке или к блоку файла, это может быть затруднительно в реализации, поэтому для начала только на файл.
(c ): 'чаще всего видел решение, подразумевающее группировку всех файлов по их типу'.
Голубчик, Вы где, собственно, работаете?
Автор показал группировку по типу файлов, по смыслу, по типу файлов внутри смысловых групп. И я до последнего думал, что ну вот сейчас он покажет, как разложить все «по DDD», с onion, портами и адаптерами, чтобы и смысл не потерялся, и технические детали не лезли из всех щелей — но статья внезапно оборвалась, жаль.
Кажется автор переизобрёл HMVC
Организация кода по смыслу, конечно, удобней. Но это ж думать надо. Вот в вашем примере с отелем, логику рум-сервиса вы куда поместите, к номерам или к гостям? Раскладывать же дтошки к дтошкам, сервисы к сервисам — легко и понятно. Плюс, «стековая» организация поддерживает некоторые важные свойства архитектуры автоматически. Можно легко доказать, например, что дто не содержат логики, а запросы в базу не делаются из фронтенда. В семантической организации это тоже можно сделать, но понадобятся более продвинутые инструменты, а это опять же «надо думать»

Думать в любом случае придется, ведь нужно будет решить, какое имя у роута. Если роут /room/.., значит к номерам. Если роут уникальный, вроде /room-service, значит и модуль отдельный

Хотел бы добавить еще на подумать, вопрос общего кода между n «смысловых» единиц, какой-нибудь Common… скорее всего в перспективе превратится в отстойник всего и вся со всех слоев, что так или иначе не удалось положить в «смысловой». Плюс, при организации кода по «стеку» внутри «смыслового» подхода, неявно наталкивает новаторов на использование разных «стековых» организаций… но, все конечно субъективно
У меня на собственном проекте примерно так, но меня это не напрягает. Просто эта свалка должна на постоянной основе рефакториться, т.е. какой-то функционал выносится из неё, какая-то добавляется в неё. Скажем так, это категория «пока не уверен, куда разумнее положить».

Если исполнять то, о чём я писал выше, это хороший и удобный рабочий инструмент, снимающий дофига когнитивной нагрузки.
Верно подмечено, но, на собственном проекте Вы как Джеймс Бонд, есть лицензия на все: любой рефакторинг, любые реорганизации проекта, в любой момент Вы даже можете пересмотреть кардинально архитектуру. И за n-ое количество времени все переделать.
Другой момент, это производственный код, который далеко не всегда можно отрефакторить при появлении первых неприятных «запахов». Собственно, это, на мой субъективный взгляд чаще всего и является причиной перехода от Common, до «да, положи пока в Common, позже зарефакторим и почистим его..»

Я делаю так: пакет support, который подразумевает, что в него пойдет вся техничка. А уже внутри него - такая же DDD организация, типа strings, graphql, exceptions, только домены тут уже сугубо технические.

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

Очень часто вижу подобное разделение в Angular. И с этим можно работать, конечно. Но! Вместо нескольких модулей подгружаемых по-требованию в таких проектах более характерен SharedModule/CommonModule со всей логикой приложения с размером в несколько десятков мегабайт. (На всякий случай - это непозволительно много, даже в эпоху гигабитных каналов у конечного пользователя)

Вопрос есть ли один более правильный подход для разных по многим параметрам проектам…? (Размер, архитектура и тд.)

Есть. Файлы, которые меняются преимущественно одновременно, располагать близко друг ко другу. Группировка по фичам — частный случай.

Если по DDD раскладывать, то так в общем-то в итоге и получится.
Другое дело, что сразу же может быть далеко не очевидно, как же лучше всего раскладывается, и придётся немного поперекладывать в процессе развития кода.


В частности, именно поэтому я всегда топлю за простые относительные импорты в js/ts — с ними перекладывать файлы можно как угодно, IDE за тебя все пути отрефакторит сама.

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


Простой пример: /post/comment


Если выкинуть post, то "комментарий" останется "комментарием". То есть можно (и нужно) сократить до /comment. В качестве бонуса — возможность цеплять комменты не только к постам.


А вот из /task/status ничего не выкинешь, так статус без контекста — это не понятно, что такое.

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

Я и правда, привык больше энтерпрайзному стилю организации.

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

Полностью согласен со статьей. Сам тоже до такого подхода дошел и после этого стало гораздо проще ориентироваться в проектах.

Как уже заметили, статья о DDD. Одна беда — при таком подходе обязательно появляется модуль «Shared» в каком угодно виде. Хотя и имеет право на жизнь.
Потому что делать просто DDD в вакууме — сложно. Неплохо его соединять с другими техниками, навроде слоистой архитектуры. и тогда этот Shared (который безусловно нужен) просто отъезжает в правильный слой вместо доменного кода
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

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


Есть вариант ещё — часто используемые вещи выносить в отдельную папочку shared или common. В целом эта архитектура не является серебряной пулей. Скорее это некий компромисс (trade off) в сторону более запутанной абстракции, но зато с высокой связанностью логического контекста.

НЛО прилетело и опубликовало эту надпись здесь

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

Почему не храните? Я вот храню под кухонной мойкой, вместе с порошком, губками и тряпками.

… и ёршиком для унитаза.

А вы не так делаете? У вас всё лежит не на кухне, а на складах (ложек, кастрюль, и т.д.)?

На самом деле, интерес в том, что если у вас кухня производит борщи 24\7 — то оно так и случится. Просто обывательская кухня этим не занимается в таком режиме, там больше фишек и появляются доп. абстракции, что ложка для борща не просто существует и хранится, а берется из репозитория ложек, который имплементирован в виде ящика на кухне.
У этого даже название есть — Feature Folder structure, в противоположность Folders by Type
НЛО прилетело и опубликовало эту надпись здесь

Если возникает циклическая зависимость - это явный признак выделить общую сущность и сделать рефакторинг

Все так нахваливают, — идея, конечно, известная и интересная. Добавлю в эту бочку меда хоть кроху сомнения в бесподобности, потому как есть к ней вопросы. Надеюсь, не только у меня)
Во-первых, куда прикажете класть классы общие для нескольких абстракций? такие как mapper'ы в конце концов?)
Во-вторых, где по Вашей гениальной идее стоит размещать специфичные классы, которые могут и не относиться к конкретной абстракции доменной модели, такие как util? exception? и кучу других, которые используются не в рамках конкретной абстракции.
В-третьих, если сущностей будет, скажем, штук 30-50, удобнее по десяткам разных пакетов будет все время скакать?
Ну и не совсем понятно с интерфейсами, конфигами, различными служебными классами и прочим(всякое бывает, системы то разные).
Я это к чему, собственнно… Такая реализация, конечно, имеет право на жизнь, но лишь в специфичных случаях. И с оглядкой на то, что поддерживать подобное будет банально дороже, т.к. люди привыкли видеть «нормальное» распределение иерархии классов в проектах и на перестройку будет уходить время. Всему свое место и свое время. Утверждать, что-то вроде "… хватит организовывать код так, давайте так" — это что-то, мягко говоря наивное и нелепое, имхо
куда прикажете класть классы общие для нескольких абстракций? такие как mapper'ы в конце концов?)

Рядом с целевым классом.


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

Да прямо в корне.


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

Вполне. У меня в одном проекте и 200 директорий в корне.


не совсем понятно с интерфейсами, конфигами, различными служебными классами и прочим(всякое бывает, системы то разные)

Всё то же самое. И вот как раз бесят системы типа github actions которые требуют все свои конфиги совать в одно место. Ну, тут как пользователь ничего не поделаешь.


люди привыкли видеть «нормальное» распределение иерархии классов в проектах

Я много проектов повидал, но ещё не видел и пары с одинаковым расположением файлов.

> Рядом с целевым классом.
Ну вот есть классы по работе с БД. Куда их класть? Да, в модулях, которые по предметной области, мы объявим провайдеры данных, где пропишем нужные нам репы/источники, но сами классы этих источников то куда класть? Порой наблюдаю, что такие базовые общие вещи кладут в корне в папку core или general.

В корень и класть:


/db/mongo
/db/postgres
/db/redis

Тогда в корне будет мешанина из вот таких функциональных модулей и модулей предметной области. Какой-то странный гибрид =/

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

много проектов повидал, но ещё не видел и пары с одинаковым расположением файлов

Ну, из такого утверждения только один вывод: либо Вы пишете на каком-то крайне неформализованном языке, либо Вам крайне нефортило с очень сомнительными проектами в плане архитектуры…
Потому как, например, в крупных коммерческих Java-проектах, не то, что «пара с одинаковым расположением файлов», из 1000 рандомных, ручаюсь, 950 будут написаны в одном и том же стиле плюс-минус и даже с одинаковыми именованиями файлов, методов, переменных и, разумеется, пакетов.
У меня в одном проекте и 200 директорий в корне

Ну лично мне не хотелось бы иметь дело с таким кодом, извините…
Рядом с целевым классом.

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

Если вы про дата-маппер, то рядом с более высокоуровневым классом.


рыться в перемешанных на уровне корня служебных, утилитарных и вспомогательных классах, и перебирать сотни

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


С уважением

На ваше хамство мне, в принципе, плевать. Но врать-то зачем?

врать-то зачем

я искрене совершенно. Вы, вполне вероятно, хороший специалист, просто в каждом языке своя специфика, свои устои.
Посмотрел проект. Теперь понимаю, почему TypeScript никогда не войдет в серьезный бэк. Поддерживать и дебажить таким образом написанные программы невозможно без боли и невероятных временных и, как следствие, денежных трат.
ну это долго объяснять, если Вы с Java кодом не работали. Если вкратце: неудачное проектирование и несбалансированная структура расположения узлов программы приводят к неинтуитивности и неочевидности при поиске и вставке элементов, к необходимости менять, а не расширять, к невозможности переиспользовать код и, разумеется, к отсутствию желания читать код, как книгу, что не способствует приятной работе. Даже очень объемная программа может иметь компактный, слабосвязанный и что самое главное, читабельный для большинства людей (даже не программистов, возможно) код при желании

В приведённом мной репозитории ничего из описанного не наблюдается.

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

Например, прорабатываем обьем исключений. Раз, наклепал исключений на все случаи жизни. Или только те которые нужны сейчас. Отошел от «станка». Пошел к станку с предметной областью (другая ось) — наклепал там DTO-шек или POJO. Сдал смену. На следующий день повтыкал одно в другое. А если заболел, пришел другой коллега и собрал из твоих заготовок готовые фичи вместо тебя, потому что все предельно просто и машинально. Работа практически не требует никакого согласования, и идет весьма плавно. Объем задачи постепенно напоняется структурой решения. Всегда есть новые артефакты готовые к сдаче. Всегда есть о чем отчитаться. Работа идет прогрессивно, уровень детализации решения растет со временем. И коммиты при таком подходе обычно затрагивают один единственный файл. Что также способствует увеличению производительности. Не приходится разбираться во взаимосвязанности изменений.

Если думать об эффективном разделении труда, то всегда получается вполне сносная архитектура.

Но есть один недостаток — нужно продумать основные оси заранее. Потому что, если потом выяснится, что дискретизация была проведена неподходящим образом или вскроются какие-то новые оси которые не стыкуются со старыми, то переделывать придется все. И на это нужно время.

P.S.: Заметил, что чаще всего utils появляется, когда не хочется искать общий знаменатель в выполняемых этими функциями задачах и выделять новую разновидность класса. А потом быстро превращается в помойку для всякой всячины.

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

Почему у вас в структуре папок Folders by Type — entity находится за пределами model?
В общем-то очевидная вещь, которая приходит в голову всем, кто работал над большими монолитами, и думал, как организовать структуру проекта лучше.
было бы интересно посмотреть на большой группе людей, что выбешивает быстрее — разложенные механически по «слоям» файлы ИЛИ необходимость в 99% случаев для каждого файла создавать папку, где он — этот файл — будет там в гордом одиночестве — как в примере у автора

Не будет он в гордом одиночестве, ибо для каждого модуля нужны как минимум:


  • код
  • тесты
  • документация

Будет, ибо:


  • тесты лежат в отдельном проекте (так всегда в моём технологическом стеке);
  • документацию никто и никогда не пишет, особенно на код.
  1. Что за стек такой, что нельзя протестировать код, не перебирая несколько версий тестов?

  2. А, так мы говнокод тут обсуждаем? Я-то думал лучшие практики, раз речь зашла про правильное расположение файлов.

Что за стек такой, что нельзя протестировать код, не перебирая несколько версий тестов?

откуда взялись несколько версий тестов? речь о том, что в .NET для тестов отдельный проект заводится.


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

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

Из системы контроля версий, очевидно. Или у вас код сразу в камне выбивается?


Примерно в таком.

почему несколько версий тестов все-таки? пишется код, под него пишутся тесты (или наоборот), потом все вместе коммитится. про какие версии и камни речь?

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

разумеется никакого отдельного репозитория под тесты нет. все в одном репозитории. меняются требования — переписываются тесты — переписывается основной код.

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

  1. Не понял про версии тестов. Я про то, что тесты лежат совсем отдельно от основного кода, в отдельном проекте. Стек .NET.
  2. Почему отсутствие документации на код сразу делает его говнокодом? Слишком сильное утверждение.
    Мы вот, например, стараемся писать самодокументируемый код. Вполне себе не говнокод получается, но отдельного файла с документацией рядом с кодом нет.
    Да и вообще я такого никогда не видел.
НЛО прилетело и опубликовало эту надпись здесь

Путь к файлу — это естественный составной ключ. Только и всего. Суть проблемы не в дихотомии "тип против смысла", а в том, какие значения брать в качестве первого значения ключа, какие в качестве второго и так далее с целью оптимизации последующего поиска. И тут всё зависит от характера использования.

Как насчёт простого "Хватит организовывать код"?

Я давно заметил за собой, что никогда не пользуюсь деревом проекта, вместо этого есть go to definition, go to symbol, и в конце концов простой поиск по тексту.

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

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


Но в целом я вас поддерживаю, я обычно пользуюсь правилом "если ты можешь не создавать очередную директорию — не создавай её".

Но если точно знаешь какой файл надо открыть – зачем что-то куда-то пролистывать? Наверное во всех пристойных IDE/редакторах есть возможность открыть файл по имени, Ctrl-P/Cmd-Shift-O.

Каждому инструменту — своё применение. Уверен что в каких-то предметных областях больше подходит один принцип, в других — другой. Главное на первоначальном этапе определиться, как будет удобнее, и делать именно так.
В защиту «стекового» принципа могу сказать, что часто бывает такое, что, например, все вью-модели (или контроллеры, или сервисы, что угодно) могут использовать какую-то общую функциональность, которая больше нигде не нужна. В «стековом» варианте вы можете для этого сделать какой-нибудь internal класс, который больше нигде не светится. В вашем же подходе придётся делать ещё одну отдельную сборку (ох уж этот Common) и напихивать всё туда, и скорее всего туда рано или поздно в одну кучу свалится всё — какая-то исключительно View-related функциональность, какая-то исключительно ViewModel-related, потом кто-нибудь положит туда методы расширения для десериализации (ну а чо, коммон же) и понеслась.
какая-то исключительно ViewModel-related

Вот во /ViewModel такое и кладите, а не в /Common.

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

С чего бы? Самим вью-моделям во /ViewModel уже делать нечего. Ибо домен ViewModel ограничивается исключительно общими для всех вью-моделей вещами.

Действительно, это вариант.
Это все красиво на примере из трёх понятных доменных областей, в каждой из которой есть все слои. А когда их 20-30-50-100? У половины один набор слоёв, у другой половины другой набор слоёв.

Что проще поддерживать в одном стиле?
100 доменных областей? Это невозможно, они все разные, у них у всех будет разный набор файлов, в одном будут ServiceObject в другом FormObject, в итоге все они будут написаны по своему, по разному(((

Намного проще поддерживать
— 50 контроллеров API в одном стиле
— 70 моделей в одном стиле
— 90 services в одном стиле

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

Что проще сделать:
— открыть папку app/controllers/api/ и проверить/подправить в ней 30 файлов на предмет изменений
или
— пройтись по 70 папкам с доменной областью, проверить, есть ли в них контроллер который для API и если есть, то что-то проверить/подправить?

По мне так первое намного проще. Значит в первом случае проверить и подправить что-то намного проще. Тупо рутинной и ментальной работы меньше. А значит и поддерживаться все будет лучше.

А еще разделение на
— models
— controllers
— services
— routes
— views
одинаковое для всех веб-проектов и задается обычно фреймворком.
В этом его плюс. Открыл любой проект с MVC архитектурой — сразу ясно, что где лежит.

А разделение на доменные области — это поле для бескрайней фантазии, которое увы, обычно превращается в адский ад, и прийти на такой проект очень и очень сложно.
Что проще сделать:

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


И внезапно становится совершенно не важно, в каких директориях оно лежит.

одинаковое для всех веб-проектов и задается обычно фреймворком.
В этом его плюс. Открыл любой проект с MVC архитектурой

Да вот если бы… мне за последние 2 года попались уже 3 проекта на Ларавеле которые упорно пытаются делать в DDD не используя стандартные фичи.

Ну т.е. например в /models вот ети все репозитории и фактори вместо элокуента, command bus и в /controllers реквест\респонсы для нее, ну и прочее подобное везде от чего мозг немного ловит клин ибо «дакокойващесмысоластанавитес*. Естественно все 3 все по разному понимали _как правильно_ и имели разную структуру))
А потом, я понял причину — в последние несколько лет студентам\джунам\накурсах_вайти начали форсить DDD из всех щелей, ну и покатился сноубол хайпа
Как выглядит смысловая разбивка давно описано в DDD. Вот пример, где даже слишком, слегка.

Для не очень больших проектов я использую смысловое разбиение с акцентом на реиспользованиe. Т.е. Я беру вот эту одну папку и переношу ее в другой проект почти без изменений.

Спасибо, но не надо.
При разбиении по принципу per layer проще контролировать сохранность архитектуры – репозитории (DAL-уровень) находятся в своём проекте, сервисы (BLL) – в своём, контроллеры (PL) – в своём. Отсутствие протечек можно контролировать на код-ревью (например, через namespaces), можно и архитектурные тесты написать (ищите выступления Дениса Цветциха).
Если файлы из разных архитектурных слоёв будут свалены в одну папку, то уровни не то что протекут – всё просто хлынет. Гарантировано.
А вот в рамках одного уровня (например, бизнес-логики) можно и по принципу per feature всё складывать, здесь проблем не вижу.

Разве то что вы описали в статье не является разделением по модулям?

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

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

Забавно, но несколько месяцев назад я прошел через это.


Запускал один проект и долго не мог выбрать стиль организации файлов. Спрашивал совета по форумам/чатикам, но ответа не получил. В итоге я выбрал, как его назвал автор, "стековый" стиль, но уже через несколько месяцев всё переделал и перешел на "смысловой".


Причина проста: с "стековым" стилем банально сложнее работать. Реальность такова, что нам очень редко нужен только один файл. Чаще всего редактируется несколько файлов паралельно. И зачастую редактируемые файлы как-то логически связаны, связаны по смыслу. Скажем, вы меняете одну страницу сайта, скорее всего вы правите html, css, js возможно поправляете какие-то тесты для этой страницы и так далее. И намного удобнее, когда в редакторе кода, всё что относится к этой странице находится под рукой. Иначе приходится постоянно скролить список файлов вверх и вниз.

per-feature + package-by-layer
НЛО прилетело и опубликовало эту надпись здесь
Недавно была целая встреча, посвященная этой теме в контексте PHP. Как раз разбирали разные подходы к организации файлов в проекте и провели дискуссию об их качествах. Если кому интересно, то вот видео: youtu.be/mZeQ-MGTsJk

Для себя сделал вывод, что package-by-feature хорошо подойдет для работы с большими монолитами, где много доменов. Вместе с тем, микросервис, у которого домен один наверное проще будет написать по «стандартной» схеме.

Собственно доведенная до абсолюта схема с разделением по доменам, позволяет одним легким движением вырезать домен в отдельный микросервис и поменять реализацию интерфейсов по взаимодействию с ним на сетевое общение. И это плюс, потому что из монолита, структурированного по package-by-type сложно вырезать сервис, если его функциональность размазана по всему проекту.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории