Domain-driven design

Введение в Domain-Driven Design (DDD)

Domain-Driven Design (DDD) — это подход к проектированию и разработке программного обеспечения, в основе которого лежит создание моделей предметной области и использование этих моделей для построения приложений. В DDD внимание уделяется не только техническим аспектам, но и глубокой проработке бизнес-логики, взаимодействию с заказчиками и пониманию терминологии предметной области. Этот подход ориентирован на совместную работу разработчиков и специалистов по бизнесу для создания высококачественных и масштабируемых решений.

Ember.js, будучи мощным фреймворком для создания одностраничных приложений (SPA), предоставляет достаточно гибкости для реализации принципов DDD, несмотря на то что сам по себе фреймворк не фокусируется напрямую на этой методологии. Тем не менее, правильное использование Ember.js в контексте DDD может значительно повысить продуктивность разработки и улучшить структуру кода.

Модели и сущности в Ember.js

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

Модели в Ember.js реализуются с помощью Ember Data — библиотеки для работы с данными. Модели могут быть связаны с REST API или другими источниками данных. В контексте DDD, каждый модель может быть представлена как сущность или агрегат, который инкапсулирует бизнес-логику и операции над данными.

Пример создания модели в Ember.js:

import Model, { attr, hasMany } from '@ember-data/model';

export default class PostModel extends Model {
  @attr('string') title;
  @attr('string') content;
  @hasMany('comment') comments;
}

Здесь PostModel представляет сущность “Пост”, которая содержит атрибуты title и content, а также связь с множеством сущностей “Комментариев”. Это отражает одну из основных концепций DDD: сущности, которые имеют идентифицируемое состояние и жизнь в рамках предметной области.

Агрегаты и их реализация

Агрегаты в DDD — это группы объектов, которые рассматриваются как единое целое для изменения данных. В контексте Ember.js агрегаты могут быть представлены как комбинации моделей, связанных между собой, с применением бизнес-логики, которая инкапсулирует все изменения.

Пример агрегата, состоящего из нескольких моделей:

import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class BlogPost extends Component {
  @tracked post;
  @tracked comments = [];

  constructor() {
    super(...arguments);
    this.loadPost();
  }

  async loadPost() {
    this.post = await this.store.findRecord('post', this.args.postId);
    this.comments = await this.store.query('comment', { postId: this.args.postId });
  }

  @action
  async addComment(content) {
    let comment = this.store.createRecord('comment', { content });
    await comment.save();
    this.comments.push(comment);
  }
}

В этом примере BlogPost является агрегатом, который инкапсулирует работу с сущностью “Пост” и связанными с ним “Комментариями”. С помощью действий и методов загрузки данных можно управлять состоянием агрегата и выполнить бизнес-логику.

Сервисный слой и взаимодействие с бизнес-логикой

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

Пример сервиса:

import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default class NotificationService extends Service {
  @service store;

  async createNotification(type, message) {
    let notification = this.store.createRecord('notification', { type, message });
    await notification.save();
  }
}

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

Репозитории и доступ к данным

Репозитории — это абстракции, отвечающие за извлечение данных из источников и их сохранение. В DDD репозиторий является ключевой частью взаимодействия с внешними хранилищами данных. В Ember.js доступ к данным осуществляется через Ember Data, однако использование паттерна репозитория помогает организовать более чистую и тестируемую архитектуру.

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

import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default class PostRepositoryService extends Service {
  @service store;

  async getPostById(postId) {
    return this.store.findRecord('post', postId);
  }

  async savePost(post) {
    await post.save();
  }
}

Здесь PostRepositoryService инкапсулирует все операции, связанные с постами, и обеспечивает взаимодействие с хранилищем через Ember Data. Репозиторий позволяет организовать код так, чтобы бизнес-логика и доступ к данным не были перепутаны, и тестирование становилось более удобным.

Применение DDD в маршрутах и контроллерах Ember.js

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

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

Пример маршрута:

import Route from '@ember/routing/route';

export default class PostRoute extends Route {
  async model(params) {
    return this.store.findRecord('post', params.post_id);
  }
}

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

Внедрение зависимостей и инверсия контроля

В DDD принцип инверсии контроля (IoC) играет ключевую роль в организации архитектуры. В Ember.js инъекция зависимостей осуществляется через систему сервисов. Сервисы можно внедрять в компоненты, контроллеры, маршруты и даже другие сервисы.

Пример инъекции сервисов:

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class PostListComponent extends Component {
  @service postRepository;
  
  async loadPosts() {
    this.posts = await this.postRepository.getPosts();
  }
}

Здесь сервис postRepository инжектируется в компонент PostListComponent. Это позволяет компоненту использовать логику доступа к данным, предоставляемую репозиторием, без явной зависимости от деталей реализации.

Разделение контекстов и границы

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

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

Заключение

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