DDD - зависимости между моделью домена, сервисами и репозиториями
Просто хотелось узнать, как другие слои их архитектуры. Скажем, у меня есть свои слои следующим образом:
Уровень домена
--Product
--Product Сервис (если imp войдет в этот слой?)
--IProductService
--IProductRepository
Уровень инфраструктуры
--Product Репозиторий (Imp IProductRepository в моем домене)
Теперь, когда создается новый продукт, у меня есть требование присвоить идентификатор продукта, вызвав метод ProductService.GetNextProductId().
Поскольку служба имеет зависимость от репозитория, я создал ProductService ctor с интерфейсом IProductRepository, который может быть введен позже. что-то вроде этого:
public class ProductService : IProductService
{
private IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public long GetNextProductId()
{
return _repository.GetNextProductId();
}
}
Моя проблема в том, что когда я использую службу в классе продуктов, я делаю ссылку на репозиторий в ctor при создании нового класса ProductService. В DDD его большой нет нет, чтобы иметь такую ссылку. Я даже не уверен, правильно ли настроен класс домена продукта, чтобы позвонить в службу, может кто-то посоветует:
public class Product : Entity
{
private ProductService _svc;
private IProductRepository _repository;
public Product(string name, Address address) //It doesnt seem right to put parm for IProductRepository in the ctor?
: base(_svc.GetNextProductId) // This is where i pass the id
{
// where to create an instance of IProductRepository?
}
}
Как я могу элегантно решить эту проблему дизайна? Я открыт для предложений от опытных разработчиков DDD
РЕДАКТИРОВАТЬ:
Спасибо за ваши комментарии. Я также сомневался, следует ли вызывать службу из класса продукта. Я еще не использовал шаблон фабрики (пока), поскольку конструкция объекта все еще проста. Я не чувствую, что это оправдывает фабричный метод?
Я в замешательстве... Отложите ProductId в сторону, если моему классу Product нужны другие данные из Сервиса, например GetSystemDateTime() (я знаю, плохой пример, но пытаюсь продемонстрировать вызов non db), где будет вызван этот метод службы?
Службы в DDD - это логические дампы, где логика не является натурным для объекта домена, правильно? Итак, как он склеивается?
Ответы
Ответ 1
К вашему последнему пункту, сервисы в DDD - это место, где можно поставить то, что я называю "неудобной" логикой. Если у вас есть какая-то логика или рабочий поток, который имеет зависимости от других объектов, это тип логики, который обычно не "подходит" внутри самого объекта домена. Пример. Если у меня есть метод на моем бизнес-объекте для выполнения некоторого типа проверки, класс службы может выполнить этот метод (сохраняя при этом фактическую логику проверки, связанную с сущностью внутри своего класса).
Другим действительно хорошим примером, который я всегда упоминаю, является метод перевода средств. У вас не было бы переноса объекта учетной записи с одного объекта на другой, но вместо этого у вас будет служба, которая берет учетную запись "to" и "from". Затем внутри службы вы будете ссылаться на метод вывода на свой счет "from" и метод депозита на ваш счет "to". Если вы попытаетесь поместить это внутри самого объекта учетной записи, это будет неудобно.
Большой подкаст, который подробно рассказывает об этой самой теме, можно найти здесь здесь. Дэвид Лариби делает действительно хорошую работу, объясняя теперь только "как", но "почему" DDD.
Ответ 2
В вашей модели домена не должно быть ссылки на ProductService или на IProductRepository. Если вы создаете новый продукт, он должен быть создан с помощью factory - factory может использовать ProductService для получения идентификатора продукта.
Фактически я бы обернул ProductService с помощью соответствующего интерфейса, такого как IProductIdGeneratorService, чтобы вы могли ввести его в factory с помощью своего контейнера IoC.
Ответ 3
Вот как я бы структурировал вашу проблему. Я считаю, что это также рекомендуется для DDD.
public class ProductService : IProductService // Application Service class, used by outside components like UI, WCF, HTTP Services, other Bounded Contexts, etc.
{
private readonly IProductRepository _prodRepository;
private readonly IStoreRepository _storeRepository;
public ProductService(IProductRepository prodRepository, IStoreRepository storeRepository) // Injected dependencies DI
{
if(prodRepository == null) throw new NullArgumentException("Prod Repo is required."); // guard
if(storeRepository == null) throw new NullArgumentException("Store Repo is required."); // guard
_prodRepository = prodRepository;
_storeRepository = storeRepository;
}
public void AddProductToStore(string name, Address address, StoreId storeId) //An exposed API method related to Product that is a part of your Application Service. Address and StoreId are value objects.
{
Store store = _storeRepository.GetBy(storeId);
IProductIdGenerator productIdGenerator = new ProductIdGenerator(_prodRepository);
Product product = Product.MakeNew(name, address, productIdGenerator);
}
... // Rest of API
}
public class Product : Entity
{
public static MakeNew(string name, Address address, IProductIdGenerator productIdGenerator) // Factory to make construction behaviour more explicit
{
return new Product(name, address, productIdGenerator);
}
protected Product(string name, Address address, IProductIdGenerator productIdGenerator)
: base(productIdGenerator.GetNextProductId())
{
Name = name;
Address = address;
}
... // Rest of Product methods, properties and fields
}
public class ProductIdGenerator : IProductIdGenerator
{
private IProductRepository _repository;
public ProductIdGenerator(IProductRepository repository)
{
_repository = repository;
}
public long GetNextProductId()
{
return _repository.GetNextProductId();
}
}
public interface IProductIdGenerator
{
long GetNextProductId();
}
В основном, ProductService является частью вашей прикладной службы, то есть точкой входа и выхода всего, что необходимо для использования вашего домена или пересечения его границы. Он отвечает за делегирование каждого варианта использования соответствующим компонентам, которые могут справиться с ним, и координация между всеми этими компонентами, если многие из них должны выполнять прецедент.
Продукт - это ваш агрегат и объект в вашем домене. Он отвечает за диктовку контракта UbiquitousLanguage, который захватывает домен вашего предприятия. Таким образом, это само по себе означает, что ваш домен имеет концепцию продукта, который содержит данные и поведение, в зависимости от того, какие данные и поведение вы публикуете публично, должна быть концепция UbiquitousLanguage. В этом поле не должно быть внешних зависимостей вне модели домена, поэтому нет служб. Но методы могут принимать доменные службы в качестве параметров, чтобы помочь ему выполнять логику поведения.
ProductIdGenerator является примером такой службы домена. Доменные службы инкапсулируют логику поведения, которая пересекает внешнюю границу Entity. Поэтому, если у вас есть логика, требующая других совокупных корней или внешних сервисов, таких как репозиторий, файловая система, криптография и т.д. В принципе, любая логика, которую вы не можете тренировать внутри вашего объекта, не нуждаясь ни в чем другом, может потребоваться доменная служба. Если логика для englobing и, по-видимому, концептуально может не принадлежать как метод для вашего Entity, это знак, которому может понадобиться совершенно новый пример использования Application Service, только для него, или вы пропустили Entity в своем дизайне. Также можно использовать Службу домена из Службы приложений напрямую, недвойным способом отправки. Это немного похоже на методы расширения С# по сравнению с обычным статическим методом.
=========== Отвечать на ваши вопросы по редактированию ===============
Я также сомневался, следует ли вызывать службу из класса продукта.
Доменные службы можно вызывать из класса продукта, если они передаются как временная ссылка через параметр метода. Службы приложений никогда не должны вызываться из класса Product.
Я не использовал шаблон factory (пока) как конструкцию объект по-прежнему прост. Я не чувствую, что это оправдывает метод factory?
Это зависит от того, что вы ожидаете, у вас будет больше времени, сделав factory сейчас, даже если у вас нет нескольких логических схем построения, или рефакторинг позже, когда вы это сделаете. Я думаю, что это не стоит для лиц, которые не должны быть построены более чем одним способом. Поскольку wikipedia объясняет, factory используется для того, чтобы сделать каждый конструктор более явным и дифференцируемым. В моем примере MakeNew factory объясняет, что эта конкретная конструкция Entity служит цели: создать новый продукт. У вас может быть больше factory, например, MakeExisting, MakeSample, MakeDeprecated и т.д. Каждый из этих factory создаст Продукт, но для разных целей и несколькими разными способами. Без Factory все эти конструкторы будут называться Product(), и было бы трудно узнать, какой из них для чего и что делает. Недостатком является то, что factory сложнее работать, когда вы расширяете свой объект, дочерний объект не может использовать родительский factory для создания дочернего элемента, поэтому я, как правило, делаю всю конструкторскую логику внутри конструкторов, и используйте только factory, чтобы иметь красивое имя для них.
Я запутался... Отложив ProductId в сторону, если мой класс Product нужны некоторые другие данные из Сервиса, например GetSystemDateTime() (я знаю, плохой пример, но попытка продемонстрировать вызов не db), где бы это метод обслуживания?
Предположим, что реализация Date была деталью инфраструктуры. Вы должны создать абстракцию вокруг нее, чтобы использовать ее в своем приложении. Он начнется с интерфейса, может быть, что-то вроде IDateTimeProvider. Этот интерфейс будет иметь метод GetSystemDateTime().
Ваши прикладные службы будут иметь возможность создавать экземпляр IDateTimeProvider и вызывать его методы в любое время, чем это может передать результат Агрегатам, сущностям, доменным службам или тому, что ему понадобится.
Ваши доменные службы могут свободно содержать ссылку на IDateTimeProvider как поле класса, но не должны создавать сам экземпляр. Либо он получает его через инъекцию зависимости, либо запрашивает его через Service Locator.
Наконец, ваши Entites и Aggregate Roots и Value Objects могут бесплатно вызвать GetSystemDateTime() и другие методы IDateTimeProvider, но не напрямую. Вам нужно будет пройти двойную рассылку, где вы должны предоставить доменную службу в качестве параметра одного из ее методов, и она будет использовать эту службу домена для запроса информации, которую она хочет, или выполнения требуемого поведения. Он также может вернуться к доменной службе, где служба домена выполнит запрос и настройку.
Если вы считаете, что ваш IDateTimeProvider на самом деле является доменной службой, как частью вездесущего языка, чем ваши сущности и агрегированные корни могут просто вызывать методы непосредственно, он просто не может содержать ссылку на него как поле класса, но локальные переменные параметров метода прекрасны.
Службы в DDD - это логические дампы, где логика не является натурной объект домена, правильно? Итак, как он склеивается?
Я думаю, что весь мой ответ уже сделал это довольно ясным. В принципе, у вас есть 3 возможности для склеивания всего (о чем я могу думать, по крайней мере, сейчас).
1) Служба приложений создает экземпляр доменной службы, вызывает на ней метод и передает возвращаемые значения в другое требуемое (репо, сущность, агрегированный корень, объект ценности, другую службу домена, фабрики и т.д.).
2) Служба домена создается экземпляром приложения и передается в качестве параметра методу того, что будет использовать его. Независимо от того, что использует его, он не сохраняет постоянную ссылку на него, это только локальная переменная.
3) Служба домена создается экземпляром домена приложения и передается в качестве параметра методу того, что будет использовать его. Независимо от того, использует ли он, используется двойная отправка, чтобы использовать доменную службу не зависимым образом. Это означает, что он передает методу Domain Service ссылку на себя, как в DomainService.DoSomething(это, имя, адрес).
Надеюсь, это поможет.
Комментарии приветствуются, если я сделал что-то неправильно или что противоречит рекомендациям DDD.
Ответ 4
Если я правильно понял ваш вопрос, вы указываете, что класс Product вызывает класс ProductService. Это не должно. Вы должны сделать это в классе factory, который отвечает за создание и настройку продукта. Когда вы вызываете этот метод, вы также можете зависеть от того, когда вы хотите опубликовать ProductId: у нас есть то, что может быть аналогичным случаем, поскольку нам нужно получить номер из нашей унаследованной системы учета для проекта. Я откладываю получение номера до тех пор, пока проект не будет сохранен, чтобы мы не теряли никаких цифр или имели пробелы. Если вы находитесь в аналогичной ситуации, вы можете захотеть выпустить ProductId в методе сохранения репозитория, а не при создании объекта.
Как вы считаете, вы действительно думаете, что у вас будет больше одного продукта или продукта? Если нет, я бы не стал беспокоиться об интерфейсах.
Отредактировано для добавления:
Я рекомендую начинать с малого и держать его простым, начиная с двух классов, Product и ProductServices. ProductServices будет выполнять все службы, включая factory и репозиторий, поскольку вы можете рассматривать их как специализированные службы.
Ответ 5
Зачем вам нужен идентификатор продукта при создании продукта в памяти? Обычно идентификатор продукта устанавливается при создании продукта в вашем репозитории.
Взгляните на следующий код:
var id1 = _repository.GetNextProductId();
var id2 = _repository.GetNextProductId();
Будет ли он возвращать два разных идентификатора продукта?
Если да, то это безопасно (но все же неудобно);
Если ответ отрицательный, тогда у вас будет огромная проблема;
Ответ 6
В согласии с Marcelo вам, вероятно, следует сначала определить, является ли идентификатор продукта действительно концепцией модели домена. Если бизнес-пользователи никогда не используют или не имеют представления об идентификаторе продукта и обычно ссылаются на продукт по имени, номеру, SKU или натуральному идентификатору продукта, составленному из имени + размеры, то это то, о чем должна знать модель домена.
Вот как я структурирую свои проекты DDD, предполагая, что идентификатор продукта является полем автоматического номера в базе данных:
Project.Business (модель домена)
Нет ссылок и, следовательно, никаких зависимостей от чего-либо.
public class Product : Entity
{
private Product(string name, Address address)
{
//set values.
}
//Factory method, even for simple ctor is used for encapsulation so we don't have
//to publically expose the constructor. What if we needed more than just a couple
//of value objects?
public static CreateNewProduct(string name, Address address)
{
return new Product(name, address);
}
public static GetAddress(string address, string city, string state, string zip) { }
}
public interface IProductRepository : IEnumerable<Product>
{
void Add(Product product);
//The following methods are extraneous, but included for completion sake.
int IndexOf(Product product);
Product this[int index] { get; set; }
}
Project.Implementation
public SqlProductRepository : List<ProductDataModel>, IProductRepository
{
public SqlProductRepository(string sqlConnectionString) { }
public void Add(Product product)
{
//Get new Id and save the product to the db.
}
public int IndexOf(Product product)
{
//Get the index of the base class and convert to business object.
}
public Product this[int index]
{
get { //find instance based on index and return; }
set { //find product ID based on index and save the passed in Business object to the database under that ID. }
}
}
Project.ApplicationName (уровень представления)
public class Application
{
IProductRepository repository = new SqlProductRepository(SqlConnectionString);
protected void Save_Click(object sender, EventArgs e)
{
Product newProduct = Product.CreateNewProduct(name, Product.GetAddress(address,city,state,zip));
repository.Add(newProduct);
}
}
При необходимости вы можете:
Project.Services (уровень приложений, который использует DTO между собой и уровнем презентации)