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

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

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

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

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

А так, использовать или нет контракты - зависит от команды, как привыкли, или из-за чего проблемы возникают.

Плюс, если есть БА(прямо как йети) в команде - при некоторых случаях изучения работы кода, контракты помогут.

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

Это навряд ли

А вообще мой посыл в том, что не надо лепить интерфейсы и абстракции просто потому, что в умных книжках так написано.
Абстракции должны изолировать потенциальные точки отказа приложения и обобщать взаимодействие с множеством реализаций. Кстати про умные книжки, давече читал "Эволюционная архитектура" и там как раз было про соотношение абстракций к реализациям - у авторов мнение (там есть какая-то формула для подсчета этого добра), что ок это примерно 1:3-1:4 соответственно, с чем я вообщем-то согласен.
А пилить интерфесы вроде IUserService и IUserRepository ну максимально бессмысленно - других реализаций для них (почти 100%) не будет, поэтому смысла скрывать реализацию нет; смене контракта интерфейс не помешает; это даже не абстракция, просто интерфейс ради интерфейса

Интересно узнать про соотношение, спасибо за рекомендацию)

Сама постановка задачи некорректная.

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

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

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

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

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

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

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

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

Не знаю какую Вы закладывали логику для получения/создания пользователей, но так как Вы не написали пример то приходится додумывать самому что там написано. А потому могу сказалить лишь то, что максимум какая логика тут может быть описана, так это обработка входящих параметров (условия выборки, пагинация и т.д.) в зависимости от выбранного хранилища и клиента для него. При этом в контракте эти параметры должны быть описаны так, чтобы в любой момент можно было подменить как хранилище, так и клиента (сомневаюсь, что это частая практика и тем не менее, вдруг с TypeOrm решили перейти например на Kysely).

3) Отсутствие комментариев
С решением согласен, а вот пример функции тригернул.

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


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

4) Неправильное управление исключениями.
Вот тут крайне спорная ситуация. Если же с Вашим бекэндом общаются только через TCP, то решение кинуть HttpException в методе SomeService имеет место быть, иначе управление исключениями явно должно быть иное.

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

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

Про третий пункт - ну пример жеж, смысл в описании флоу функции, плюс мнение на реализацию у каждой команды будет свое.

Про исключения не понял проблемы 😞.

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

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

Например (mermaid)

Hidden text

graph LR

  AppModule-->ResourcesModule

  ResourcesModule-->ResourcesPresentersModule

  ResourcesPresentersModule-->ResourcesHttpApiModule

  ResourcesHttpApiModule-->ResourcesApplicationModule

  ResourcesApplicationModule-->ResourcesDomainModule

  ResourcesDomainModule-->ResourcesRepositoryModule

  ResourcesApplicationModule-->TagsApplicationModule

  TagsApplicationModule-->TagsDomainModule

  TagsDomainModule-->TagsRepositoryModule


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

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

Hidden text
// Репозиторий
@Injectable()
export class UserRepository implements IUserRepository {
    constructor(
       @InjectRepository(User) private readonly userRepository: Repository<User>,
    ) {}
    async findAll(): Promise<User[]> {
        // Логика для получения всех пользователей
    }
    async findOne(id: string): Promise<User> {
        // Логика для получения пользователя по ID
    }
    async create(createUserDto: CreateUserDto): Promise<User> {
        // Логика для создания нового пользователя
    }
}

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

Потому, думаю, что можно было написать +- что-то подобное. Как раз дальше показав, что у Вас в репозитории может использоваться хоть ORM, хоть чистый SQL, но они оба следуют одному контракту и всё что требуется для подмены реализации, так это только описать саму реализацию и подменить на нужную в модуле. Но в моем примере будет еще и бизнес слой затронут, так как всё таки для разных клиентов where и relations будут написаны по разному.

Hidden text
export interface IUserRepository {
    findAll<W = Record<string, unknown>, R = Record<string, unknown>>(args?: { limit?: number, page?: number, where?: W, query?: string, spleen?: string, relations?: R }): Promise<User[]>;
    findOne<W = Record<string, unknown>, R = Record<string, unknown>>(args: { where: W, relations?: R }): Promise<User>;
}

// Репозиторий
@Injectable()
export class UserPrismaRepository implements IUserRepository {
  constructor(private readonly client: PrismaClient) {}

  findAll<W = Record<string, unknown>, R = Record<string, unknown>>(args?: { limit?: number, page?: number, where?: W, query?: string, spleen?: string, relations?: R }): Promise<User[]> {
    // Логика обработки входящих аргументов для построения запроса
    return this.client.user.findMany({
      where,
      //...other
    })
  }
  
  findOne<W = Record<string, unknown>, R = Record<string, unknown>>(args: { where: W, relations?: R }): Promise<User> {
    // Логика обработки входящих аргументов для построения запроса
    return this.client.user.findOne({
      where,
      //...other
    })
  }
}

// Репозиторий
@Injectable()
export class UserTypeOrmRepository implements IUserRepository {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>
  ) {}

  findAll<W = Record<string, unknown>, R = Record<string, unknown>>(args?: { limit?: number, page?: number, where?: W, query?: string, spleen?: string, relations?: R }): Promise<User[]> {
    // Логика обработки входящих аргументов для построения запроса
    return this.usersRepository.findMany({
      where,
      //...other
    })
  }
  
  findOne<W = Record<string, unknown>, R = Record<string, unknown>>(args: { where: W, relations?: R }): Promise<User> {
    // Логика обработки входящих аргументов для построения запроса
    return this.usersRepository.findOne({
      where,
      //...other
    })
  }
}

Про третий пункт - ну пример жеж, смысл в описании флоу функции, плюс мнение на реализацию у каждой команды будет свое

Да тут я вообще зря быканул)

Про исключения не понял проблемы 😞.

В таком варинте, совершенно никакой проблемы. Тут хоть в репозитории киньте HttpException (только это вообще не его ЗО), установите глобальный фильтр исключений и всё будет нормально

Hidden text
graph LR
UsersHttpController-->UserApplicationService-->UsersDomainService-->UsersRepository

Но в таком варианте глобальный фильтр уже не подойдет. На каждую точку входа (транспорт) нужно писать отдельный фильтр, потому что форматы ответа будут отличаться. Если быть точнее, то можно сделать "дефолтный" глобальный фильтр для HTTP, а на RMQ контроллер кинуть другой фильтр и тогда проблем не будет.

Hidden text
graph LR
UsersHttpController-->UserApplicationService
UsersRmqController-->UserApplicationService
UserApplicationService-->UsersDomainService
UsersDomainService-->UsersRepository

Спасибо за разбор, в следующих статья буду более осмысленные примеры делать, проба пера, как говорится)

Простите, но не могу молчать, для меня тема комментариев в коде весьма острая)

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

Указывать типы аргументов и возврата в jsdoc при использовании ts у меня вызывает только один вопрос - зачем?) Написать бесполезную строку, что переменная email это мыло пользователя? А description зачем там же? Сделайте функцию с соответствующими названием и ваш дескрипшн никто и никогда не прочитает. JSdoc считаю оправданным для более сложной информации вроде примеров использования или ссылок на доку.

Чтобы избавится от комментов и не потерять понятности, нужно соблюдать несколько правил:

  • Писать небольшие функции, которые делают только то, что сказано в названии

  • Соблюдать в каждой функции один уровень абстракции

  • Давать говорящие имена сущностям

Этого уже будет достаточно, что бы избавится от практически любого коммента

но тестирования, хотя бы юниты, обязаны быть.

может хотя бы e2e вы хотели сказать?.)

if (!user) {
    throw new NotFountException({ message: 'Пользователь не найден!' });
}

не найденный пользователь - это разве исключительная ситуация? вполне себе обычная..

Есть подозрение что NotFount - это в целом про качество материала

Разве «Выбросили исключение, затем либо наш, либо дефолтный фильтр подберет и подставит ххх код ответа, полезную нагрузку» не будет намного лучше, чем нагружать бизнес логику самостоятельным пробрасыванием данных и требуемого кода ответа с последующим проставлением в объекте ответа?

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

Публикации

Истории