Дизайн разделения бизнес-слоя (логического) на два уровня (физических)
От: zelenprog  
Дата: 23.05.24 07:36
Оценка: 4 (1)
Снова всем Здравствуйте!

Я продолжаю "бороться" с архитектурой приложения.
Суть архитектуры приложения в целом понятна — надо разделить приложение на логические слои:
— UI — пользовательский интерфейс,
— Model — модель, бизнес-логика, бизнес-слой,
— Infrastructure — это все низкоуровневые детали типа базы данных и т.д.

А вот многие детали реализации архитектуры мне непонятны в связи с недостатком опыта.
Возник следующий вопрос.

Кроме деления приложения на "логические" слои, приложение делится еще и на "физические" уровни.
Уровень, грубо говоря, — это физический компьютер, на котором выполняется программный код.
"Уровень" — это возможно не совсем точный термин.
Но вот в этой статье от Microsoft применяется именно термин "уровень":
https://learn.microsoft.com/ru-ru/azure/architecture/guide/architecture-styles/n-tier

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

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

Как это реализуется?
Как построить правильную архитектуру (правильный дизайн) такого разделения слоя?

Тут проблема в следующем.
Объекты слоя бизнес-логики должны обращаться только к другим объектам бизнес-логики. Они не должны ничего знать про инфраструктуру. Однако, чтобы "связать" две части бизнес-логики, расположенные на разных ПК (на разных уровнях), нужно использовать низкоуровневое взаимодействие через сеть.
Получается, что если один объект BusinessObject1, расположенный на ПК-1, хочет создать экземпляр BusinessObject2, который реализован на ПК-2, то BusinessObject1 просто вынужден "знать" про инфраструктуру, чтобы запросить через сеть (например, через REST-запрос) у другого ПК экземпляр BusinessObject2.

Как-то это неправильно. Или я чего-то не понимаю.

Объясните, пожалуйста, как это делается?
Интересует хотя бы примерная схема\дизайн решения такого взаимодействия "внутри" одного слоя через сеть.
Re: Дизайн разделения бизнес-слоя (логического) на два уровн
От: vsb Казахстан  
Дата: 23.05.24 08:00
Оценка: 5 (3) +1
Лично я считаю немного по-другому. Тут немного абстрактно опишу, извиняюсь заранее.

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

Теперь вернёмся к делению и группировки кода. В конечном итоге у нас имеется код на каком-то языке программирования, состоящий из строк. Эти строки объединяются в функции, функции могут объединяться в классы и тд. Какие-то строки будут расположены рядом. К примеру методы одного класса обычно будут в одном файле. Может быть даже идти друг за другом. Какие-то строки будут далеко друг от друга. К примеру один сервис вызывает другой. Код, который вызывает через HTTP Client, формирует запрос и тд находится в одном репозитории, код, который обрабатывает этот запрос — находится в другом репозитории.

Во время написания кода архитектура не особенно важна. Написать как-то работающую программу можно вообще в одну функцию зачастую. После того, как программа написана и поставлена в условный релиз, она переходит в стадию развития и сопровождения. В этой стадии бывают багфиксы, тут не очень интересно. А самое интересное это развитие. Когда к изначальные требования меняются. Убираются старые требования, добавляются новые, изменяются. Каждое требование это какая-то условно атомарная задача. Ну или его можно разделить на эти атомарные задачи. Как правило, при взятии задачи в работу такое деление будет произведено. После того, как задача выполнена, можно сформировать diff между кодом "до" и "после". Иными словами понять, какой код изменился.

И вот теперь приходим к оценке архитектуры. Мой главный принцип — при реализации изменений в ТЗ, код должен меняться как можно более локально. Т.е. приходит требование, мы его реализуем и в идеале код меняется в одной функции. Или в одном классе. Или в одном пакете. Или хотя бы в одном сервисе.

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

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

Теперь что касается слоёв. Вообще слои зависят от используемых фреймворков. К примеру если мы работаем с SQL, без ORM, слой для доступа к БД весьма желателен. Ну сложно без него, каша будет. Но этот слой опять же вполне может быть прямо внутри фичи. А где-то можно и без него обойтись, если код простой. Усложнить всегда успеем, extract to method есть в любой IDE. А начинать надо с простого кода, даже если там смешиваются концепции, ничего страшного. Куда хуже писать hello world в enterprise стиле. А если мы работаем с ORM, то вполне можно посчитать, что код ORM это уже и есть наш Data Abstraction Layer. И городить ещё один, в котором методы будут состоять в основном из одного вызова соответствующего метода ORM, это привнесение сложности на ровном месте.

Также хочу ещё заметить, что считаю весьма полезным максимальную изоляцию интерфейсов между компонентами в любом виде. Вот как иногда делают — сделали Entity-класс, отражающий таблицу из БД, навесили на него аннотаций, а потом берут и его же в JSON и отдают фронту. Вот это плохо. Правильно сделать пусть даже на сегодня полную копию этого класса, но отдельную. Да, придётся немножко boilerplate кода написать, копирующего одно в другое, ничего страшного. Если сильно противно — можно автоматизировать. Но связь между backend и frontend должна быть независимой от связи между backend и database. А если вы уверены, что она не должна быть независимой, зачем вы вообще пишете код, возьмите PostgREST.

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

Однозначное правило только одно — однозначных правил нет. Всегда и везде есть плюсы и минусы. Как говорится — there's no silver bullet. Поэтому спрашивать про правильную архитектуру в отрыве от конкретной задачи — смысла просто нет.
Отредактировано 23.05.2024 8:05 vsb . Предыдущая версия . Еще …
Отредактировано 23.05.2024 8:04 vsb . Предыдущая версия .
Отредактировано 23.05.2024 8:04 vsb . Предыдущая версия .
Re: Дизайн разделения бизнес-слоя (логического) на два уровня (физических)
От: r0nd  
Дата: 23.05.24 10:22
Оценка: 6 (1)
On May 23, 2024, 10:36 AM, zelenprog <140063@users.rsdn.org> wrote:

Z>Объясните, пожалуйста, как это делается?


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

Основные концепции

  1. Сервисы: Используйте концепцию сервисов для разделения функциональности. Каждый сервис будет отвечать за определенную часть бизнес-логики и будет размещен на соответствующем сервере (физическом уровне).
  2. API и протоколы: Взаимодействие между сервисами будет происходить через хорошо определенные API. REST, gRPC или другие сетевые протоколы могут быть использованы для взаимодействия.
  3. Шаблон "Фасад": Используйте шаблон "Фасад" для скрытия деталей взаимодействия с инфраструктурой. Фасад будет предоставлять высокоуровневые методы для бизнес-логики и управлять сетевыми взаимодействиями за кулисами.

Примерная схема решения

Определение сервисов:
Фасады для взаимодействия:

Пример реализации

Service1Facade на ПК-1

public class Service1Facade {
    private Service1 service1;

    public Service1Facade(Service1 service1) {
        this.service1 = service1;
    }

    public BusinessObject2 getBusinessObject2(int id) {
        // Взаимодействие с Service2 через сеть
        String url = "http://service2.example.com/api/businessObject2/" + id;
        BusinessObject2DTO dto = RestClient.get(url, BusinessObject2DTO.class);
        return mapToBusinessObject2(dto);
    }

    private BusinessObject2 mapToBusinessObject2(BusinessObject2DTO dto) {
        // Маппинг DTO в бизнес-объект
        return new BusinessObject2(dto.getId(), dto.getName(), dto.getValue());
    }
}

Service2 на ПК-2

@RestController
@RequestMapping("/api/businessObject2")
public class Service2 {
    @GetMapping("/{id}")
    public BusinessObject2DTO getBusinessObject2(@PathVariable int id) {
        // Логика получения BusinessObject2
        BusinessObject2 obj = businessLogic.getBusinessObject2ById(id);
        return mapToDTO(obj);
    }

    private BusinessObject2DTO mapToDTO(BusinessObject2 obj) {
        // Маппинг бизнес-объекта в DTO
        return new BusinessObject2DTO(obj.getId(), obj.getName(), obj.getValue());
    }
}


Объяснение

  1. Фасад: Service1Facade предоставляет методы для взаимодействия с Service2. Он скрывает детали сетевых взаимодействий, предоставляя бизнес-логике высокоуровневые методы.
  2. API: Service2 на ПК-2 предоставляет REST API для доступа к своим бизнес-объектам. Это позволяет Service1 на ПК-1 запрашивать данные через HTTP.
  3. DTO: Использование Data Transfer Objects (DTO) для передачи данных между сервисами. Это обеспечивает слабую связанность и инкапсуляцию данных.

Преимущества

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

❧ “Strength is keeping it together when everyone expects you to fall apart.” — Chris Bradford
Re[2]: Дизайн разделения бизнес-слоя (логического) на два ур
От: zelenprog  
Дата: 23.05.24 13:08
Оценка:
R>Для решения проблемы разделения слоя бизнес-логики на два физических уровня, необходимо использовать архитектурные подходы, которые позволяют скрыть детали низкоуровневой инфраструктуры и при этом обеспечивают необходимое взаимодействие между частями бизнес-логики. Вот примерная схема и дизайн решения такого взаимодействия ...

Спасибо! Идею понял.

R>
R>public class Service1Facade {
R>    private Service1 service1;

R>    public Service1Facade(Service1 service1) {
R>        this.service1 = service1;
R>    }

R>    public BusinessObject2 getBusinessObject2(int id) {
R>        // Взаимодействие с Service2 через сеть
R>        String url = "http://service2.example.com/api/businessObject2/" + id;
R>        BusinessObject2DTO dto = RestClient.get(url, BusinessObject2DTO.class);
R>        return mapToBusinessObject2(dto);
R>    }

R>    private BusinessObject2 mapToBusinessObject2(BusinessObject2DTO dto) {
R>        // Маппинг DTO в бизнес-объект
R>        return new BusinessObject2(dto.getId(), dto.getName(), dto.getValue());
R>    }
R>}
R>


Получается, что мы сформировали BusinessObject2 на втором физическом уровне-2, "упаковали" его в DTO, передали на уровень-1, и "распаковали".
То есть на уровне-1 есть класс, реализующий функциональность BusinessObject2.
И далее на уровне-1 мы уже работаем с ним как с обычным локальным бизнес-объектом.
Получается, что в данном примере на уровень-2 вынесено только создание\инициализация объекта. Как я понял, в этом случае предполагается, что и на уровне-1 и на уровне-2 есть одинаковая реализация класса BusinessObject2.

А что делать, если функциональность BusinessObject2 поделена на два уровня? Или вообще вся его функциональность находится на уровне-2?
То есть обращаясь к какому-либо методу BusinessObject2 на уровне-1, фактически должна происходить переадресация на уровень-2.

public class Service1AnyController {
    private Service1Facade mService1Facade;

    public Service1AnyController(Service1Facade pService1Facade) {
        this.mService1Facade = pService1Facade;
    }

    public SomeMethod (int id) {
        //...
        lBusinessObject2 = this.mService1Facade.getBusinessObject2(id);
        lRes1 = lBusinessObject2.Operation1();
        lRes2 = lBusinessObject2.Operation2();
        lSum = lRes1 + lRes2;
        return lSum;
    }
}


В этом случае вызов каждого метода BusinessObject2 — это обращение на уровень-2.
Получается, BusinessObject2 должен сам "переадресовывать" вызовы на другой уровень?
Отредактировано 23.05.2024 13:09 zelenprog . Предыдущая версия .
Re: Дизайн разделения бизнес-слоя (логического) на два уровня (физических)
От: zelenprog  
Дата: 23.05.24 13:35
Оценка:
Z>Кроме деления приложения на "логические" слои, приложение делится еще и на "физические" уровни.
Z>Уровень, грубо говоря, — это физический компьютер, на котором выполняется программный код.

Z>Но возможен и другой случай, когда один логический слой разделяется на два физических уровня.

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

Z>Как это реализуется?


Мне кажется, что к моему вопросу подходит вот такая картинка:



Это картинка из статьи "Современные архитектуры фронт-энда":
https://habr.com/ru/articles/500072/

На этой картинке объекты View и Controller есть и на клиентской машине, и на серверной.
То есть какая-то часть их функциональности реализована на клиенте, а другая часть на сервере.
Получается, что тут оба слоя (и View-слой и Controller-слой) являются разделенными по двум физическим уровням.

Как это разделение реализуется на практике?
Re[3]: Дизайн разделения бизнес-слоя (логического) на два ур
От: r0nd  
Дата: 23.05.24 13:50
Оценка: 4 (1)
On May 23, 2024, 4:08 PM, zelenprog <140063@users.rsdn.org> wrote:

Z>Получается, BusinessObject2 должен сам "переадресовывать" вызовы на другой уровень?



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

Прокси для BusinessObject2 на уровне-1

  1. Интерфейс BusinessObject2: Определите интерфейс, который будет описывать методы бизнес-объекта.
  2. Прокси для BusinessObject2: Реализуйте прокси-класс, который будет выполнять сетевые запросы для вызова методов на уровне-2.

Интерфейс BusinessObject2

public interface BusinessObject2 {
    int operation1();
    int operation2();
    // Другие методы
}


Прокси-класс на уровне-1

public class BusinessObject2Proxy implements BusinessObject2 {
    private int id;
    private RestClient restClient; // Предположим, что это ваш HTTP клиент

    public BusinessObject2Proxy(int id, RestClient restClient) {
        this.id = id;
        this.restClient = restClient;
    }

    @Override
    public int operation1() {
        String url = "http://service2.example.com/api/businessObject2/" + id + "/operation1";
        return restClient.get(url, Integer.class);
    }

    @Override
    public int operation2() {
        String url = "http://service2.example.com/api/businessObject2/" + id + "/operation2";
        return restClient.get(url, Integer.class);
    }

    // Другие методы
}


REST API на уровне-2

@RestController
@RequestMapping("/api/businessObject2")
public class Service2 {

    @GetMapping("/{id}/operation1")
    public int operation1(@PathVariable int id) {
        // Логика выполнения operation1
        BusinessObject2 obj = businessLogic.getBusinessObject2ById(id);
        return obj.operation1();
    }

    @GetMapping("/{id}/operation2")
    public int operation2(@PathVariable int id) {
        // Логика выполнения operation2
        BusinessObject2 obj = businessLogic.getBusinessObject2ById(id);
        return obj.operation2();
    }

    // Другие методы
}

Использование прокси в контроллере на уровне-1

public class Service1AnyController {
    private Service1Facade mService1Facade;
    private RestClient restClient; // Предположим, что это ваш HTTP клиент

    public Service1AnyController(Service1Facade pService1Facade, RestClient restClient) {
        this.mService1Facade = pService1Facade;
        this.restClient = restClient;
    }

    public int someMethod(int id) {
        // Создаем прокси-объект для BusinessObject2
        BusinessObject2 lBusinessObject2 = new BusinessObject2Proxy(id, restClient);

        // Вызовы методов, которые фактически будут переадресованы на уровень-2
        int lRes1 = lBusinessObject2.operation1();
        int lRes2 = lBusinessObject2.operation2();
        int lSum = lRes1 + lRes2;
        return lSum;
    }
}


  1. Интерфейс: Интерфейс BusinessObject2 определяет методы, которые должен реализовывать бизнес-объект.
  2. Прокси: Прокси-класс BusinessObject2Proxy реализует интерфейс и содержит логику для переадресации вызовов методов на уровень-2 через сетевые запросы.
  3. REST API: На уровне-2 реализуются соответствующие конечные точки API для обработки запросов и выполнения логики.
  4. Контроллер: Контроллер на уровне-1 использует прокси-объект для взаимодействия с бизнес-логикой на уровне-2. Вызовы методов прокси-объекта фактически переадресуются на уровень-2.


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

❧ “Do not wait for the perfect time and place to enter, for you are already onstage.”
Re[4]: Дизайн разделения бизнес-слоя (логического) на два ур
От: zelenprog  
Дата: 24.05.24 06:48
Оценка:
R>В случае, когда функциональность BusinessObject2 распределена между двумя уровнями или полностью находится на втором уровне, вы можете использовать подход, основанный на шаблоне "Прокси". Этот подход позволяет объекту на уровне-1 переадресовывать вызовы методов на уровень-2.
...
R>Этот подход позволяет обеспечить слабую связанность между уровнями и скрыть детали сетевых взаимодействий от бизнес-логики, сохраняя при этом чистоту архитектуры.

Но ведь по "теории" бизнес-слой не должен зависеть от технических деталей.
А здесь у нас бизнес-объект очень тесно завязан на сетевое взаимодействие.
Не противоречит ли это чистой архитектуре?
Re: Дизайн разделения бизнес-слоя (логического) на два уровня (физических)
От: Kernan Ниоткуда https://rsdn.ru/forum/flame.politics/
Дата: 24.05.24 08:29
Оценка:
Здравствуйте, zelenprog, Вы писали:

Z>Я продолжаю "бороться" с архитектурой приложения.

Возьми за основу hexagonal архитектуру, только не смотри видосы где энтити выносят в общедоступный слой.
Sic luceat lux!
Re[2]: Дизайн разделения бизнес-слоя (логического) на два уровня (физических)
От: zelenprog  
Дата: 24.05.24 08:42
Оценка:
K>Возьми за основу hexagonal архитектуру...

Я читал про hexagonal-архитектуру.
Пытаюсь сделать приложение на ее основе.
Только эта архитектура не говорит о том, как разделить один слой на два уровня.
В этой архитектуре предполагается, что все слои находятся в одном приложении.
Поэтому и возник вопрос: как применить эту архитектуру, когда слои "распределенные".

K>...только не смотри видосы где энтити выносят в общедоступный слой.


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