Русский
Русский
English
Статистика
Реклама

Слоистая архитектура

Архитектурный слой. Понятие, определение, представление

16.07.2020 14:23:13 | Автор: admin
Сейчас сложно найти короткое и понятное определение таких понятий как слой приложения и уровень абстракции. Это влечёт различия в понимании одного и того же либо непонимания данного понятия среди разработчиков.

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


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

В чём сейчас заключается путаница:
  • принято считать, что слоёв должно быть 3: данные, бизнес-логика, интерфейса но на самом деле слоёв может быть любое необходимое количество
  • нет критериев, по которым те или иные задачи относятся к тому или иному слою, что часто приводит, к созданию одного большого прикладного слоя, либо один какой либо из слоёв дорабатывается под задачи, не характерные для него
  • нет конкретного короткого определения
  • есть пересечения между понятиями слоя (layer), уровня и яруса (tier), к которым так же нет точных определений. По Фаулеру уровни относятся, подразумевая физическое разделение, а на практике такой определённости нет


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

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

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

Составляющие слоя:
  • если слой относится к реализации: классы, объекты, контекст, сервисы, контроллеры, прокси, сборки ...
  • если слой относится к абстракции: модели данных (идеализированные), действия...


Чем характеризуется архитектурный слой:
  • прикладное назначение, конкретное, логичное, определяет множество задач, которые можно отнести к определённому слою
  • определённое множество задач, реализуемых на данном слое
  • ресурсы и инструменты, с помощью которых реализуются задачи: данные, объекты и действия, которые можно выполнить над данными и объектами
  • данные и логика внутри определённого слоя согласованы внутри себя
  • согласованность определяет предсказуемость и предопределённость во взаимодествии ресурсов определённого слоя, другими словами, детали из которых образуется слой совместимы сдруг с другом
  • отдельный слой можно воспринимать как единое самодостаточное целое
  • зависимость между слоями можно свести к минимуму
  • созданный слой может быть основой для нескольких различных слоёв более высокого уровня
  • при переходе от одного слоя к другому, моделируемые сущности обычно подвергаются преобразованию из одного представления в другое


Коротко о порядке проектирования архитектурных слоёв:
  1. Выделяется все бизнес требования, которые структурируются по категориям.
  2. Требования разбиваются на задачи, которые должны решаться приложением.
  3. Задачи категоризируются и объединяются в группы на основе схожести своего предметного назначения.
  4. На основе этих категорий выделяется общее назначение архитектурного слоя, в котором будут решатся задачи.
  5. Решения задач можно представить как алгоритм, или процесс, который обеспечивает желаемый результат. Из всех задач выделяются общие составляющие (детали), из которых они реализуются. (модели и действия над ними). О том как это делается, будет дополнительная статья.
  6. На основе выделенных составляющих реализуются классы соответствующего слоя и как правило объединяются в одну отдельную сборку.


Пример 1.
Модель ISO/OSI

Пример 2.
Стоит задача автоматизировать электронный документооборот, которая подразумевает
выполнение элементарных действий с различными видами документов: подписание, согласование, отправка и другое
автоматическая фоновая работа в несколько потоков
управление работой потоков оперативно через пользовательский интерфейс
обмен данными с другими системами (приём, отправка, конвертирование документов)
image

Пример 3.
Строительство офиса
1 уровень задачи по строительству здания, фундамент, возведение стен, кирпичи, цемент
2 уровень отделка мебелирования, детали обои, мебель
3 уровень логическое распределение помещений, людей, деление на отделы детали: отделы, рабочие места, помещения

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

Один слой предоставляет только набор тех ресурсов, которые ему логически характерны. 3й слой (места, кабинеты, отделы), работает только в рамках этих сущностей и действий. Если мест не хватает, то кабинет не достраивается, но его можно достроить, доработав ниже стоящий слой.
Подробнее..

Почему здравый смысл важнее паттернов, а Active Record не так уж и плох

18.08.2020 12:17:00 | Автор: admin
Так уж вышло, что разработчики, особенно молодые, любят паттерны, любят спорить о том, какой паттерн нужно применять здесь или там. Спорить до хрипоты: это фасад или прокси, а может даже синглтон. А если у вас не чистая, гексагональная архитектура, то некоторые разработчики готовы сжечь на костре Святой Инквизиции.

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

А наличие во фреймворке нужных паттернов никак не гарантирует их правильного и осознанного применения.



Блеск и нищета Active Record


Давайте рассмотрим в качестве антипаттерна паттерн Active Record, которого в некоторых языках программирования и фреймворках стараются избегать всеми возможными путями.

Суть Active Record проста: мы храним бизнес-логику с логикой хранения сущности. Иными словами, если очень упрощенно, каждой табличке в БД соответствует класс сущности вместе с поведением.


Есть достаточно устойчивое мнение, что объединять бизнес логику с логикой хранения в одном классе это очень плохой, негодный паттерн. Он нарушает принцип единственной ответственности. И по этой причине Django ORM плоха by design.

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

Возьмём для примера модели User и Profile. Это довольно распространенный паттерн. Есть основная табличка, и есть дополнительная, в которой хранятся не всегда обязательные, но иногда нужные данные.


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

   def create(self, validated_data):        # create user         user = User.objects.create(            url = validated_data['url'],            email = validated_data['email'],            # etc ...        )        profile_data = validated_data.pop('profile')        # create profile        profile = Profile.objects.create(            user = user            first_name = profile_data['first_name'],            last_name = profile_data['last_name'],            # etc...        )        return user

Чтобы получить список пользователей, нужно обязательно думать, а будет ли у этих пользователей забираться атрибут profile, чтобы сразу заселектить две таблички с джоином и не получить SELECT N+1 в цикле.

user = User.objects.get(email='example@examplemail.com')user.userprofile.company_nameuser.userprofile.country

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

При этом, конечно же, очень не хочется, чтобы внешних пользователей API это как-то заботило. Есть REST-ресурс /users/{user_id}, и с ним хотелось бы работать, не думая о том, как внутри устроено хранение данных. Если они хранятся в разных источниках, то изменять пользователя или получать список данных будет сложнее.

Вообще говоря, модель ОРМ != модель предметной области!


И чем больше отличается реальный мир от предположения одна табличка в БД одна сущность предметной области, тем больше проблем с паттерном Active Record.

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

Методы ORM самый низкий уровень абстракции. Они не поддерживают никаких ограничений предметной области, а значит, дают возможность ошибиться. А еще скрывают от пользователя, какие запросы на самом деле делаются в БД, что приводит к неэффективным и долгим запросам. Классика, когда запросы делаются в циклах, вместо join-а или фильтра.

А что ещё, кроме querybuilding (возможности выстраивать запросы), нам дает ОRМ? Да ничего. Возможность переехать на новую БД? А кто в здравом уме и твердой памяти переезжал на новую БД и ему в этом помогла ОRМ? Если воспринимать её не как попытку смаппить модель предметной области (!) в БД, а как простую библиотеку, которая позволяет делать запросы к БД в удобном виде, то всё становится на свои места.

И даже несмотря на то, что в названиях классов используется Model, а в названии файлов models, моделями они не становятся. Не надо себя обманывать. Это просто описание табличек. Они не помогут ничего инкапсулировать.

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

Слоистая архитектура наносит ответный удар!


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

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

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


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

Но такое разделение ответственностей не дается бесплатно. Надо понимать, что дополнительные слои абстракции созданы для того, чтобы мешать плохим изменениям кода. Очевидно, что Repository скрывает факт хранения объекта в SQL-БД, поэтому надо стараться не давать SQL-изму вылезать за пределы Repository. И все запросы, даже самые простые и очевидные, придется протаскивать через слой хранения.

Например, если возникнет необходимость получить офис по имени и подразделению, придется написать так:

# пример на котлинообразном языке программированияinterface OfficeRepository: CrudRepository<OfficeEntity, Long> {    @Query("select o from OfficeEntity o " +            "where o.number = :office and o.branch.number = :branch")    fun getOffice(@Param("branch") branch: String,                  @Param("office") office: String): OfficeEntity? ...

А в случае с Active Record всё значительно проще:

Office.objects.get(name=Name, branch=Branch)

Всё не так просто и в том случае, если бизнес-сущность на самом деле хранится нетривиальным образом (в нескольких табличках, в разных сервисах и т.д.). Чтобы реализовать это хорошо (и правильно) для чего этот паттерн и создавался, чаще всего приходится использовать такие паттерны, как агрегаты, Unit of work и Data mappers.

Правильно выделить агрегат, правильно соблюсти все накладываемые на него ограничения, правильно сделать data mapping это сложно. И справится с этой задачей только очень хороший разработчик. Тот самый, который и в случае с Active Record смог бы сделать всё правильно.

А что происходит с обычными разработчиками? Которые знают все паттерны и свято уверены, что если они используют слоистую архитектуру, то у них автоматически код становится поддерживаемым и хорошим, не чета Active Record. А они создают CRUD-репозитории на каждую табличку. И работают в концепции

одна табличка один репозиторий один объект (entity).

А не:

один репозиторий один объект предметной области.


Они так же слепо уверены, что если в классе используется слово Entity, то оно отражает модель предметной области. Как слово Model в Active Record.

А в результате получается более сложный и менее гибкий слой хранения, который имеет все отрицательные свойства как Active Record, так и Repository/Data mappers.

Но на этом слоистая архитектура не заканчивается. Еще обычно выделяют сервисный слой.

Правильная реализация такого сервисного слоя это тоже сложная задача. И, например, неопытные разработчики делают сервисный слой, который представляет собой сервисы прокси к репозиториям или ORM (DAO). Т.е. сервисы написаны так, что на самом деле не инкапсулируют бизнес-логику:

# псевдокод на котлинообразном языке в @Serviceclass AccountServiceImpl(val accountDaoService: AccountDaoService) : AccountService {    override fun saveAccount(account: Account) =            accountDaoService.saveAccount(convertClass(account, AccountEntity::class.java))    override fun deleteAccount(id: Long) =            accountDaoService.deleteAccount(id)

И возникает сочетание недостатков как Active Record, так и Service layer.

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


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

Наличие во фреймворке паттернов ООП не гарантирует их правильного и адекватного применения.

Серебряной пули нет


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

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

Чем проще приложение или сервис, тем меньше слоев абстракций нужно.

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

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

Перевод Пишем CRUD-приложение на Go с помощью Mysql, GORM, Echo, Clean Architecture

24.11.2020 14:17:12 | Автор: admin

Начнем сначала


В этой статье будет сказ о том, как на Clean Architecture написать API с функциями CR(U)D, где в качестве БД взят Mysql, фреймворк Echo, ORMapper GORM.

Что делаем


API с функциями Create, Read, (Update), Delete. Обновление на самом деле реализовать особо не удалось, милости прошу попробовать самостоятельно.

Целевая аудитория


Те разработчики, которые хотят создать простой API после освоения Go.

Основное cодержание


Что такое Clean Architecture, и с чем его едят


Это мы можем подробно рассмотреть на следующей картинке:

image

Цель Clean Architecture это разделение сфер. Чтобы удачно добиться этого разделения, нужно всегда держать в голове зависимости каждого слоя на картинке с остальными. Разделение на слои улучшает читаемость кода и делает его устойчивым к изменениям.

На рисунке выше стрелка указывает снаружи внутрь круга это направление зависимости. Важно: зависимости направлены извне внутрь, не наоборот.

Другими словами, вы можете снаружи вызывать вещи, объявленные внутри, но вы не можете вызывать вещи, объявленные снаружи, находясь внутри.

Код данного приложения написан с учетом зависимостей, как и завещает Clean Architecture.

О функциях


Endpointы каждой функции следующие:

POST: /users
GET: /users
DELETE: /users/:id

Структура директорий


image

Взаимное отображение слоев архитектуры и директорий выглядит так:

  • domain Entities
  • usecase Use Cases
  • interface Controllers Presenters
  • infrasctructure External Interfaces


domain


Директории domain соответствует слой Entity. Так как он находится в самом ядре, вызываться может с любого слоя.

image

src/domain/user.go
package domaintype User struct {    ID   int    `json:"id" gorm:"primary_key"`    Name string `json:"name"`}


Создаем struct User с id и именем и устанавливаем идентификатор в качестве первичного ключа.

Немного о json:id gorm:primary_key:
В json:id json отвечает за маппинг. В gorm:primary_key идет пометка на модели с помощью gorm.
Конечно, помимо primary_key вы можете использовать not null, unique, default и т.д.,

Полезная ссылка объявление моделей на GORM

infrastructure


Самый крайний, внешний слой. Здесь описывается часть, в которой приложение связано с внешним миром. В нашем случае, в этом слое объявляется связь с БД и Router.

image

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

src/infrastucture/sqlhandler.go
package infrastructureimport (    "gorm.io/driver/mysql"    "gorm.io/gorm"    "echoSample/src/interfaces/database")type SqlHandler struct {    db *gorm.DB}func NewSqlHandler() database.SqlHandler {    dsn := "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})    if err != nil {        panic(err.Error)    }    sqlHandler := new(SqlHandler)    sqlHandler.db = db    return sqlHandler}func (handler *SqlHandler) Create(obj interface{}) {    handler.db.Create(obj)}func (handler *SqlHandler) FindAll(obj interface{}) {    handler.db.Find(obj)}func (handler *SqlHandler) DeleteById(obj interface{}, id string) {    handler.db.Delete(obj, id)}


По поводу баз данных я оставлю ссылки на документацию: gorm.io

Далее идет маршрутизация.
В этом приложении я использую веб-фреймворк Echo. Фреймворк определяет Method и Path API
Ссылка на документацию: https://echo.labstack.com/

src/infrastructure/router.go
package infrastructureimport (    controllers "echoSample/src/interfaces/api"    "net/http"    "github.com/labstack/echo")func Init() {    // Echo instance    e := echo.New()    userController := controllers.NewUserController(NewSqlHandler())    e.GET("/users", func(c echo.Context) error {        users := userController.GetUser()         c.Bind(&users)         return c.JSON(http.StatusOK, users)    })    e.POST("/users", func(c echo.Context) error {        userController.Create(c)        return c.String(http.StatusOK, "created")    })    e.DELETE("/users/:id", func(c echo.Context) error {        id := c.Param("id")        userController.Delete(id)        return c.String(http.StatusOK, "deleted")    })    // Start server    e.Logger.Fatal(e.Start(":1323"))}


interfaces


Слой Controllers Presenters.
Вот здесь уже нужно вспомнить о зависимостях.

image

Нет проблем с вызовом со слоев domain и usecase, но нельзя вызвать слой infrastructure напрямую, поэтому объявим interface. (Получилось немного запутанно, но речь идет про интерфейс sqlHandler, определенный на слое infrastrucure)

src/interfaces/api/user_controller.go
package controllersimport (    "echoSample/src/domain"    "echoSample/src/interfaces/database"    "echoSample/src/usecase"    "github.com/labstack/echo")type UserController struct {    Interactor usecase.UserInteractor}func NewUserController(sqlHandler database.SqlHandler) *UserController {    return &UserController{        Interactor: usecase.UserInteractor{            UserRepository: &database.UserRepository{                SqlHandler: sqlHandler,            },        },    }}func (controller *UserController) Create(c echo.Context) {    u := domain.User{}    c.Bind(&u)    controller.Interactor.Add(u)    createdUsers := controller.Interactor.GetInfo()    c.JSON(201, createdUsers)    return}func (controller *UserController) GetUser() []domain.User {    res := controller.Interactor.GetInfo()    return res}func (controller *UserController) Delete(id string) {    controller.Interactor.Delete(id)}


В controller вызываем со слоёв domain и usecase, поэтому проблем нет.

src/interfaces/api/context.go
package controllerstype Context interface {    Param(string) string    Bind(interface{}) error    Status(int)    JSON(int, interface{})}


Связь с БД

src/interfaces/database/user_repository.go
package databasepackage databaseimport (    "echoSample/src/domain")type UserRepository struct {    SqlHandler}func (db *UserRepository) Store(u domain.User) {    db.Create(&u)}func (db *UserRepository) Select() []domain.User {    user := []domain.User{}    db.FindAll(&user)    return user}func (db *UserRepository) Delete(id string) {    user := []domain.User{}    db.DeleteById(&user, id)}


В repository вызывается sqlHandler, но он вызывается не напрямую со слоя infrastructure, а с помощью объявленного там же interface.
Это называется принципом инверсии зависимостей.

src/interfaces/db/sql_handler.go
package databasetype SqlHandler interface {    Create(object interface{})    FindAll(object interface{})    DeleteById(object interface{}, id string)}


Теперь вы можете вызывать процесс sql_handler.

usecase


Последний оставшийся слой, usecase.

image

src/usecase/user_interactor.go
package usecaseimport "echoSample/src/domain"type UserInteractor struct {    UserRepository UserRepository}func (interactor *UserInteractor) Add(u domain.User) {    interactor.UserRepository.Store(u)}func (interactor *UserInteractor) GetInfo() []domain.User {    return interactor.UserRepository.Select()}func (interactor *UserInteractor) Delete(id string) {    interactor.UserRepository.Delete(id)}


Опять же, нам нужно применить принцип инверсии зависимости, как и раньше. Поэтому определяем user_repository.go.

src/usecase/user_repository.go
package usecaseimport (    "echoSample/src/domain")type UserRepository interface {    Store(domain.User)    Select() []domain.User    Delete(id string)}


На этом реализация завершена.
После этого запустите mysql с docker-compose.yml, запустите сервер, и все должно работать.

docker-compose.ymlversion: "3.6"services:  db:    image: mysql:5.7    container_name: go_sample    volumes:      # настройки mysql      - ./mysql/conf:/etc/mysql/conf.d      - ./mysql/data:/var/lib/mysql    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci    ports:      - 3306:3306    environment:      MYSQL_DATABASE: go_sample      MYSQL_ROOT_PASSWORD: password      MYSQL_USER: root      TZ: "Asia/Tokyo"
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru