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

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

Стоило бы цитаты из/про шаблоны проектирования перевести на русский.

Добавил переводы, спасибо

В этом случае повысить качество абстракции мне помогает другое правило - я не пользуюсь префиксами/суффиксами I/Impl/Abstract/Default и им подобным. Интерфейсы я называю абстрактно, а в классы реализации добавляю что-то (прилагательное, название технологии и т.п.), характеризующее суть реализации.

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

1) Слой хранения. В нём, например, класс UserEntity (1) и интерфейс IUsersRepository (2) для работы с пользователями в БД. Плюс много других сущностей, сделанных по той же схеме.

2) API бизнес-логики. Интерфейс IUsersService (3), который чуть менее чем полностью повторяет IUsersRepository. Дефолтная реализация этого интерфейса NoOpUsersService (4). DTO-класс User (5), который содержательно практически идентичен UserEntity. UserMapper (6) между UserEntity и User.

3) Реализация бизнес-логики. Класс UsersService (7).

4) Контроллеры. Класс UsersController (8), интерфейс которого практически полностью повторяет IUsersRepository и IUsersService.

Итого 8 классов и интерфейсов в 4 разных Java-проектах, если я ещё чего-то не забыл. Это для каждой сущности, понятно что сущностей больше. Конечно такой подход имеет право на существование. Но, блин, вероятность того, что пользователи и другие сущности будут храниться не в реляционной БД, а в текстовых файлах, мировом эфире или где-то ещё конечно не нулевая, но очень близка к этому.

Я конечно понимаю смысл DTO-классов и сам их использую в некоторых проектах. Но тут мы просто нафиг всё это выкинули, оставили Entity-классы и репозитории. А те же контроллеры генерятся Spring'ом. Безусловно теряется какая-то гибкость, но как минимум в 4 раза сокращается количество кода и сокращается время на написание, чтение и сопровождение всего этого. Если в будущем дефолтной Spring'овой реализации будет недостаточно, то в принципе можно вернуться к исходному подходу, но весь этот код совершенно точно не должен писаться руками. Мы для такого обычно используем кодогенераторы, но это уже холиворная тема.

как минимум в 4 раза сокращается количество кода

Это главное. Лучший код — тот, который не написан: его не надо читать, поддерживать, тестировать

Считаю наоборот, строгие паттерны в наименовании сущностей — это хорошо.

У инструментов JetBrains есть универсальный переход по имени, и там очень удобно видеть, что тебе нужно открыть: IService или ServiceImpl, или UserDTO, или UserVM, сразу видно, к какому слою проекта относится символ.

Некоторые миддлы на проекте вообще не знают английский, и чтобы что-то назвать, идут в переводчик, и рождается SendAccount вместо BillSender. Другая проблема: начинающие (некоторые всю жизнь не могут перерасти этот этап) разработчики, когда выдумывают наименование, видят очень узкий контекст и не думают, как это наименование встроится в проект. То есть, сервис рассылки могут назвать просто SendService, не уточняя — рассылки чего именно.
И если тут не будет стандартных префиксов/суффиксов, можно сразу застрелиться, выбирая между наименованиями, порожденными сумеречными разумами, что они хотели этим сказать: Send — это сервис, DTO, или интерфейс?

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

Я с вами частично согласен, частично нет.

С одной стороны, против *Controller, *Repo, *DTO я ничего не имею.

С другой стороны *Service - ну как-то так... Не ООП-но и в итоге это часто превращается ПП-подход с процедурами внутри сервисов и структурами данных в сущностях.

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

*Service - плохая практика, так как мотивирует напихать туда всего, так же как и *Manager и подобное. Если не получается подобрать подходящее имя класса (например PostCreator, ProductFinder и т.д.), то сразу возникает мысль, не нарушается ли здесь SRP?

Из суффиксов Controller и Repository по крайней мере можно понять домен использования, поэтому оно больше полезно, чем вредно.

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

Лично я для себя определил следующие пункты:

1) Совершенно точно в рамках одного проекта должна использоваться одна схема именования, в этом все сходятся.

2) Названия классов, полей, методов, таблиц, столбцов, объектов, отношений и т.п. на английском языке должны соответствовать правилам английского языка. В английском главное/определяемое слово ставится в конце, а дополнительные/описывающие слова ставятся в начале. Например, DocumentChangeEventHandler. В русском языке порядок слов часто обратный: обработчик событий изменения документов, поэтому если используется калька с русского языка, то может начаться какой-то хаос. А если к этому добавить ещё технические суффиксы и префиксы, то хаос усиливается ещё сильнее. Гораздо проще запомнить одно правило, что английские названия пишем по-английски и всё.

3) Иногда есть смысл вести словарь каких-то основных терминов. Например, сервисные классы иногда называют SomethingHelper, SomethingService, SomethingUtil, SomethingExtension, ..., плюс то же самое, но во множественном числе. Можно зафиксировать, что обычные сервисы (которые подключаются через DI, у которых может быть несколько реализаций) мы называем Service, а просто классы с какой-то вспомогательной функциональностью (например, для строк, для потоков, ..., классы у которых точно не будет нескольких реализаций, которые не будут подключаться через DI, которые в C# обычно реализуются через расширения) - называем, например, Helper.

Пункт 2 и 3 как-раз и дают стандартные суффиксы и префиксы. Например, всё что относится к документам скорее всего будет начинаться со слова Document (или содержать это слово где-то близко к началу - например, AbstractDocumentEventHandler), а все обработчики событий скорее всего будут заканчиваться на EventHandler. Но это не какие-то искусственные, технические суффиксы и префиксы, которые на каждом проекте будут свои и будут выносить мозг не русскоязычным разработчикам, а вроде достаточно естественные, универсальные и понятные.

Причем, префикс I у интерфейсов просто не имеет смысла. Почему бы тогда у классов не делать префикс C, у таблиц - T, у полей и столбцов - префикс в зависимости от типа данных. На мой взгляд, это какие-то технические детали реализации в названии, которые а) не приближают людей к пониманию назначения этого объекта б) вытаскивают на верхний уровень детали реализации. Например, сейчас эта штука реализована как класс, а потом мне захочется превратить её в интерфейс. И что, теперь всё переименовывать? Или какая мне разница, например, значение в переменной "расстояние до объекта" хранится в виде целого числа, или с одинарной, с двойной точностью. А что если я в будущем захочу изменить этот тип?

К тому же, IDE обычно и так показывает тип объекта, зачем это тянуть ещё и в название.

Насчет Impl - ну, это масло масляное. Все классы что-то реализуют. Или какое-нибудь слово Default - хотелось бы больше деталей. Например, мы реализуем какой-нибудь сервис получения настроек приложения. Название EnvironmentSettingsService звучит понятнее, чем DefaultSettingsService. Оно отражает, что настройки берутся из переменных окружения, в будущем могут появиться альтернативные реализации, например, DatabaseSettingsService. К тому же название DefaultSettingsService не очень устойчивое к изменениям. А что если в будущем дефолтной будет другая реализация сервиса?

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

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

4) Мы стараемся не использовать сокращения и аббревиатуры. Если только какие-то общепринятые или добавленные в словарь (всё тот же пункт 3 выше).

5) В аббревиатурах только первую букву делаем заглавной, остальные строчными. Это обычно более удобный вариант для IDE. В них заглавная буква считается началом слова, можно перемещаться по словам, можно искать по словам. И для людей это более читаемо, иначе может получиться какое-нибудь EAEUCDXML - сложно понять что это значит, где какое слово начинается, нужно совершать усилия при чтении.

Плюс у нас есть ещё куча разных рекомендаций по именованию CRUD методов и т.п.

Насчет того, что джунам сложно называть классы:

1) Если есть четкие рекомендации типа пункта 2 выше, то всё становится на много проще. Даже если человек не знает английского, это правило легко усвоить.

2) Часть вещей может проверяться Checkstyle и аналогичными инструментами.

3) Более сложные вещи проверяются в рамках код-ревью.

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

Причем, префикс I у интерфейсов просто не имеет смысла
Смотря с чем работаешь. С одной стороны, если в метод приходит Dog, с точки зрения использования, должно быть без разницы, класс это или интерфейс, лишь бы у него был метод Bark(). С другой стороны, на типичном энтерпрайзе, мы в основном только и делаем, что гоняем данные туда-сюда, и тут очень важно, финальный класс это или абстрактный класс, или интерфейс, потому что как только я принимаю решение метод вынести в REST (или любой другой RPC) сервис, становится важно, как я могут это сериализовать/десериализовать, нужны ли мне обёртки для этого, или я могу сразу Dog кидать. Также становится важно, Dog — это класс репозитория или модели, чтобы я его не передал выше области действия транзакции.
Почему бы тогда у классов не делать префикс C, у таблиц — T, у полей и столбцов — префикс в зависимости от типа данных
Как по мне, прекрасная практика в SQL все вьюшки называть на букву v, уже не попытаешься их апдейтить. Суффиксы с типом данных тоже очень неплохо выглядят
create table Cities (CityId int, CityName nvarchar(max), CountryId int);
Если заканчивается на Id — это код какой-то сущности, заканчивается на Name — имя сущности.
какая мне разница, например, значение в переменной «расстояние до объекта» хранится в виде целого числа, или с одинарной, с двойной точностью. А что если я в будущем захочу изменить этот тип?
Это да, Венгерскую нотацию тоже считаю вредной.

Суффиксы с типом данных тоже очень неплохо выглядятcreate table Cities (CityId int, CityName nvarchar(max), CountryId int);

Эти названия вполне соответствуют пунктам 2 и 3 из моего предыдущего комментария. Если добавить пробелы, то это будут обычные названия в соответствии с правилами английского языка: city identifier (id - это общепринятое сокращение), city name, country identifier. Т.е. это не просто слова с техническими суффиксами/префиксами (city_char, cCity, iCountry, intCountry, idCountry, fk_country), а осмысленные названия.

Есть стандарты ISO/IEC 11179, ISO 7372 (UNTDED), UN/CEFACT CCTS, ISO 20022, OASIS UBL, NIEM и многие другие. Практически везде там используется примерно такая схема именования. Названия свойств обычно заканчиваются на Id, Name, Date, Amount, ...

Также становится важно, Dog — это класс репозитория или модели, чтобы я его не передал выше области действия транзакции.

Да, здесь как минимум два класса, которые можно было бы назвать Dog. Это JPA-сущность и DTO-класс. Скорее всего они будут лежать в разных пакетах, по которым можно понять что это за класс, но с другой стороны, конечно лучше им дать разные названия, например, DogEntity и DogDto. Но опять-таки это ничему не противоречит. DogEntity - это сокращение от dog persistence entity. DogDto - сокращение от dog data transfer object. Т.е. это осмысленные названия на английском языке. Вот, если бы были DtoDog или DogJPA, то у меня возникли бы вопросы.

Скорее всего они будут лежать в разных пакетах, но с другой стороны, конечно лучше им дать разные названия, например, DogEntity и DogDto
Комбинируя это с п.1 ваших правил, получается что все entity в проекте должны заканчиваться суффиксом *Entity.
Так и до Service недалеко )))
Лучше всё единообразно LoginService, чем каждый раз удивлятся изобретениям типа краткого Login (это сервис или dto, или вообще интерфейс?) или Gatekeeper — для сервиса входа юзера в приложение.

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

Зато придуманы юнит-тесты. Нужен тест, который пишет в базу через один контекст и затем читает через другой. Тогда в сценарии из первого эпизода ошибка не попадет в продакшен

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

Не уверен, что правильно вас понял, но возможно есть такой язык - Haskell. Если дали IO-монаду - надо сохранять, не дали - не надо:)

Зато придуманы юнит-тесты

У меня в черновике была заметка про тесты, но я решил убрать её, чтобы не размывать фокус поста:)

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

После этого случая он стал сторонником детроицкой школы тестирования и больше у него таких проблем не будет:)

есть такой язык

Спасибо, буду знать

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории