DDD - Зависимости между моделью домена, сервисами и репозиториями - PullRequest
16 голосов
/ 16 апреля 2009

Просто хотел узнать, как другие наслоили свою архитектуру. Скажем, у меня есть следующие слои:

Доменный уровень
--Product
--ProductService (Должен ли бес войти в этот слой?)
--IProductService
--IProductRepository

Уровень инфраструктуры
--ProductRepository (Imp из IProductRepository в моем домене)

Теперь, когда создается новый продукт, у меня есть требование назначить идентификатор продукта, вызвав метод ProductService.GetNextProductId ().

Поскольку служба зависит от хранилища, я настроил ctor ProductService с интерфейсом 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'ers

EDIT:

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

Я в замешательстве ... Откладывая ProductId, если моему классу Product нужны какие-то другие данные из службы, например, GetSystemDateTime () (я знаю, это плохой пример, но я пытаюсь продемонстрировать вызов не из БД), где будет этот метод службы называется?

Службы в DDD - это логические дампы, где логика не является естественной для объекта домена, верно? Так как же это склеить?

Ответы [ 6 ]

15 голосов
/ 17 апреля 2009

К вашему последнему пункту, сервисы в DDD - это место, где можно описать то, что я называю «неловкой» логикой. Если у вас есть какой-то тип логики или рабочий процесс, который зависит от других объектов, это тип логики, который обычно не «вписывается» в сам объект домена. Пример: если у меня есть бизнес-объект для выполнения некоторого типа проверки, класс сервиса может выполнить этот метод (сохраняя при этом фактическую логику проверки, связанную с сущностью внутри своего класса)

Другой действительно хороший пример, который я всегда упоминаю, - это метод перевода средств. У вас не будет возможности переносить объект учетной записи из одного объекта в другой, но вместо этого у вас будет служба, которая использует учетную запись «to» и учетную запись «from». Затем внутри сервиса вы будете вызывать метод снятия средств со своего счета «с» и метод депозита с вашего счета «на». Если вы попытаетесь поместить это внутри самой учетной записи, это будет неудобно.

Большой подкаст, в котором подробно рассказывается на эту тему, можно найти здесь . Дэвид Лариби делает действительно хорошую работу, объясняя теперь только «как», но «почему» DDD.

10 голосов
/ 16 апреля 2009

Ваша модель домена не должна иметь ссылки ни на ProductService, ни на IProductRepository. Если вы создаете новый продукт, он должен быть создан на фабрике - фабрика может использовать ProductService для получения идентификатора продукта.

Фактически я бы обернул ProductService соответствующим интерфейсом, таким как IProductIdGeneratorService, чтобы вы могли внедрить его на заводе с помощью контейнера IoC.

3 голосов
/ 01 мая 2014

Вот как я бы структурировал вашу проблему. Я считаю, что это также рекомендуемый способ 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 является частью вашей службы приложений, то есть точкой входа и выхода всего, что необходимо для использования вашего домена или пересечения его границ. Он отвечает за делегирование каждого варианта использования соответствующим компонентам, которые могут с ним работать, и координацию между всеми этими компонентами, если для выполнения сценария использования требуется много.

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

ProductIdGenerator является примером такой доменной службы. Доменные службы инкапсулируют логику поведения, которая выходит за пределы собственной границы сущности. Поэтому, если у вас есть логика, которая требует других совокупных корней или внешних сервисов, таких как репозиторий, файловая система, криптография и т. Д. По сути, любая логика, которую вы не можете тренировать изнутри вашей сущности, не нуждаясь ни в чем другом, вам может понадобиться доменная служба. Если логика заключается в том, что она занята и кажется, что концептуально она может не относиться к методу в вашей сущности, это признак того, что вам может понадобиться совершенно новый вариант использования службы приложений, либо вы пропустили сущность в своем дизайне. Также возможно использовать Службу домена непосредственно из Службы приложений, не используя двойной способ отправки. Это немного похоже на методы расширения C # по сравнению с обычным статическим методом.

=========== Ответить на ваши вопросы о редактировании ===============

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

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

Я не использовал заводской шаблон (пока) в качестве конструкции Объект все еще прост. Я не чувствую, что это оправдывает фабричный метод?

Зависит от того, что вы ожидаете, что вам потребуется больше времени на создание фабрики сейчас, даже если у вас нет логики множественного построения, или рефакторинг позже, когда вы это сделаете. Я думаю, что это не стоит того, чтобы сущности не нуждались в создании более чем одним способом. Как объясняет wikipedia , фабрика используется, чтобы сделать то, что каждый конструктор делает более явным и дифференцируемым. В моем примере фабрика MakeNew объясняет, для чего служит эта конкретная конструкция сущности: создать новый продукт. У вас может быть больше фабрики, такой как MakeExisting, MakeSample, MakeDeprecated и т. Д. Каждая из этих фабрик создаст Продукт, но для разных целей и немного по-разному. Без фабрики все эти конструкторы были бы названы Product (), и было бы трудно понять, какой из них предназначен для чего и для чего. Недостатком является то, что с Factory сложнее работать, когда вы расширяете вашу сущность, дочерняя сущность не может использовать родительскую Factory для создания дочерней, поэтому в любом случае я стараюсь выполнять всю конструкторскую логику внутри конструкторов и использовать только Factory иметь красивое имя для них.

Я в замешательстве ... Откладываю ProductId, если мой класс Product нужны некоторые другие данные из службы, например, GetSystemDateTime () (я знаю, плохой пример, но пытается продемонстрировать не вызов БД) где бы это метод обслуживания будет вызван?

Скажем, вы думали, что реализация Date - это деталь инфраструктуры. Вы должны создать вокруг него абстракцию для использования в своем приложении. Это будет начинаться с интерфейса, может быть, что-то вроде IDateTimeProvider. Этот интерфейс будет иметь метод GetSystemDateTime ().

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

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

Наконец, ваши Entites и Aggregate Roots и Value Object могут свободно вызывать GetSystemDateTime () и другие методы IDateTimeProvider, но не напрямую. Он должен был бы пройти двойную диспетчеризацию, где вы бы указали ему доменную службу в качестве параметра одного из его методов, и она бы использовала эту доменную службу для запроса необходимой информации или для выполнения требуемого поведения. Он также может передавать себя обратно в службу домена, где служба домена будет выполнять запросы и настройку.

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

Службы в DDD - это логические дампы, в которых логика не является естественной для доменный объект, верно? Так как же это склеить?

Я думаю, что весь мой ответ уже ясно показал это. По сути, у вас есть 3 варианта склеивания всего этого (о чем я могу думать, по крайней мере, сейчас).

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

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

3) Доменная служба создается в Прикладном домене и передается в качестве параметра методу того, что будет его использовать. Что бы оно ни использовало, использует двойную диспетчеризацию для использования доменной службы независимым образом. Это означает, что он передает методу доменной службы ссылку на себя, как в DomainService.DoSomething (this, name, Address).

Надеюсь, это поможет. Комментарии приветствуются, если я сделал что-то не так или это противоречит лучшим методикам DDD.

1 голос
/ 16 апреля 2009

Если я правильно понимаю ваш вопрос, вы утверждаете, что ваш класс Product вызывает класс ProductService. Это не должно Вы должны сделать это в фабричном классе, который отвечает за создание и настройку продукта. То, где вы вызываете этот метод, может также зависеть от того, когда вы хотите выдать ProductId: у нас есть аналогичный случай, когда нам нужно получить номер из нашей прежней системы учета для проекта. Я откладываю получение номера до тех пор, пока проект не будет сохранен, чтобы мы не теряли цифры и не оставляли пробелов. Если вы находитесь в аналогичной ситуации, вы можете использовать ProductId в методе сохранения в хранилище, а не при создании объекта.

Кроме того, действительно ли вы думаете, что у вас когда-нибудь будет более одного ProductService или ProductRepository? Если нет, то я не стал бы беспокоиться об интерфейсах.

Отредактировано, чтобы добавить:

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

0 голосов
/ 12 декабря 2012

По соглашению с 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 между собой и уровнем представления)

0 голосов
/ 12 декабря 2012

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

Взгляните на следующий код:

var id1 = _repository.GetNextProductId (); var id2 = _repository.GetNextProductId ();

Будет ли возвращено два разных идентификатора продукта?

Если ответ «да», то это безопасно (но все еще неудобно); Если ответ «нет», то у вас возникнет огромная проблема;

...