[Перевод] Осваиваем чистую архитектуру кода на Golang: практическое руководство по сущностям и слоям моделей

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

8a6e9cb0971671ef30391b4178d24715.png

Чистая структура кода — это набор рекомендаций и принципов, направленных на то, чтобы сделать код более читаемым, удобным и масштабируемым. В контексте архитектуры программного обеспечения обычной практикой является разграничивать задачи путем разделения кодовой базы на отдельные слои. Два общих слоя в этом смысле — это слои сущностей и моделей. В теории это разные уровни, но иногда они могут быть объединены в один.

536b5c9fb14dec8ea35303dacb728c7e.jpg

Концепции сущностей и моделей

Сущности  не зависят от конкретного механизма хранения и предназначены для отображения бизнес-логики. Сущности часто носят уникальные имена и связаны с другими сущностями, а также могут использовать методы для выполнения операций, характерных для данного домена. Это простые объекты (POJOs в Java, POCOs в C#), которые отражают основные понятия бизнес-домена: пользователь, продукт, заказ и т.д. Эти объекты в основном сосредоточены на бизнес-логике и не зависят от каких-либо внешних ресурсов или инфраструктуры.

Модели данных представляют собой структуры данных, используемые для взаимодействия между различными частями приложения, например, между пользовательским интерфейсом, API и системами хранения данных. Модели часто выступают в качестве объектов передачи данных (DTO), которые помогают в сериализации, десериализации и проверке данных. Они в основном сосредоточены на структуре данных и их отображении, а не на бизнес-логике. Как правило, они легко поддаются сериализации в различные форматы, такие как JSON или XML. 

Ключевые характеристики и различия

Основные характеристики слоя сущностей:

  • Представляет основные бизнес-концепции и правила.

  • Независимость от внешних ресурсов, инфраструктуры или фреймворков.

  • Содержит состояние и поведение бизнес-объектов.

Основные характеристики слоя моделей

  • Обеспечивает структурированное представление данных для связи между различными частями приложения.

  • Может включать логику проверки данных, сериализации и десериализации.

  • Отделяет проблемы отображения и обработки данных от основной бизнес-логики.

Различия между слоем сущностей и слоем моделей:

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

  2. Зависимости: сущности не зависят от внешних ресурсов или инфраструктуры, в то время как модели могут взаимодействовать с различными частями приложения, такими как пользовательский интерфейс, API или системы хранения данных.

  3. Функции: сущности передают состояние и поведение бизнес-объектов, а модели выполняют проверку, сериализацию и десериализацию данных.

  4. Связь: сущности представляют основные концепции бизнес-области и обычно не сильно привязаны к конкретным технологиям или фреймворкам. Модели могут быть  связаны с UI-фреймворками или системами хранения данных.

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

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

Объединять их или нет, зависит от конкретных требований

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

Изображение

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

Что дает объединение уровней сущностей и моделей:

  1. Упрощение: в небольших приложениях или приложениях с ограниченной областью применения разделение слоев может добавить ненужную сложность.

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

  3. Ограниченная бизнес-логика: в некоторых приложениях бизнес-логика может быть минимальной или тесно связанной со структурами данных.

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

  1. Масштабируемость и ремонтопригодность: при слиянии слоев может стать сложнее масштабировать приложение или поддерживать кодовую базу.

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

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

Объединять или разделять слои в микросервисном приложении?

  1. Сложность: Если микросервисный продукт имеет минимальную бизнес-логику или логика тесно связана со структурами данных, объединение слоев может быть более практичным. Однако если приложение имеет сложные бизнес-правила или требует четкого разделения между данными и логикой, лучше разделить слои.

  2. Простота обслуживания:   Разделение слоев может помочь сделать код более удобным для обслуживания за счет изоляции изменений и минимизации влияния модификаций.

  3. Возможность повторного использования: В некоторых случаях микросервису может потребоваться поделиться своими доменными сущностями или моделями данных с другими микросервисами или частями системы. В таких сценариях разделение уровней сущностей и моделей может помочь повторно использовать компоненты и поддержании четкой границы между ними.

  4. Эволюция: По мере развития микросервисов и роста их функциональности может возникнуть необходимость разделения слоев. Объединение может подойти для оперативной разработки или подготовки прототипов, но со временем может возникнуть необходимость в рефакторинге кода для разделения слоев.

  5. Объединение слоев иногда используется в микросервисных приложениях. Решение о слиянии или разделении зависит от конкретных требований, сложности и контекста каждого микросервиса.

Методы обработки данных в объединенных уровнях

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

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

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

  3. Шаблон сопоставления данных: используйте паттерн Data Mapper для преобразования данных между различными форматами отображения данных. Это поможет сохранить разделение между различными видами представления информации и облегчит адаптацию к изменениям в структурах данных.

  4. Валидация: реализуйте логику валидации в отдельных классах или в качестве части сервисного уровня. Это поможет обеспечить согласованность данных и соблюдать необходимых ограничений даже при объединении слоев.

  5. Объекты передачи данных (DTO): в некоторых случаях для передачи данных между различными частями приложения или между приложением и внешними системами может потребоваться создание отдельных объектов.

  6. Паттерн адаптера: используйте паттерн адаптера для преобразования данных между различными форматами или интерфейсами. Это поможет разделить задачи и упростить интеграцию со сторонними системами или переключение между различными вариантами хранения данных.

Объединение уровней сущностей и моделей не лишает вас возможности использовать различные техники и паттерны для эффективной обработки и управления данными.

Пример разделения

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

Следующий код является частью файла entity.go.

type Product struct {
   ID          int
   Name        string
   Description string
   Price       float64
   Quantity    int
}

// Entity: User
type User struct {
   ID       int
   Name     string
   Email    string
   Password string
   Address  string
}

Ниже приведен код содержимого файла model.go.

package models

// Product Model
type Product struct {
   Name         string  `json:"name"`
   Price        float64 `json:"price"`
   ThumbnailURL string  `json:"thumbnail_url"`
}

// CartItem Model
type CartItem struct {
   ProductID  int     `json:"product_id"`
   Quantity   int     `json:"quantity"`
   TotalPrice float64 `json:"total_price"`
}

Обычно разные объекты всегда используют разные файлы сущностей и файлы моделей.

Далее — содержимое файла main.go.

package main

import (
   "encoding/json"
   "fmt"

   "github.com//entity_model/entities"
   "github.com//entity_model/models"
)

func main() {
   // Create a Product entity
   product := entities.Product{
      ID:          1,
      Name:        "iPhone 13",
      Description: "A powerful smartphone",
      Price:       999.99,
      Quantity:    10,
   }

   // Create a Product from the Product entity
   productModel := models.Product{
      Name:         product.Name,
      Price:        product.Price,
      ThumbnailURL: "https://example.com/thumbnail.jpg",
   }

   // Create a CartItem from the Product entity and a quantity
   quantity := 2
   cartItemModel := models.CartItem{
      ProductID:  product.ID,
      Quantity:   quantity,
      TotalPrice: product.Price * float64(quantity),
   }

   // Output the Product, Product, and CartItem as JSON
   productJSON, _ := json.Marshal(product)
   fmt.Println("Product:", string(productJSON))

   productModelJSON, _ := json.Marshal(productModel)
   fmt.Println("ProductModel:", string(productModelJSON))

   cartItemModelJSON, _ := json.Marshal(cartItemModel)
   fmt.Println("CartItemModel:", string(cartItemModelJSON))
}

В этом примере я задаю сущности Product и User и модели ProductModel и CartItemModel. Затем мы создаем экземпляры этих сущностей и моделей и выводим их в формате JSON. 

Пример объединения

Я приведу пример, в котором слои объединены.

Ниже приведен код файла user.go, он находится в папке models.

package models

import "time"

type User struct {
   ID        int       `json:"id,omitempty"`
   FirstName string    `json:"first_name"`
   LastName  string    `json:"last_name"`
   Email     string    `json:"email"`
   Password  string    `json:"-"`
   CreatedAt time.Time `json:"created_at,omitempty"`
   UpdatedAt time.Time `json:"updated_at,omitempty"`
}

func (u *User) FullName() string {
   return u.FirstName + " " + u.LastName
}

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

Следующий код — это файл user.go, он находится в папке service.

В приведенном выше коде функция CreateUser показывает, как объединенные сущности и модели используются вместе. Сервисный уровень выполняет проверку и бизнес-логику непосредственно на структуре User, которая представляет собой как доменную сущность, так и модель данных.

Заключение

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

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

От редакции

17–18 июня Слёрм проводит интенсив «Чистая архитектура приложения на Go». Вы изучите, что такое чистая архитектура на языке Golang, под руководством опытного спикера создадите сервис по работе с контактами и возможностью их группировки и получите подробный фидбэк.

У интенсива прогрессивная цена — чем ближе к дате релиза, тем дороже. До 10 июня вы можете записаться по самой низкой цене — 25 000 рублей

© Habrahabr.ru