Search
Write a publication
Pull to refresh

Comments 16

Как все сложно... а проще нельзя структурировать?

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

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

Моки.

для моков интерфейс не нужен. не скажу за другие ЯП, но в Java всё прекрасно мокается без использования интерфейсов

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

пример
public class MyService {
  
  private final DbClient dbClient;

  public void doSomething(Request request) {
    Long id = dbClient.getLastId();
    dbClient.update(id, request);
  }
}

public class DbClient {

  public Long getLastId() {
    //запрос в БД
  }

  public void update(Long id, Request request) {
    //запрос в БД
  }
}

public class MyServiceTest {

  private MyService myService;

  public void doSomethingTest() {
    DbClient dbClient = mock(DbClient.class);
    when(dbClient.getLastId()).thenReturn(1);
    doNothing().when(dbClient.update(any(), any()));

    Request request = createRequest();
    myService.doSomething();

    verify(dbClient).getLastId();
    veryfy(dbClient).update(eq(1), eq(request));
  }
}

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

Я знаю, как это можно сделать. Я утверждаю, что вы ломаете контракт (в данном примере контракта вообще нет, но обычно он есть).

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

Ну представьте, что вы ходите в базу потенциально из пяти (ста) разных потоков, и вам нужно проверить, что только один правильный поток вызывается при вызове myService.doSomething(). Тут 2025 год на дворе, моки должны легко и внятно уметь тестировать параллельное выполнение.

Я утверждаю, что вы ломаете контракт (в данном примере контракта вообще нет, но обычно он есть)

что вы подразумеваете под контрактом?

То, что описано вот тут (под заголовком «Необходимость явных контрактов», прямую ссылку дать не могу из-за того, как хабр рендерит маркдаун).

В ООП это чаще всего реализуется интерфейсом.

Проблема с кодом выше в том, что при изменении имплементации DbClient без изменения его внешнего API тесты могут запросто поломаться, и с увеличением сложности проекта, увеличивается связность — а следовательно, снижается легкость сопровождения.

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

при изменении реализации DbClient сломается интеграционный тест. мок не сломается, т.к. его поведение задаётся в тесте

Спасибо за статью.
Часто бизнес-логика представляет собой последовательность вызовов методов, как например операция "Создание заявки на кредит". Как это может быть реализовано в предложенном вами подходе? Что думаете про использование паттерна фасад для выделения законченного бизнес сценария?

  1. если такая операция вызывается только извне, создаётся обработчик для неё (handler/operation), последовательно вызывающий шаги (service)

  2. можно использовать движок бизнес-процессов, где "создание заявки на кредит" выделено в отдельный процесс, который можно переиспользовать. и вызываться он будет непосредственно движком БП.

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

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

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

Типовой подход для пакета service состоит в разбиении логики на 2 части - application logic и domain logic. Элемент функционала application logic может обращаться к domain logic и не может делать вызовы к другим элементам application logic. Domain logic состоит из доменных сервисов и агрегатов/сущностей домена. Доменные сервисы обычно используются когда надо работать одновременно с более чем одним агрегатом. Всё это детально описано в огромном количестве литературы по domain driven design.

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

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

Функционал application logic взаимодействует не только с доменной логикой, но и другим блоком функционала, который работает с внешними источниками данных.

Sign up to leave a comment.

Articles