DDD (Domain Driven Design), how to handle entity state changes, and encapsulate business rules that requires large amount of data to be processed

后端 未结 4 886
萌比男神i
萌比男神i 2021-01-31 04:47
public class Person
{
    public IList SpecialBirthPlaces;
    public static readonly DateTime ImportantDate;
    public String BirthPlace {get;set;}

             


        
相关标签:
4条回答
  • 2021-01-31 04:54

    The following is a sample implementation. This implementation consists of several layers: the domain layer, the service layer and the presentation layer. This purpose of the service layer is to expose the functionality of your domain layer to other layers, such as the presentation layer or a web service. To that end, its methods correspond to specific commands that can be processed by the domain layer. In particular we have the command to change the birthday. Furthermore, this implementation uses Udi Dahan's version of a domain event framework. This is done to decouple the domain entity from the business logic associated with changing the birthday. This can be regarded as both a benefit and a drawback. The drawback is that your overall business logic is spread across multiple classes. The benefit is that you gain a lot of flexibility in how you handle domain events. Additionally, this approach is more extensible, since you can add subscribers to the BirthDateChangedEvent which perform auxiliary functions. Yet another benefit, (which contributed to the reasoning behind Udi's implementation) is that your Person entity no longer needs to be aware of any repositories, which seem outside the scope of the domain entity. Overall, this implementation calls for quite a bit of infrastructure, however if you envision investing greatly into your domain then it is worth the initial trouble. Also note, that this implementation assumed an ASP.NET MVC based presentation layer. In a stateful UI, the presentation logic would need to change and the ViewModel would need to provide change notifications.

    /// <summary>
    /// This is your main entity, while it may seem anemic, it is only because 
    /// it is simplistic.
    /// </summary>
    class Person
    {
        public string Id { get; set; }
        public string BirthPlace { get; set; }
    
        DateTime birthDate;
    
        public DateTime BirthDate
        {
            get { return this.birthDate; }
            set
            {
                if (this.birthDate != value)
                {
                    this.birthDate = value;
                    DomainEvents.Raise(new BirthDateChangedEvent(this.Id));
                }
            }
        }
    }
    
    /// <summary>
    /// Udi Dahan's implementation.
    /// </summary>
    static class DomainEvents
    {
        public static void Raise<TEvent>(TEvent e) where TEvent : IDomainEvent
        {
        }
    }
    
    interface IDomainEvent { }
    
    /// <summary>
    /// This is the interesting domain event which interested parties subscribe to 
    /// and handle in special ways.
    /// </summary>
    class BirthDateChangedEvent : IDomainEvent
    {
        public BirthDateChangedEvent(string personId)
        {
            this.PersonId = personId;
        }
    
        public string PersonId { get; private set; }
    }
    
    /// <summary>
    /// This can be associated to a Unit of Work.
    /// </summary>
    interface IPersonRepository
    {
        Person Get(string id);
        void Save(Person person);
    }
    
    /// <summary>
    /// This can implement caching for performance.
    /// </summary>
    interface IBirthPlaceRepository
    {
        bool IsSpecial(string brithPlace);
        string GetBirthPlaceFor(string birthPlace, DateTime birthDate);
    }
    
    interface IUnitOfWork : IDisposable
    {
        void Commit();
    }
    
    static class UnitOfWork
    {
        public static IUnitOfWork Start()
        {
            return null;
        }
    }
    
    class ChangeBirthDateCommand
    {
        public string PersonId { get; set; }
        public DateTime BirthDate { get; set; }
    }
    
    /// <summary>
    /// This is the application layer service which exposes the functionality of the domain 
    /// to the presentation layer.
    /// </summary>
    class PersonService
    {
        readonly IPersonRepository personDb;
    
        public void ChangeBirthDate(ChangeBirthDateCommand command)
        {
            // The service is a good place to initiate transactions, security checks, etc.
            using (var uow = UnitOfWork.Start())
            {
                var person = this.personDb.Get(command.PersonId);
                if (person == null)
                    throw new Exception();
    
                person.BirthDate = command.BirthDate;
    
                // or business logic can be handled here instead of having a handler.
    
                uow.Commit();
            }
        }
    }
    
    /// <summary>
    /// This view model is part of the presentation layer.
    /// </summary>
    class PersonViewModel
    {
        public PersonViewModel() { }
    
        public PersonViewModel(Person person)
        {
            this.BirthPlace = person.BirthPlace;
            this.BirthDate = person.BirthDate;
        }
    
        public string BirthPlace { get; set; }
        public DateTime BirthDate { get; set; }
    }
    
    /// <summary>
    /// This is part of the presentation layer.
    /// </summary>
    class PersonController
    {
        readonly PersonService personService;
        readonly IPersonRepository personDb;
    
        public void Show(string personId)
        {
            var person = this.personDb.Get(personId);
            var viewModel = new PersonViewModel(person);
            // UI framework code here.
        }
    
        public void HandleChangeBirthDate(string personId, DateTime birthDate)
        {
            this.personService.ChangeBirthDate(new ChangeBirthDateCommand { PersonId = personId, BirthDate = birthDate });
            Show(personId);
        }
    }
    
    interface IHandle<TEvent> where TEvent : IDomainEvent
    {
        void Handle(TEvent e);
    }
    
    /// <summary>
    /// This handler contains the business logic associated with changing birthdates. This logic may change
    /// and may depend on other factors.
    /// </summary>
    class BirthDateChangedBirthPlaceHandler : IHandle<BirthDateChangedEvent>
    {
        readonly IPersonRepository personDb;
        readonly IBirthPlaceRepository birthPlaceDb;
        readonly DateTime importantDate;
    
        public void Handle(BirthDateChangedEvent e)
        {
            var person = this.personDb.Get(e.PersonId);
            if (person == null)
                throw new Exception();
    
            if (person.BirthPlace != null && person.BirthDate < this.importantDate)
            {
                if (this.birthPlaceDb.IsSpecial(person.BirthPlace))
                {
                    person.BirthPlace = this.birthPlaceDb.GetBirthPlaceFor(person.BirthPlace, person.BirthDate);
                    this.personDb.Save(person);
                }
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-31 05:00

    IMO the best approach performance-wise would be to create a stored procedure in your db and mark entity on property changed event to invoke it when committing changes to db (SaveChanges() call). ObjectContext.ExecuteFunction is your friend in that case.

    Put all your logic of birthplace lookup and update in that sproc. Make sure sproc is included in the transaction - so that changes are rolled back if update fails.

    Edit: Sorry for not DDD related answer.

    0 讨论(0)
  • 2021-01-31 05:15

    I think the statements "I need to reflect this change in the user interface" and "It's an important domain rule which I need to capture" describe two different problems. Clearly, the first one needs to be solved; it isn't clear that the second one does.

    If other parts of your domain model need to know about changes here, you would do well to have a look at Domain Events (for example, Udi Dahan's implementation). You could also use this to set the BirthPlace property when the BirthDate gets set, even asynchronously if it is a potentially lengthy operation.

    Otherwise, let's just look at the UI issue. First of all, in my domain model, I would have each entity abstracted as an interface. If you don't, then you may need to at least make some properties virtual. I'd also be using a layer of abstraction for generation/returning my entities, such as IoC/factory/repository. I consider this layer to be outside the bounds of the domain model itself.

    Now, we need a mechanism to notify the UI of changes to properties in domain entities, but of course the domain model itself is in a sense a closed system: we don't want to introduce new members or behaviours to satisfy the needs of any outside concern.

    What if we decorate the entity in question with an implementation that implements INotifyPropertyChanged? We could do this in our repository, which we've established is outside the bounds of the domain, so we would not be modifying the domain model itself, only using composition to wrap the entities with functionality that the system outside the domain model needs. To restate, the recalculation of BirthPlace remains a concern of the domain model, while the UI notification logic remains a concern outside of the domain model.

    It would look something like this:

    public class NotifyPerson : IPerson, INotifyPropertyChanged
    {
        readonly IPerson _inner;
    
        public NotifyPerson(IPerson inner) // repository puts the "true" domain entity here
        {
            _inner = inner;
        }
    
        public DateTime BirthDate
        {
            set 
            {
                if(value == _inner.BirthDate)
                    return;
    
                var previousBirthPlace = BirthPlace;
                _inner.BirthDate = value;
                Notify("BirthDate");
    
                if(BirthPlace != previousBirthPlace) 
                    Notify("BirthPlace");
            }
        }
    
        void Notify(string property)
        {
            var handler = PropertyChanged;
            if(handler != null) handler(this, new PropertyChangedEventArgs(property));
        }
    }
    

    If not using interfaces, you would simply inherit from Person and override the BirthDate property, calling members on base instead of _inner.

    0 讨论(0)
  • 2021-01-31 05:17

    The way I've encapsulated this problem, which is modification tracking, is with the Unit of Work pattern. I have my DDD repositories associated with a unit of work, and I can query the unit of work for any set of entities which I get from any of the repositories to see which are modified.

    As for the large collection, it appears to be a read-only set. One way to handle this is to preload and cache this locally if it is ever accessed, then the repositories can run queries against the in-memory version. I use NHibernate, and it is easy to handle this case with it. If it is way too big to store in RAM (like 100s of MB or more), you'll probably need to special case repository queries against it, so that the SpecialBirthPlaces.Contains(BirthPlace) query is executed on the database (perhaps in a stored proc, ha!). You'd probably want to express SpecialBirthPlaces as a repository of entities, rather than just a big collection of strings, which would allow the "Query" pattern to free you from needing to load the entire thing.

    After this lengthy narrative, here's some example:

    public class BirthPlace
    {
        public String Name { get; set; }
    } 
    
    public class SpecialBirthPlace : BirthPlace
    {
    }
    
    public class Person 
    {
        public static readonly DateTime ImportantDate;
        public BirthPlace BirthPlace { get; set; } 
    
        public DateTime BirthDate 
        { 
            get; private set;
        } 
    
        public void CorrectBirthDate(IRepository<SpecialBirthPlace> specialBirthPlaces, DateTime date)
        {
            if (BirthPlace != null && date < ImportantDate && specialBirthPlaces.Contains(BirthPlace)) 
            { 
                BirthPlace = specialBirthPlaces.GetForDate(date); 
            }
        }
    } 
    

    Having a method where you pass in the corrected birth date is a better design since it tells you via the parameters what is needed to actually correct the birth date: a repository (i.e collection) of SpecialBirthPlace entities and the correct date. This explicit contract makes it clear what the domain is doing, and makes the business needs clear just by reading the entity contracts, where putting the whole collection in the state of the entity hides it.

    Now that we've made BirthPlace into an entity, we can see that there may be one more optimization to make the domain model a bit flatter. We don't really need to specialize BirthPlace but we do need to indicate if it is special. We can add a property to the object (some people begrudge properties on domain objects, but I don't, since it makes queries easier, especially with LINQ) to indicate if it is special. Then we can get rid of the Contains query altogether:

    public class BirthPlace
    {
        public BirthPlace(String name, Boolean isSpecial = false)
        {
            Name = name;
            IsSpecial = isSpecial
        } 
    
        public String Name { get; private set; }
        public Boolean IsSpecial { get; private set; }
    }
    
    public class Person 
    {
        public static readonly DateTime ImportantDate;
        public BirthPlace BirthPlace { get; set; } 
    
        public DateTime BirthDate 
        { 
            get; private set;
        } 
    
        public void CorrectBirthDate(IRepository<BirthPlace> birthPlaces, DateTime date)
        {
            if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial) 
            { 
                BirthPlace = birthPlaces.GetForDate(date); 
            }
        }
    } 
    
    0 讨论(0)
提交回复
热议问题