Подтвердить что ты не робот

Луковая архитектура - Репозиторий Vs Service?

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

Во многих примерах репозиторий, похоже, имеет некоторую бизнес-логику, подобную GetAllProductsByCategoryId или GetAllXXXBySomeCriteriaYYY.

Для списков кажется, что служба - это просто оболочка в репозитории без какой-либо логики. Для иерархий (parent/children/children) это почти та же проблема: является ли роль репозитория для загрузки полной иерархии?

4b9b3361

Ответ 1

Репозиторий не является шлюзом для доступа к базе данных. Это абстракция, которая позволяет хранить и загружать объекты домена из некоторого хранилища сохраняемости. (База данных, кеш или даже простая коллекция). Он принимает или возвращает объекты домена вместо своего внутреннего поля, следовательно, это объектно-ориентированный интерфейс.

Не рекомендуется добавлять в репозиторий некоторые методы, такие как GetAllProductsByCategoryId или GetProductByName, потому что вы добавите все больше и больше методов в хранилище по мере увеличения количества полей вашего использования/объекта. Вместо этого лучше иметь метод запроса в репозитории, который принимает спецификацию. Вы можете передавать различные реализации Спецификации для извлечения продуктов.

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

Для второго вопроса: если я вижу код ProductRepository в коде, я бы ожидал, что он вернет мне список продуктов. Я также ожидаю, что каждый экземпляр продукта будет завершен. Например, если Product имеет ссылку на объект ProductDetail, я ожидаю, что Product.getDetail() вернет мне экземпляр ProductDetail, а не null. Возможно, реализация загрузки репозитория ProductDetail вместе с Продуктом, возможно, метод getDetail() вызывает ProductDetailRepository на лету. Мне все равно, как пользователь репозитория. Также возможно, что Product возвращает только ProductDetail id при вызове getDetail(). Это идеальный вариант с точки зрения контракта на репозиторий. Однако это усложняет мой код клиента и заставляет меня сам называть ProductDetailRepository.

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

Ответ 2

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

Таким образом, репозитории должны предоставлять интерфейс для работы CRUD на объектах домена. Помните, что Хранилища имеют дело с целым агрегатом.

Агрегаты - это группы вещей, которые принадлежат друг другу. Агрегатный корень - это то, что держит их всех вместе.

Пример Order и OrderLines:

OrderLines не имеют оснований для существования без их родительского ордера, а также не могут принадлежать к любому другому Заказу. В этом случае Order and OrderLines, вероятно, будет Aggregate, а Order будет представлять собой Aggregate Root

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

Ответ 3

Я считаю, что репозиторий должен быть только для операций CRUD.

public interface IRepository<T>
{
    Add(T)
    Remove(T)
    Get(id)
    ...
}

Таким образом, IRepository будет иметь: Добавлять, удалять, обновлять, получать, GetAll и, возможно, версию каждой из них, которая принимает список, т.е. AddMany, RemoveMany и т.д.

Для выполнения поисковых операций вы должны иметь второй интерфейс, такой как IFinder. Вы можете либо пойти со спецификацией, поэтому IFinder может иметь метод Find (критерий), который принимает критерии. Или вы можете пойти с такими вещами, как IPersonFinder, который будет определять пользовательские функции, такие как: FindPersonByName, FindPersonByAge и т.д.

public interface IMyObjectFinder
{
    FindByName(name)
    FindByEmail(email)
    FindAllSmallerThen(amount)
    FindAllThatArePartOf(group)
    ...
}

Альтернативой может быть:

public interface IFinder<T>
{
    Find(criterias)
}

Этот второй подход более сложный. Вам необходимо определить стратегию для критериев. Вы собираетесь использовать какой-либо язык запросов или более простую ассоциацию ключевых значений и т.д. Полную мощность интерфейса также трудно понять, просто взглянув на него. Это также облегчает утечку реализаций с помощью этого метода, поскольку критерии могут основываться на определенном типе системы сохранения, например, если вы берете SQL-запрос в качестве критериев, например. С другой стороны, это может помешать вам постоянно возвращаться к IFinder, потому что вы попали в специальный прецедент, требующий более конкретного запроса. Я могу сказать, что это может быть так, потому что ваша стратегия критериев не обязательно будет покрывать 100% случаев использования запросов, которые могут вам понадобиться.

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

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

Роль репозитория будет загружать полную иерархию того, что необходимо для правильной сборки возвращаемого элемента. Поэтому, если ваш репозиторий возвращает элемент, который имеет список другого типа элемента, он должен уже решить это. Лично мне нравится создавать мои объекты так, чтобы они не содержали ссылок на другие элементы, потому что это делает репозиторий более сложным. Я предпочитаю, чтобы мои объекты сохраняли ИД других элементов, так что, если клиенту действительно нужен этот другой элемент, он может запросить его снова с соответствующим репозиторием, данным Id. Это выравнивает все элементы, возвращаемые репозиториями, но все же позволяет создавать иерархии, если вам нужно.

Вы могли бы, если бы действительно почувствовали необходимость, добавить механизм ограничения в свой репозиторий, чтобы вы могли точно указать, в каком поле элемента вам нужно. Скажите, что у вас есть Личность, и только заботитесь о его имени, вы можете сделать Get (id, name), и Repository не потрудился бы получить все поля Person, только это поле имени. Выполнение этого, тем не менее, значительно усложняет репозиторий. И делать это с иерархическими объектами еще сложнее, особенно если вы хотите ограничить поля внутри полей полей. Поэтому я не рекомендую его. Единственная веская причина для этого, для меня, - это случаи, когда производительность имеет решающее значение, и больше ничего нельзя сделать для повышения производительности.

Ответ 4

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

В примере GetProductsByCategory(int id)

Во-первых, подумайте о первоначальной необходимости. Мы попали в контроллер, возможно, в CategoryController, поэтому у вас есть что-то вроде:

public CategoryController(ICategoryService service) {
    // here we inject our service and keep a private variable.
}

public IHttpActionResult Category(int id) {
    CategoryViewModel model = something.GetCategoryViewModel(id); 
    return View()
} 

до сих пор, так хорошо. Нам нужно объявить "что-то", которое создает модель представления. Пусть упрощаются и говорят:

public IHttpActionResult Category(int id) {
    var dependencies = service.GetDependenciesForCategory(id);
    CategoryViewModel model = new CategoryViewModel(dependencies); 
    return View()
} 

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

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

public IHttpActionResult Category(int id) {
    var products = repository.GetCategoryProducts(id);
    var category = repository.GetCategory(id); // full details of the category
    var childs = repository.GetCategoriesSummary(category.childs);
    CategoryViewModel model = new CategoryViewModel(products, category, childs); // awouch! 
    return View()
} 

вернитесь к службам:

public IHttpActionResult Category(int id) {
    var category = service.GetCategory(id);
    if (category == null) return NotFound(); //
    var model = new CategoryViewModel(category);
    return View(model);
}

гораздо лучше, но что именно внутри service.GetCategory(id)?

public CategoryService(ICategoryRespository categoryRepository, IProductRepository productRepository) {
    // same dependency injection here

    public Category GetCategory(int id) {
        var category = categoryRepository.Get(id);
        var childs = categoryRepository.Get(category.childs) // int[] of ids
        var products = productRepository.GetByCategory(id) // this doesn't look that good...
        return category;
    }

}

Попробуй другой подход, блок работы, я буду использовать инфраструктуру Entity в качестве UoW и репозиториев, поэтому нет необходимости создавать их.

public CategoryService(DbContext db) {
    // same dependency injection here

    public Category GetCategory(int id) {
        var category = db.Category.Include(c=> c.Childs).Include(c=> c.Products).Find(id);
        return category;
    }
}

Итак, здесь мы используем синтаксис "запрос" вместо синтаксиса метода, но вместо реализации нашего собственного комплекса мы можем использовать наш ORM. Кроме того, у нас есть доступ ко всем репозиториям, поэтому мы можем по-прежнему выполнять нашу Единицу работы внутри нашего сервиса.

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

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

public CategoryService(DbContext db) {
    // same dependency injection here

    public Category GetCategory(int id) {
        var category = db.Category.Find(id);
        return category;
    }
}

Итак, где все продукты и внутренние категории?

давайте взглянем на ViewModel, помните, что это будет ТОЛЬКО отображать данные в значения, если вы делаете что-то еще здесь, вы, вероятно, несете большую ответственность за свой ViewModel.

public CategoryViewModel(Category category) {
    Name = category.Name;
    Id = category.Id;
    Products = category.Products.Select(p=> new CategoryProductViewModel(p));
    Childs = category.Childs.Select(c => c.Name); // only childs names.
}

вы можете себе представить CategoryProductViewModel прямо сейчас.

НО (почему всегда есть но?)

Мы делаем 3 db-хита, и мы извлекаем все поля категорий из-за Find. Также включена Lazy Loading must. Не настоящее решение не так ли?

Чтобы улучшить это, мы можем изменить find с где... но это делегирует Single или Find в ViewModel, также он вернет IQueryable<Category>, где мы знаем, что он должен быть точно одним.

Помнишь, я сказал: "Я все еще боюсь?" это в основном почему. Чтобы исправить это, мы должны вернуть точные данные из службы (также знаете, как..... вы это знаете... да! ViewModel).

поэтому вернемся к нашему контроллеру:

public IHttpActionResult Category(int id) {
    var model = service.GetProductCategoryViewModel(id);
    if (category == null) return NotFound(); //
    return View(model);
}

внутри метода GetProductCategoryViewModel мы можем вызвать частные методы, которые возвращают разные части и собирают их как ViewModel.

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

Мы создаем интерфейс, этот интерфейс является фактическим контрактом того, что этот метод вернется.

ICategoryWithProductsAndChildsIds // quite verbose, i know.

nice, теперь нам нужно только объявить нашу ViewModel как

public class CategoryViewModel : ICategoryWithProductsAndChildsIds 

и реализовать его так, как мы хотим.

Интерфейс выглядит так, что у него слишком много вещей, конечно его можно разделить на ICategoryBasic, IProducts, IChilds или что бы вы ни назвали.

Поэтому, когда мы реализуем другой viewModel, мы можем выбрать только IProducts. У наших служб есть методы (частные или нет) для извлечения этих контрактов и склеивание частей на уровне обслуживания. (Легко сказать, чем сделать)

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

Ответ 5

В Domain Driven Design репозиторий отвечает за получение всей совокупности.